diff --git a/.changeset/fluffy-elephants-beg.md b/.changeset/fluffy-elephants-beg.md new file mode 100644 index 0000000000..0a4403c509 --- /dev/null +++ b/.changeset/fluffy-elephants-beg.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/slang": minor +--- + +rename `parser/Parser.supportedVersions()` API to `utils/LanguageFacts.supportedVersions()`. diff --git a/.changeset/gentle-onions-drum.md b/.changeset/gentle-onions-drum.md new file mode 100644 index 0000000000..a9e3df2839 --- /dev/null +++ b/.changeset/gentle-onions-drum.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/slang": minor +--- + +expose the `BingingGraph` API to allow querying definitions/references between source files. diff --git a/.changeset/pink-flowers-draw.md b/.changeset/pink-flowers-draw.md new file mode 100644 index 0000000000..0e013f2eaf --- /dev/null +++ b/.changeset/pink-flowers-draw.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/slang": minor +--- + +add a `CompilationBuilder` API to incrementally load and resolve source files and their imports. diff --git a/crates/codegen/runtime/cargo/crate/Cargo.toml b/crates/codegen/runtime/cargo/crate/Cargo.toml index dea69d8bb6..b15b372050 100644 --- a/crates/codegen/runtime/cargo/crate/Cargo.toml +++ b/crates/codegen/runtime/cargo/crate/Cargo.toml @@ -12,6 +12,7 @@ description = "Cargo runtime copied over by codegen" default = [] __experimental_bindings_api = ["dep:metaslang_bindings"] __private_ariadne_errors = ["dep:ariadne"] +__private_compilation_api = [] __private_testing_utils = [] [build-dependencies] diff --git a/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs b/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs new file mode 100644 index 0000000000..eee8b7880f --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs @@ -0,0 +1,12 @@ +use semver::Version; + +use crate::bindings::BindingGraph; +use crate::parser::ParserInitializationError; + +#[allow(clippy::needless_pass_by_value)] +pub fn add_built_ins( + _binding_graph: &mut BindingGraph, + _version: Version, +) -> Result<(), ParserInitializationError> { + unreachable!("Built-ins are Solidity-specific") +} diff --git a/crates/codegen/runtime/cargo/crate/src/extensions/compilation/mod.rs b/crates/codegen/runtime/cargo/crate/src/extensions/compilation/mod.rs new file mode 100644 index 0000000000..ddbb6d5a87 --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/extensions/compilation/mod.rs @@ -0,0 +1,15 @@ +use crate::cst::Cursor; + +pub struct ImportPathsExtractor; + +impl ImportPathsExtractor { + pub fn new() -> Self { + Self + } + + #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_value)] + pub fn extract(&self, _: Cursor) -> Vec { + unreachable!("Import paths are Solidity-specific") + } +} diff --git a/crates/codegen/runtime/cargo/crate/src/extensions/mod.rs b/crates/codegen/runtime/cargo/crate/src/extensions/mod.rs new file mode 100644 index 0000000000..78402f8764 --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/extensions/mod.rs @@ -0,0 +1,8 @@ +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; + +#[cfg(feature = "__experimental_bindings_api")] +pub mod bindings; diff --git a/crates/codegen/runtime/cargo/crate/src/lib.rs b/crates/codegen/runtime/cargo/crate/src/lib.rs index 115e9459b8..87bdec06cd 100644 --- a/crates/codegen/runtime/cargo/crate/src/lib.rs +++ b/crates/codegen/runtime/cargo/crate/src/lib.rs @@ -2,6 +2,7 @@ // The final code (generated in output crates) is checked for dead-code anyways. #![allow(dead_code, unused_imports)] +mod extensions; mod runtime; pub use runtime::*; diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/built_ins.rs.jinja2 b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/built_ins.rs.jinja2 index 856f983808..c28f4643e8 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/built_ins.rs.jinja2 +++ b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/built_ins.rs.jinja2 @@ -1,7 +1,8 @@ use semver::Version; +// TODO: This should be moved to the Solidity-specific 'extensions' sub-module. #[allow(unused_variables)] -pub fn get_contents(version: &Version) -> &'static str { +pub fn get_built_ins_contents(version: &Version) -> &'static str { {%- if not rendering_in_stubs -%} {%- for version in model.bindings.built_ins_versions %} {%- if not loop.first -%} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/generated/built_ins.rs b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/generated/built_ins.rs index f7337ecff2..e956dd160a 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/generated/built_ins.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/generated/built_ins.rs @@ -2,7 +2,8 @@ use semver::Version; +// TODO: This should be moved to the Solidity-specific 'extensions' sub-module. #[allow(unused_variables)] -pub fn get_contents(version: &Version) -> &'static str { +pub fn get_built_ins_contents(version: &Version) -> &'static str { unreachable!("Built-ins not defined in stubs") } 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 3769f5d1b1..4766b80e6c 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs @@ -2,31 +2,46 @@ mod binding_rules; #[path = "generated/built_ins.rs"] -mod built_ins; +pub mod built_ins; -use std::sync::Arc; +use std::rc::Rc; -use metaslang_bindings::{self, PathResolver}; use semver::Version; use crate::cst::KindTypes; -pub type Bindings = metaslang_bindings::Bindings; +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 BindingLocation = metaslang_bindings::BindingLocation; +pub type UserFileLocation = metaslang_bindings::UserFileLocation; + +pub use metaslang_bindings::{BuiltInLocation, PathResolver}; + +use crate::parser::ParserInitializationError; + +#[derive(thiserror::Error, Debug)] +pub enum BindingGraphInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} pub fn create_with_resolver( version: Version, - resolver: Arc, -) -> Bindings { - Bindings::create(version, binding_rules::BINDING_RULES_SOURCE, resolver) + resolver: Rc>, +) -> Result { + let mut binding_graph = BindingGraph::create( + version.clone(), + binding_rules::BINDING_RULES_SOURCE, + resolver, + ); + + crate::extensions::bindings::add_built_ins(&mut binding_graph, version)?; + + Ok(binding_graph) } #[cfg(feature = "__private_testing_utils")] pub fn get_binding_rules() -> &'static str { binding_rules::BINDING_RULES_SOURCE } - -pub fn get_built_ins(version: &semver::Version) -> &'static str { - built_ins::get_contents(version) -} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/file.rs b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/file.rs new file mode 100644 index 0000000000..07693c8335 --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/file.rs @@ -0,0 +1,45 @@ +use std::collections::BTreeMap; + +use metaslang_cst::text_index::TextIndex; + +use crate::cst::{Cursor, Node}; + +#[derive(Clone)] +pub struct File { + id: String, + tree: Node, + + resolved_imports: BTreeMap, +} + +impl File { + pub(super) fn new(id: String, tree: Node) -> Self { + Self { + id, + tree, + + resolved_imports: BTreeMap::new(), + } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn tree(&self) -> &Node { + &self.tree + } + + pub fn create_tree_cursor(&self) -> Cursor { + self.tree.clone().cursor_with_offset(TextIndex::ZERO) + } + + pub(super) fn resolve_import(&mut self, import_path: &Cursor, destination_file_id: String) { + self.resolved_imports + .insert(import_path.node().id(), destination_file_id); + } + + pub(super) fn resolved_import(&self, import_path: &Cursor) -> Option<&String> { + self.resolved_imports.get(&import_path.node().id()) + } +} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/internal_builder.rs b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/internal_builder.rs new file mode 100644 index 0000000000..f35bea3920 --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/internal_builder.rs @@ -0,0 +1,89 @@ +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::compilation::{CompilationUnit, File}; +use crate::cst::Cursor; +use crate::extensions::compilation::ImportPathsExtractor; +use crate::parser::{Parser, ParserInitializationError}; + +pub struct InternalCompilationBuilder { + parser: Parser, + imports: ImportPathsExtractor, + files: BTreeMap, +} + +#[derive(thiserror::Error, Debug)] +pub enum CompilationInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} + +impl InternalCompilationBuilder { + pub fn create(language_version: Version) -> Result { + let parser = Parser::create(language_version)?; + + Ok(Self { + parser, + imports: ImportPathsExtractor::new(), + files: BTreeMap::new(), + }) + } + + pub fn add_file(&mut self, id: String, contents: &str) -> AddFileResponse { + if self.files.contains_key(&id) { + // Already added. No need to process it again: + return AddFileResponse { + import_paths: vec![], + }; + } + + let parse_output = self.parser.parse(Parser::ROOT_KIND, contents); + + let import_paths = self.imports.extract(parse_output.create_tree_cursor()); + + let file = File::new(id.clone(), parse_output.tree().clone()); + self.files.insert(id, file); + + AddFileResponse { import_paths } + } + + pub fn resolve_import( + &mut self, + source_file_id: &str, + import_path: &Cursor, + destination_file_id: String, + ) -> Result<(), ResolveImportError> { + self.files + .get_mut(source_file_id) + .ok_or_else(|| ResolveImportError::SourceFileNotFound(source_file_id.to_owned()))? + .resolve_import(import_path, destination_file_id); + + Ok(()) + } + + pub fn build(&self) -> CompilationUnit { + let language_version = self.parser.language_version().to_owned(); + + let files = self + .files + .iter() + .map(|(id, file)| (id.to_owned(), Rc::new(file.to_owned()))) + .collect(); + + CompilationUnit::new(language_version, files) + } +} + +pub struct AddFileResponse { + pub import_paths: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum ResolveImportError { + #[error( + "Source file not found: '{0}'. Make sure to add it first, before resolving its imports." + )] + SourceFileNotFound(String), +} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/mod.rs b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/mod.rs new file mode 100644 index 0000000000..09d9cef65e --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/mod.rs @@ -0,0 +1,7 @@ +mod file; +mod internal_builder; +mod unit; + +pub use file::File; +pub use internal_builder::{AddFileResponse, InternalCompilationBuilder}; +pub use unit::CompilationUnit; diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs new file mode 100644 index 0000000000..aea2d3cd9c --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs @@ -0,0 +1,69 @@ +use std::cell::OnceCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::bindings::{ + create_with_resolver, BindingGraph, BindingGraphInitializationError, PathResolver, +}; +use crate::compilation::File; +use crate::cst::{Cursor, KindTypes}; + +pub struct CompilationUnit { + language_version: Version, + files: BTreeMap>, + binding_graph: OnceCell, BindingGraphInitializationError>>, +} + +impl CompilationUnit { + pub(super) fn new(language_version: Version, files: BTreeMap>) -> Self { + Self { + language_version, + files, + binding_graph: OnceCell::new(), + } + } + + pub fn language_version(&self) -> &Version { + &self.language_version + } + + pub fn files(&self) -> Vec> { + self.files.values().cloned().collect() + } + + pub fn file(&self, id: &str) -> Option> { + self.files.get(id).cloned() + } + + pub fn binding_graph(&self) -> &Result, BindingGraphInitializationError> { + self.binding_graph.get_or_init(|| { + let resolver = Resolver { + files: self.files.clone(), + }; + + let mut binding_graph = + 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()); + } + + Ok(Rc::new(binding_graph)) + }) + } +} + +struct Resolver { + files: BTreeMap>, +} + +impl PathResolver for Resolver { + fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option { + self.files + .get(context_path)? + .resolved_import(path_to_resolve) + .cloned() + } +} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/mod.rs b/crates/codegen/runtime/cargo/crate/src/runtime/mod.rs index 6856966831..b1ff783ba1 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/mod.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/mod.rs @@ -1,5 +1,11 @@ #[cfg(feature = "__experimental_bindings_api")] pub mod bindings; +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; pub mod cst; pub mod diagnostic; pub mod parser; +pub mod utils; diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/parser/generated/parser.rs b/crates/codegen/runtime/cargo/crate/src/runtime/parser/generated/parser.rs index d155877f4a..bc7976769f 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/parser/generated/parser.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/parser/generated/parser.rs @@ -27,10 +27,11 @@ use crate::parser::scanner_macros::{ scan_not_followed_by, scan_one_or_more, scan_optional, scan_sequence, scan_zero_or_more, }; use crate::parser::ParseOutput; +use crate::utils::LanguageFacts; #[derive(Debug)] pub struct Parser { - pub version: Version, + language_version: Version, } #[derive(thiserror::Error, Debug)] @@ -40,22 +41,25 @@ pub enum ParserInitializationError { } impl Parser { - pub const SUPPORTED_VERSIONS: &'static [Version] = &[]; - pub const ROOT_KIND: NonterminalKind = NonterminalKind::Stub1; - pub fn create(version: Version) -> std::result::Result { - if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { - Ok(Self { version }) + pub fn create( + language_version: Version, + ) -> std::result::Result { + if LanguageFacts::SUPPORTED_VERSIONS + .binary_search(&language_version) + .is_ok() + { + Ok(Self { language_version }) } else { Err(ParserInitializationError::UnsupportedLanguageVersion( - version, + language_version, )) } } - pub fn version(&self) -> &Version { - &self.version + pub fn language_version(&self) -> &Version { + &self.language_version } pub fn parse(&self, kind: NonterminalKind, input: &str) -> ParseOutput { diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parse_output.rs b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parse_output.rs index 77b6b157f5..4fbb7d45f1 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parse_output.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parse_output.rs @@ -1,15 +1,15 @@ use crate::cst::{Cursor, Node, TextIndex}; use crate::parser::ParseError; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseOutput { - pub(crate) parse_tree: Node, + pub(crate) tree: Node, pub(crate) errors: Vec, } impl ParseOutput { - pub fn tree(&self) -> Node { - self.parse_tree.clone() + pub fn tree(&self) -> &Node { + &self.tree } pub fn errors(&self) -> &Vec { @@ -22,6 +22,6 @@ impl ParseOutput { /// Creates a cursor that starts at the root of the parse tree. pub fn create_tree_cursor(&self) -> Cursor { - self.parse_tree.clone().cursor_with_offset(TextIndex::ZERO) + self.tree.clone().cursor_with_offset(TextIndex::ZERO) } } diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser.rs.jinja2 b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser.rs.jinja2 index a2c8b8cc2f..b5d5399f5d 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser.rs.jinja2 +++ b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser.rs.jinja2 @@ -11,6 +11,7 @@ use semver::Version; use crate::cst; +use crate::utils::LanguageFacts; use crate::cst::{ EdgeLabel, IsLexicalContext, LexicalContext, LexicalContextType, NonterminalKind, TerminalKind, }; @@ -37,7 +38,7 @@ pub struct Parser { {%- endfor -%} {%- endif -%} - pub version: Version, + language_version: Version, } #[derive(thiserror::Error, Debug)] @@ -47,34 +48,26 @@ pub enum ParserInitializationError { } impl Parser { - pub const SUPPORTED_VERSIONS: &'static [Version] = &[ - {%- if not rendering_in_stubs -%} - {% for version in model.all_language_versions %} - Version::new({{ version | split(pat=".") | join(sep=", ") }}), - {% endfor %} - {%- endif -%} - ]; - pub const ROOT_KIND: NonterminalKind = NonterminalKind::{{ model.kinds.root_kind }}; - pub fn create(version: Version) -> std::result::Result { - if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { + pub fn create(language_version: Version) -> std::result::Result { + if LanguageFacts::SUPPORTED_VERSIONS.binary_search(&language_version).is_ok() { Ok(Self { {%- if not rendering_in_stubs -%} {%- for version in model.breaking_language_versions %} - version_is_at_least_{{ version | replace(from=".", to="_") }}: Version::new({{ version | split(pat=".") | join(sep=", ") }}) <= version, + version_is_at_least_{{ version | replace(from=".", to="_") }}: Version::new({{ version | split(pat=".") | join(sep=", ") }}) <= language_version, {%- endfor -%} {%- endif -%} - version, + language_version, }) } else { - Err(ParserInitializationError::UnsupportedLanguageVersion(version)) + Err(ParserInitializationError::UnsupportedLanguageVersion(language_version)) } } - pub fn version(&self) -> &Version { - &self.version + pub fn language_version(&self) -> &Version { + &self.language_version } {%- if not rendering_in_stubs -%} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser_support/parser_function.rs b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser_support/parser_function.rs index f2ab664108..e291b1df49 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser_support/parser_function.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/parser/parser_support/parser_function.rs @@ -92,7 +92,7 @@ where Node::nonterminal(no_match.kind.unwrap(), trivia_nodes) }; ParseOutput { - parse_tree: tree, + tree, errors: vec![ParseError::new( start..start + input.into(), no_match.expected_terminals, @@ -156,18 +156,17 @@ where )); ParseOutput { - parse_tree: Node::nonterminal(topmost_node.kind, new_children), + tree: Node::nonterminal(topmost_node.kind, new_children), errors, } } else { - let parse_tree = Node::Nonterminal(topmost_node); + let tree = Node::Nonterminal(topmost_node); let errors = stream.into_errors(); // Sanity check: Make sure that succesful parse is equivalent to not having any invalid nodes debug_assert_eq!( errors.is_empty(), - parse_tree - .clone() + tree.clone() .cursor_with_offset(TextIndex::ZERO) .remaining_nodes() .all(|edge| edge @@ -176,7 +175,7 @@ where .is_none()) ); - ParseOutput { parse_tree, errors } + ParseOutput { tree, errors } } } } diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/utils/generated/language_facts.rs b/crates/codegen/runtime/cargo/crate/src/runtime/utils/generated/language_facts.rs new file mode 100644 index 0000000000..12af5c1f2e --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/utils/generated/language_facts.rs @@ -0,0 +1,11 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use semver::Version; + +pub struct LanguageFacts; + +impl LanguageFacts { + pub const NAME: &'static str = "CodegenRuntime"; + + pub const SUPPORTED_VERSIONS: &'static [Version] = &[]; +} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/utils/language_facts.rs.jinja2 b/crates/codegen/runtime/cargo/crate/src/runtime/utils/language_facts.rs.jinja2 new file mode 100644 index 0000000000..fe91f6ae7f --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/utils/language_facts.rs.jinja2 @@ -0,0 +1,13 @@ +use semver::Version; + +pub struct LanguageFacts; + +impl LanguageFacts { + pub const NAME: &'static str = "{{ model.language_name }}"; + + pub const SUPPORTED_VERSIONS: &'static [Version] = &[ + {% for version in model.all_language_versions %} + Version::new({{ version | split(pat=".") | join(sep=", ") }}), + {% endfor %} + ]; +} diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/utils/mod.rs b/crates/codegen/runtime/cargo/crate/src/runtime/utils/mod.rs new file mode 100644 index 0000000000..924814eded --- /dev/null +++ b/crates/codegen/runtime/cargo/crate/src/runtime/utils/mod.rs @@ -0,0 +1,4 @@ +#[path = "generated/language_facts.rs"] +mod language_facts; + +pub use language_facts::LanguageFacts; diff --git a/crates/codegen/runtime/cargo/wasm/Cargo.toml b/crates/codegen/runtime/cargo/wasm/Cargo.toml index bd8bcc7a90..65a9a1c454 100644 --- a/crates/codegen/runtime/cargo/wasm/Cargo.toml +++ b/crates/codegen/runtime/cargo/wasm/Cargo.toml @@ -17,7 +17,10 @@ codegen_runtime_generator = { workspace = true } infra_utils = { workspace = true } [dependencies] -codegen_runtime_cargo_crate = { workspace = true } +codegen_runtime_cargo_crate = { workspace = true, features = [ + "__experimental_bindings_api", + "__private_compilation_api", +] } paste = { workspace = true } semver = { workspace = true } serde_json = { workspace = true } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/bindings.rs.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/bindgen.rs.jinja2 similarity index 100% rename from crates/codegen/runtime/cargo/wasm/src/runtime/bindings.rs.jinja2 rename to crates/codegen/runtime/cargo/wasm/src/runtime/bindgen.rs.jinja2 diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/config.json.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/config.json.jinja2 index 395999cccb..3444eb9169 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/config.json.jinja2 +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/config.json.jinja2 @@ -1,5 +1,65 @@ { "mappings": { + "nomic-foundation:slang:bindings:definition.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.name-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.definiens-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:binding-location": { + "Variant": { + "as_direct_union_of_resource_classes": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.file-id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.cursor()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.language-version()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.binding-graph()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.tree()": { + "Function": { + "as_getter": true + } + }, "nomic-foundation:slang:cst:terminal-kind": { "Enum": { "as_typescript_enum": true @@ -105,7 +165,7 @@ "as_iterator": true } }, - "nomic-foundation:slang:parser:parser.version()": { + "nomic-foundation:slang:parser:parser.language-version()": { "Function": { "as_getter": true } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/generated/bindings.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/generated/bindgen.rs similarity index 100% rename from crates/codegen/runtime/cargo/wasm/src/runtime/generated/bindings.rs rename to crates/codegen/runtime/cargo/wasm/src/runtime/generated/bindgen.rs diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/generated/config.json b/crates/codegen/runtime/cargo/wasm/src/runtime/generated/config.json index 395999cccb..3444eb9169 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/generated/config.json +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/generated/config.json @@ -1,5 +1,65 @@ { "mappings": { + "nomic-foundation:slang:bindings:definition.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.name-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.definiens-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:binding-location": { + "Variant": { + "as_direct_union_of_resource_classes": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.file-id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.cursor()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.language-version()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.binding-graph()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.tree()": { + "Function": { + "as_getter": true + } + }, "nomic-foundation:slang:cst:terminal-kind": { "Enum": { "as_typescript_enum": true @@ -105,7 +165,7 @@ "as_iterator": true } }, - "nomic-foundation:slang:parser:parser.version()": { + "nomic-foundation:slang:parser:parser.language-version()": { "Function": { "as_getter": true } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/bindings.wit.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/bindings.wit.jinja2 new file mode 100644 index 0000000000..d1586c4fdb --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/bindings.wit.jinja2 @@ -0,0 +1,70 @@ +interface bindings { + use cst.{cursor}; + + /// A giant graph that contains name binding information for all source files within the compilation unit. + /// It stores cursors to all definitions and references, and can resolve the edges between them. + resource binding-graph { + /// If the provided cursor points at a definition `Identifier`, it will return the + /// corresponding definition. Otherwise, it will return `undefined`. + definition-at: func(cursor: borrow) -> option; + + /// If the provided cursor points at a reference `Identifier`, it will return the + /// corresponding reference. Otherwise, it will return `undefined`. + reference-at: func(cursor: borrow) -> option; + } + + /// Represents a definition in the binding graph. + resource definition { + /// Returns a unique numerical identifier of the definition. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the definition's name. + /// For `contract X {}`, that is the location of the `X` `Identifier` node. + name-location: func() -> binding-location; + + /// Returns the location of the definition's definiens. + /// For `contract X {}`, that is the location of the parent `ContractDefinition` node. + definiens-location: func() -> binding-location; + } + + /// Represents a reference in the binding graph. + resource reference { + /// Returns a unique numerical identifier of the reference. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the reference. + /// For `new X()`, that is the location of the `X` `Identifier` node. + location: func() -> binding-location; + + /// Returns a list of all definitions related to this reference. + /// Most references have a single definition, but some have multiple, such as when a symbol + /// is imported from another file, and renamed (re-defined) in the current file. + definitions: func() -> list; + } + + /// Represents a location of a symbol (definition or reference) in the binding graph. + /// It can either be in a user file, or a built-in in the language. + variant binding-location { + /// Represents a location of a user-defined symbol in a user file. + user-file(user-file-location), + /// Represents a location of a built-in symbol in the language. + built-in(built-in-location) + } + + /// Represents a location of a user-defined symbol in a user file. + resource user-file-location { + /// Returns the ID of the file that contains the symbol. + file-id: func() -> string; + + /// Returns a cursor to the CST node that contains the symbol. + cursor: func() -> cursor; + } + + /// Represents a location of a built-in symbol in the language. + resource built-in-location { + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/compilation.wit.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/compilation.wit.jinja2 new file mode 100644 index 0000000000..1436368ad1 --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/compilation.wit.jinja2 @@ -0,0 +1,66 @@ +interface compilation { + use bindings.{binding-graph}; + use cst.{node, cursor}; + + /// A builder for creating compilation units. + /// Allows incrementally building a transitive list of all files and their imports. + /// + /// This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + /// This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + resource internal-compilation-builder { + /// Creates a new compilation builder for the specified language version. + create: static func(language-version: string) -> result; + + /// Adds a source file to the compilation unit. + add-file: func(id: string, contents: string) -> add-file-response; + + /// Resolves an import in the source file to the destination file. + resolve-import: func(source-file-id: string, import-path: borrow, destination-file-id: string) -> result<_, string>; + + /// Builds and returns the final compilation unit. + build: func() -> compilation-unit; + } + + /// Contains information about imports found in an added source file. + record add-file-response { + /// List of cursors to any import paths found in the file. + import-paths: list, + } + + /// A complete compilation unit is a complete view over all compilation inputs: + /// + /// - All source files, stored as CSTs. + /// - Name binding graph that exposes relationships between definitions and references in these files. + /// - Any relevant compilation options. + /// + /// It also exposes utilities to traverse the compilation unit and query it. + resource compilation-unit { + /// Returns the language version this compilation unit is configured for. + language-version: func() -> string; + + /// Returns a list of all files in the compilation unit. + files: func() -> list; + + /// Returns the file with the specified ID, if it exists. + file: func(id: string) -> option; + + /// Calculates name binding information for all source files within the compilation unit. + /// Returns a graph that contains all found definitions and their references. + /// + /// Note: building this graph is an expensive operation. + /// It is done lazily on the first access, and cached thereafter. + binding-graph: func() -> result; + } + + /// A single source file in the compilation unit. + resource file { + /// Returns the unique identifier of this file. + id: func() -> string; + + /// Returns the syntax tree of this file. + tree: func() -> node; + + /// Creates a cursor for traversing the syntax tree of this file. + create-tree-cursor: func() -> cursor; + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/bindings.wit b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/bindings.wit new file mode 100644 index 0000000000..1721afc82d --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/bindings.wit @@ -0,0 +1,72 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface bindings { + use cst.{cursor}; + + /// A giant graph that contains name binding information for all source files within the compilation unit. + /// It stores cursors to all definitions and references, and can resolve the edges between them. + resource binding-graph { + /// If the provided cursor points at a definition `Identifier`, it will return the + /// corresponding definition. Otherwise, it will return `undefined`. + definition-at: func(cursor: borrow) -> option; + + /// If the provided cursor points at a reference `Identifier`, it will return the + /// corresponding reference. Otherwise, it will return `undefined`. + reference-at: func(cursor: borrow) -> option; + } + + /// Represents a definition in the binding graph. + resource definition { + /// Returns a unique numerical identifier of the definition. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the definition's name. + /// For `contract X {}`, that is the location of the `X` `Identifier` node. + name-location: func() -> binding-location; + + /// Returns the location of the definition's definiens. + /// For `contract X {}`, that is the location of the parent `ContractDefinition` node. + definiens-location: func() -> binding-location; + } + + /// Represents a reference in the binding graph. + resource reference { + /// Returns a unique numerical identifier of the reference. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the reference. + /// For `new X()`, that is the location of the `X` `Identifier` node. + location: func() -> binding-location; + + /// Returns a list of all definitions related to this reference. + /// Most references have a single definition, but some have multiple, such as when a symbol + /// is imported from another file, and renamed (re-defined) in the current file. + definitions: func() -> list; + } + + /// Represents a location of a symbol (definition or reference) in the binding graph. + /// It can either be in a user file, or a built-in in the language. + variant binding-location { + /// Represents a location of a user-defined symbol in a user file. + user-file(user-file-location), + /// Represents a location of a built-in symbol in the language. + built-in(built-in-location) + } + + /// Represents a location of a user-defined symbol in a user file. + resource user-file-location { + /// Returns the ID of the file that contains the symbol. + file-id: func() -> string; + + /// Returns a cursor to the CST node that contains the symbol. + cursor: func() -> cursor; + } + + /// Represents a location of a built-in symbol in the language. + resource built-in-location { + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/compilation.wit b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/compilation.wit new file mode 100644 index 0000000000..c9db286d76 --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/compilation.wit @@ -0,0 +1,68 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface compilation { + use bindings.{binding-graph}; + use cst.{node, cursor}; + + /// A builder for creating compilation units. + /// Allows incrementally building a transitive list of all files and their imports. + /// + /// This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + /// This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + resource internal-compilation-builder { + /// Creates a new compilation builder for the specified language version. + create: static func(language-version: string) -> result; + + /// Adds a source file to the compilation unit. + add-file: func(id: string, contents: string) -> add-file-response; + + /// Resolves an import in the source file to the destination file. + resolve-import: func(source-file-id: string, import-path: borrow, destination-file-id: string) -> result<_, string>; + + /// Builds and returns the final compilation unit. + build: func() -> compilation-unit; + } + + /// Contains information about imports found in an added source file. + record add-file-response { + /// List of cursors to any import paths found in the file. + import-paths: list, + } + + /// A complete compilation unit is a complete view over all compilation inputs: + /// + /// - All source files, stored as CSTs. + /// - Name binding graph that exposes relationships between definitions and references in these files. + /// - Any relevant compilation options. + /// + /// It also exposes utilities to traverse the compilation unit and query it. + resource compilation-unit { + /// Returns the language version this compilation unit is configured for. + language-version: func() -> string; + + /// Returns a list of all files in the compilation unit. + files: func() -> list; + + /// Returns the file with the specified ID, if it exists. + file: func(id: string) -> option; + + /// Calculates name binding information for all source files within the compilation unit. + /// Returns a graph that contains all found definitions and their references. + /// + /// Note: building this graph is an expensive operation. + /// It is done lazily on the first access, and cached thereafter. + binding-graph: func() -> result; + } + + /// A single source file in the compilation unit. + resource file { + /// Returns the unique identifier of this file. + id: func() -> string; + + /// Returns the syntax tree of this file. + tree: func() -> node; + + /// Creates a cursor for traversing the syntax tree of this file. + create-tree-cursor: func() -> cursor; + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/parser.wit b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/parser.wit index bbe8faa7c8..df41c2e0fc 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/parser.wit +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/parser.wit @@ -10,15 +10,11 @@ interface parser { /// This represents the starting point for parsing a complete source file. root-kind: static func() -> nonterminal-kind; - /// Returns a list of language versions supported by this parser. - /// Each version string represents a specific grammar configuration. - supported-versions: static func() -> list; - /// Creates a new parser instance for the specified language version. - create: static func(version: string) -> result; + create: static func(language-version: string) -> result; /// Returns the language version this parser instance is configured for. - version: func() -> string; + language-version: func() -> string; /// Parses the input string starting from the specified nonterminal kind. parse: func(kind: nonterminal-kind, input: string) -> parse-output; diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/utils.wit b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/utils.wit new file mode 100644 index 0000000000..9bb93e160f --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/utils.wit @@ -0,0 +1,9 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface utils { + /// Provides information about the supported language versions and the grammar. + resource language-facts { + /// Returns a list of language versions supported by Slang, sorted ascendingly. + supported-versions: static func() -> list; + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/world.wit b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/world.wit index c5e1378860..bb9b7b10be 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/world.wit +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/generated/world.wit @@ -4,6 +4,9 @@ package nomic-foundation:slang; world slang { export ast; + export bindings; + export compilation; export cst; export parser; + export utils; } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/parser.wit.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/parser.wit.jinja2 index dd9ddccb28..3a155a6535 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/parser.wit.jinja2 +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/parser.wit.jinja2 @@ -8,15 +8,11 @@ interface parser { /// This represents the starting point for parsing a complete source file. root-kind: static func() -> nonterminal-kind; - /// Returns a list of language versions supported by this parser. - /// Each version string represents a specific grammar configuration. - supported-versions: static func() -> list; - /// Creates a new parser instance for the specified language version. - create: static func(version: string) -> result; + create: static func(language-version: string) -> result; /// Returns the language version this parser instance is configured for. - version: func() -> string; + language-version: func() -> string; /// Parses the input string starting from the specified nonterminal kind. parse: func(kind: nonterminal-kind, input: string) -> parse-output; diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/utils.wit.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/utils.wit.jinja2 new file mode 100644 index 0000000000..7becc47d08 --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/utils.wit.jinja2 @@ -0,0 +1,7 @@ +interface utils { + /// Provides information about the supported language versions and the grammar. + resource language-facts { + /// Returns a list of language versions supported by Slang, sorted ascendingly. + supported-versions: static func() -> list; + } +} diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/world.wit.jinja2 b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/world.wit.jinja2 index 395b2ec702..fe488e5c0f 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/interface/world.wit.jinja2 +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/interface/world.wit.jinja2 @@ -2,6 +2,9 @@ package nomic-foundation:slang; world slang { export ast; + export bindings; + export compilation; export cst; export parser; + export utils; } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/mod.rs index ed55e0e62e..bf7b06b7f7 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/mod.rs @@ -1,8 +1,8 @@ -#[path = "./generated/bindings.rs"] -mod bindings; +#[path = "./generated/bindgen.rs"] +mod bindgen; mod utils; mod wrappers; struct World; -crate::wasm_crate::bindings::export!(World with_types_in crate::wasm_crate::bindings); +crate::wasm_crate::bindgen::export!(World with_types_in crate::wasm_crate::bindgen); diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/ast/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/ast/mod.rs index 10b4fb6b87..b6bcb389e2 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/ast/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/ast/mod.rs @@ -4,10 +4,10 @@ mod selectors; use crate::wasm_crate::utils::IntoFFI; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::ast::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::ast::{ Guest, GuestSelectors, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Node, NonterminalNodeBorrow, }; } 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 new file mode 100644 index 0000000000..c093fb0b7c --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/bindings/mod.rs @@ -0,0 +1,174 @@ +use crate::wasm_crate::utils::{define_rc_wrapper, define_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::{ + BindingGraph, BindingGraphBorrow, BindingLocation, BuiltInLocation, BuiltInLocationBorrow, + CursorBorrow, Definition, DefinitionBorrow, Guest, GuestBindingGraph, GuestBuiltInLocation, + GuestDefinition, GuestReference, GuestUserFileLocation, Reference, ReferenceBorrow, + UserFileLocation, UserFileLocationBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::Cursor; +} + +mod rust { + pub use crate::rust_crate::bindings::{ + BindingGraph, BindingLocation, BuiltInLocation, 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 { + type BindingGraph = BindingGraphWrapper; + + type Definition = DefinitionWrapper; + type Reference = ReferenceWrapper; + + type UserFileLocation = UserFileLocationWrapper; + type BuiltInLocation = BuiltInLocationWrapper; +} + +//================================================ +// +// resource binding-graph +// +//================================================ + +define_rc_wrapper! { BindingGraph { + fn definition_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .definition_at(&cursor._borrow_ffi()) + .map(rust::Definition::from) + .map(IntoFFI::_into_ffi) + } + + fn reference_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .reference_at(&cursor._borrow_ffi()) + .map(rust::Reference::from) + .map(IntoFFI::_into_ffi) + } +} } + +//================================================ +// +// resource definition +// +//================================================ + +define_wrapper! { Definition { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn name_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().name_location.clone()._into_ffi() + } + + fn definiens_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().definiens_location.clone()._into_ffi() + } +} } + +//================================================ +// +// resource reference +// +//================================================ + +define_wrapper! { Reference { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn location(&self) -> ffi::BindingLocation { + self._borrow_ffi().location.clone()._into_ffi() + } + + fn definitions(&self) -> Vec { + self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + } +} } + +//================================================ +// +// variant binding-location +// +//================================================ + +impl IntoFFI for rust::BindingLocation { + #[inline] + fn _into_ffi(self) -> ffi::BindingLocation { + match self { + Self::BuiltIn(location) => ffi::BindingLocation::BuiltIn(location._into_ffi()), + Self::UserFile(location) => ffi::BindingLocation::UserFile(location._into_ffi()), + } + } +} + +//================================================ +// +// resource user-file-location +// +//================================================ + +define_wrapper! { UserFileLocation { + fn file_id(&self) -> String { + self._borrow_ffi().file_id().to_owned() + } + + fn cursor(&self) -> ffi::Cursor { + self._borrow_ffi().cursor().to_owned()._into_ffi() + } +} } + +//================================================ +// +// resource built-in-location +// +//================================================ + +define_wrapper! { BuiltInLocation { +} } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/compilation/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/compilation/mod.rs new file mode 100644 index 0000000000..3ecc07023c --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/compilation/mod.rs @@ -0,0 +1,120 @@ +use std::rc::Rc; + +use semver::Version; + +use crate::wasm_crate::utils::{define_rc_wrapper, define_refcell_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::BindingGraph; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::compilation::{ + AddFileResponse, CompilationUnit, CompilationUnitBorrow, CursorBorrow, File, FileBorrow, + Guest, GuestCompilationUnit, GuestFile, GuestInternalCompilationBuilder, + InternalCompilationBuilder, InternalCompilationBuilderBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{Cursor, Node}; +} + +mod rust { + pub use crate::rust_crate::compilation::{ + AddFileResponse, CompilationUnit, File, InternalCompilationBuilder, + }; +} + +impl ffi::Guest for crate::wasm_crate::World { + type InternalCompilationBuilder = InternalCompilationBuilderWrapper; + type CompilationUnit = CompilationUnitWrapper; + type File = FileWrapper; +} + +//================================================ +// +// resource internal-compilation-builder +// +//================================================ + +define_refcell_wrapper! { InternalCompilationBuilder { + fn create(language_version: String) -> Result { + let language_version = Version::parse(&language_version).map_err(|e| e.to_string())?; + + rust::InternalCompilationBuilder::create(language_version) + .map(IntoFFI::_into_ffi) + .map_err(|e| e.to_string()) + } + + fn add_file(&self, id: String, contents: String) -> ffi::AddFileResponse { + self._borrow_mut_ffi() + .add_file(id, &contents) + ._into_ffi() + } + + fn resolve_import(&self, source_file_id: String, import_path: ffi::CursorBorrow<'_>, destination_file_id: String) -> Result<(), String> { + self._borrow_mut_ffi() + .resolve_import(&source_file_id, &import_path._borrow_ffi(), destination_file_id) + .map_err(|e| e.to_string()) + } + + fn build(&self) -> ffi::CompilationUnit { + Rc::new(self._borrow_ffi().build())._into_ffi() + } +} } + +//================================================ +// +// record add-file-response +// +//================================================ + +impl IntoFFI for rust::AddFileResponse { + #[inline] + fn _into_ffi(self) -> ffi::AddFileResponse { + let Self { import_paths } = self; + + ffi::AddFileResponse { + import_paths: import_paths.into_iter().map(IntoFFI::_into_ffi).collect(), + } + } +} + +//================================================ +// +// resource compilation-unit +// +//================================================ + +define_rc_wrapper! { CompilationUnit { + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() + } + + fn files(&self) -> Vec { + self._borrow_ffi().files().into_iter().map(IntoFFI::_into_ffi).collect() + } + + fn file(&self, id: String) -> Option { + self._borrow_ffi().file(&id).map(IntoFFI::_into_ffi) + } + + fn binding_graph(&self) -> Result { + self._borrow_ffi().binding_graph().as_ref().map(Rc::clone).map(IntoFFI::_into_ffi).map_err(|e| e.to_string()) + } +} } + +//================================================ +// +// resource file +// +//================================================ + +define_rc_wrapper! { File { + fn id(&self) -> String { + self._borrow_ffi().id().to_owned() + } + + fn tree(&self) -> ffi::Node { + self._borrow_ffi().tree().to_owned()._into_ffi() + } + + fn create_tree_cursor(&self) -> ffi::Cursor { + self._borrow_ffi().create_tree_cursor()._into_ffi() + } +} } diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/cst/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/cst/mod.rs index c0e31cdba3..0f93bf33a1 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/cst/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/cst/mod.rs @@ -5,7 +5,7 @@ use crate::wasm_crate::utils::{ }; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ AncestorsIterator, AncestorsIteratorBorrow, Cursor, CursorBorrow, CursorIterator, CursorIteratorBorrow, Edge, EdgeLabel, Guest, GuestAncestorsIterator, GuestCursor, GuestCursorIterator, GuestNonterminalNode, GuestQuery, GuestQueryMatchIterator, diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/mod.rs index 26cb73d858..f477b99957 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/mod.rs @@ -1,3 +1,6 @@ mod ast; +mod bindings; +mod compilation; mod cst; mod parser; +mod utils; diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/parser/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/parser/mod.rs index 4add8de247..679567d03e 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/parser/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/parser/mod.rs @@ -1,10 +1,10 @@ use crate::wasm_crate::utils::{define_wrapper, FromFFI, IntoFFI}; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Cursor, Node, TextRange, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::parser::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::parser::{ Guest, GuestParseError, GuestParseOutput, GuestParser, NonterminalKind, ParseError, ParseErrorBorrow, ParseOutput, ParseOutputBorrow, Parser, ParserBorrow, }; @@ -31,22 +31,15 @@ define_wrapper! { Parser { rust::Parser::ROOT_KIND._into_ffi() } - fn supported_versions() -> Vec { - rust::Parser::SUPPORTED_VERSIONS - .iter() - .map(|v| v.to_string()) - .collect() - } - - fn create(version: String) -> Result { - semver::Version::parse(&version) - .map_err(|_| format!("Invalid semantic version: '{version}'")) + fn create(language_version: String) -> Result { + semver::Version::parse(&language_version) + .map_err(|_| format!("Invalid semantic version: '{language_version}'")) .and_then(|version| rust::Parser::create(version).map_err(|e| e.to_string())) .map(IntoFFI::_into_ffi) } - fn version(&self) -> String { - self._borrow_ffi().version.to_string() + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() } fn parse(&self, kind: ffi::NonterminalKind, input: String) -> ffi::ParseOutput { @@ -78,7 +71,7 @@ define_wrapper! { ParseError { define_wrapper! { ParseOutput { fn tree(&self) -> ffi::Node { - self._borrow_ffi().tree()._into_ffi() + self._borrow_ffi().tree().clone()._into_ffi() } fn errors(&self) -> Vec { diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/utils/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/utils/mod.rs new file mode 100644 index 0000000000..a621d61d36 --- /dev/null +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/utils/mod.rs @@ -0,0 +1,30 @@ +use crate::wasm_crate::utils::define_wrapper; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::utils::{ + Guest, GuestLanguageFacts, LanguageFacts, LanguageFactsBorrow, + }; +} + +mod rust { + pub use crate::rust_crate::utils::LanguageFacts; +} + +impl ffi::Guest for crate::wasm_crate::World { + type LanguageFacts = LanguageFactsWrapper; +} + +//================================================ +// +// resource language-facts +// +//================================================ + +define_wrapper! { LanguageFacts { + fn supported_versions() -> Vec { + rust::LanguageFacts::SUPPORTED_VERSIONS + .iter() + .map(|v| v.to_string()) + .collect() + } +} } diff --git a/crates/codegen/runtime/generator/src/lib.rs b/crates/codegen/runtime/generator/src/lib.rs index 786747c84c..1c47a92c6f 100644 --- a/crates/codegen/runtime/generator/src/lib.rs +++ b/crates/codegen/runtime/generator/src/lib.rs @@ -63,6 +63,7 @@ struct ModelWrapper { #[derive(Serialize)] struct RuntimeModel { slang_version: Version, + language_name: String, all_language_versions: BTreeSet, breaking_language_versions: BTreeSet, @@ -76,6 +77,7 @@ impl RuntimeModel { fn from_language(language: &Rc) -> Result { Ok(Self { slang_version: CargoWorkspace::local_version()?, + language_name: language.name.to_string(), all_language_versions: language.versions.iter().cloned().collect(), breaking_language_versions: language.collect_breaking_versions(), @@ -91,6 +93,7 @@ impl Default for RuntimeModel { fn default() -> Self { Self { slang_version: Version::new(0, 0, 0), + language_name: "CodegenRuntime".to_string(), all_language_versions: BTreeSet::default(), breaking_language_versions: BTreeSet::default(), diff --git a/crates/codegen/runtime/npm/package/package.json b/crates/codegen/runtime/npm/package/package.json index 00c41a6001..86c8e58339 100644 --- a/crates/codegen/runtime/npm/package/package.json +++ b/crates/codegen/runtime/npm/package/package.json @@ -6,8 +6,11 @@ "exports": { ".": "./target/generated/index.mjs", "./ast": "./target/generated/ast/index.mjs", + "./bindings": "./target/generated/bindings/index.mjs", + "./compilation": "./target/generated/compilation/index.mjs", "./cst": "./target/generated/cst/index.mjs", - "./parser": "./target/generated/parser/index.mjs" + "./parser": "./target/generated/parser/index.mjs", + "./utils": "./target/generated/utils/index.mjs" }, "__dependencies_comment__": "__SLANG_NPM_PACKAGE_DEPENDENCIES__ (keep in sync)", "dependencies": { diff --git a/crates/codegen/runtime/npm/package/src/runtime/bindings/index.mts b/crates/codegen/runtime/npm/package/src/runtime/bindings/index.mts new file mode 100644 index 0000000000..f5855d023e --- /dev/null +++ b/crates/codegen/runtime/npm/package/src/runtime/bindings/index.mts @@ -0,0 +1,21 @@ +import * as generated from "../../../wasm/index.mjs"; + +export const BindingGraph = generated.bindings.BindingGraph; +export type BindingGraph = generated.bindings.BindingGraph; + +export const Definition = generated.bindings.Definition; +export type Definition = generated.bindings.Definition; + +export const Reference = generated.bindings.Reference; +export type Reference = generated.bindings.Reference; + +export type BindingLocation = generated.bindings.BindingLocation; + +export const BindingLocationType = generated.bindings.BindingLocationType; +export type BindingLocationType = generated.bindings.BindingLocationType; + +export const UserFileLocation = generated.bindings.UserFileLocation; +export type UserFileLocation = generated.bindings.UserFileLocation; + +export const BuiltInLocation = generated.bindings.BuiltInLocation; +export type BuiltInLocation = generated.bindings.BuiltInLocation; diff --git a/crates/codegen/runtime/npm/package/src/runtime/compilation/builder.mts b/crates/codegen/runtime/npm/package/src/runtime/compilation/builder.mts new file mode 100644 index 0000000000..029be84e07 --- /dev/null +++ b/crates/codegen/runtime/npm/package/src/runtime/compilation/builder.mts @@ -0,0 +1,112 @@ +import { Cursor } from "../cst/index.mjs"; +import { CompilationUnit } from "./index.mjs"; + +import * as generated from "../../../wasm/index.mjs"; + +const InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; +type InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; + +/** + * User-provided options and callbacks necessary for the `CompilationBuilder` class to perform its job. + */ +export interface CompilationBuilderConfig { + /** + * The language version to parse files with. + */ + languageVersion: string; + + /** + * Callback used by this builder to load the contents of a file. + * + * The user is responsible for fetching the file from the filesystem. + * If the file is not found, the callback should return undefined. + * Any errors thrown by the callback will be propagated to the caller. + */ + readFile: (fileId: string) => Promise; + + /** + * Callback used by this builder to resolve an import path. + * For example, if a source file contains the following statement: + * + * ```solidity + * import {Foo} from "foo.sol"; + * ``` + * + * Then the API will invoke the callback with a cursor pointing to the `"foo.sol"` string literal. + * + * The user is responsible for resolving it to a file in the compilation, and return its ID. + * If the callback returns `undefined`, the import will stay unresolved. + * Any errors thrown by the callback will be propagated to the caller. + */ + resolveImport: (sourceFileId: string, importPath: Cursor) => Promise; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a list of all files and their imports. + */ +export class CompilationBuilder { + private readonly seenFiles: Set = new Set(); + + private constructor( + private readonly internalBuilder: InternalCompilationBuilder, + + /** + * The user-supplied configuration. + */ + public readonly config: CompilationBuilderConfig, + ) {} + + /** + * Creates a new compilation builder for the specified language version. + */ + public static create(config: CompilationBuilderConfig): CompilationBuilder { + const internalBuilder = InternalCompilationBuilder.create(config.languageVersion); + return new CompilationBuilder(internalBuilder, config); + } + + /** + * Adds a source file to the compilation unit. + * Typically, users only need to add the "root" file, which contains the main contract they are trying to analyze. + * Any files that are imported by the root file will be discovered and loaded automatically by the config callbacks. + * + * Adding multiple files (roots) is supported. For example, an IDE can choose to add all NPM dependencies, + * regardless of whether they are imported or not, to be able to query the definitions there. + * + * Adding a file that has already been added is a no-op. + */ + public async addFile(id: string): Promise { + if (this.seenFiles.has(id)) { + return; + } else { + this.seenFiles.add(id); + } + + const contents = await this.config.readFile(id); + if (contents === undefined) { + return; + } + + const { importPaths } = this.internalBuilder.addFile(id, contents); + + await Promise.all( + importPaths.map(async (importPath) => { + const destinationFileId = await this.config.resolveImport(id, importPath); + if (destinationFileId === undefined) { + return; + } + + this.internalBuilder.resolveImport(id, importPath, destinationFileId); + + await this.addFile(destinationFileId); + }), + ); + } + + /** + * Builds and returns the final compilation unit. + */ + public build(): CompilationUnit { + return this.internalBuilder.build(); + } +} diff --git a/crates/codegen/runtime/npm/package/src/runtime/compilation/index.mts b/crates/codegen/runtime/npm/package/src/runtime/compilation/index.mts new file mode 100644 index 0000000000..ef9fb5201e --- /dev/null +++ b/crates/codegen/runtime/npm/package/src/runtime/compilation/index.mts @@ -0,0 +1,14 @@ +import * as generated from "../../../wasm/index.mjs"; +import * as builder from "./builder.mjs"; + +// This is a wrapper around 'generated.compilation.InternalCompilationBuilder': +export const CompilationBuilder = builder.CompilationBuilder; +export type CompilationBuilder = builder.CompilationBuilder; + +export type CompilationBuilderConfig = builder.CompilationBuilderConfig; + +export const CompilationUnit = generated.compilation.CompilationUnit; +export type CompilationUnit = generated.compilation.CompilationUnit; + +export const File = generated.compilation.File; +export type File = generated.compilation.File; diff --git a/crates/codegen/runtime/npm/package/src/runtime/cst/index.mts b/crates/codegen/runtime/npm/package/src/runtime/cst/index.mts index 4ad2bc383b..2d6ef2a414 100644 --- a/crates/codegen/runtime/npm/package/src/runtime/cst/index.mts +++ b/crates/codegen/runtime/npm/package/src/runtime/cst/index.mts @@ -48,10 +48,9 @@ export type TextIndex = generated.cst.TextIndex; export type TextRange = generated.cst.TextRange; -/* - * Helpers: +/** + * Asserts that this node is a `NonterminalNode` with the provided kind and text. */ - export function assertIsNonterminalNode( node: unknown, kind?: NonterminalKind, @@ -70,6 +69,9 @@ export function assertIsNonterminalNode( } } +/** + * Asserts that this node is a `TerminalKind` with the provided kind and text. + */ export function assertIsTerminalNode(node: unknown, kind?: TerminalKind, text?: string): asserts node is TerminalNode { if (!(node instanceof TerminalNode)) { throw new Error("Node provided is not a TerminalNode."); diff --git a/crates/codegen/runtime/npm/package/src/runtime/index.mts b/crates/codegen/runtime/npm/package/src/runtime/index.mts index 92781586e8..12fb24e0b1 100644 --- a/crates/codegen/runtime/npm/package/src/runtime/index.mts +++ b/crates/codegen/runtime/npm/package/src/runtime/index.mts @@ -1,3 +1,6 @@ export * as ast from "./ast/index.mjs"; +export * as bindings from "./bindings/index.mjs"; +export * as compilation from "./compilation/index.mjs"; export * as cst from "./cst/index.mjs"; export * as parser from "./parser/index.mjs"; +export * as utils from "./utils/index.mjs"; diff --git a/crates/codegen/runtime/npm/package/src/runtime/utils/index.mts b/crates/codegen/runtime/npm/package/src/runtime/utils/index.mts new file mode 100644 index 0000000000..fbf07b806d --- /dev/null +++ b/crates/codegen/runtime/npm/package/src/runtime/utils/index.mts @@ -0,0 +1,4 @@ +import * as generated from "../../../wasm/index.mjs"; + +export const LanguageFacts = generated.utils.LanguageFacts; +export type LanguageFacts = generated.utils.LanguageFacts; diff --git a/crates/codegen/runtime/npm/package/wasm/generated/codegen_runtime_cargo_wasm.component.d.ts b/crates/codegen/runtime/npm/package/wasm/generated/codegen_runtime_cargo_wasm.component.d.ts index c7614866dd..959f0ad174 100644 --- a/crates/codegen/runtime/npm/package/wasm/generated/codegen_runtime_cargo_wasm.component.d.ts +++ b/crates/codegen/runtime/npm/package/wasm/generated/codegen_runtime_cargo_wasm.component.d.ts @@ -2,7 +2,13 @@ import { NomicFoundationSlangCst } from "./interfaces/nomic-foundation-slang-cst.js"; import { NomicFoundationSlangAst } from "./interfaces/nomic-foundation-slang-ast.js"; +import { NomicFoundationSlangBindings } from "./interfaces/nomic-foundation-slang-bindings.js"; +import { NomicFoundationSlangCompilation } from "./interfaces/nomic-foundation-slang-compilation.js"; import { NomicFoundationSlangParser } from "./interfaces/nomic-foundation-slang-parser.js"; +import { NomicFoundationSlangUtils } from "./interfaces/nomic-foundation-slang-utils.js"; export * as cst from "./interfaces/nomic-foundation-slang-cst.js"; export * as ast from "./interfaces/nomic-foundation-slang-ast.js"; +export * as bindings from "./interfaces/nomic-foundation-slang-bindings.js"; +export * as compilation from "./interfaces/nomic-foundation-slang-compilation.js"; export * as parser from "./interfaces/nomic-foundation-slang-parser.js"; +export * as utils from "./interfaces/nomic-foundation-slang-utils.js"; diff --git a/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts new file mode 100644 index 0000000000..b2b0d4dc94 --- /dev/null +++ b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts @@ -0,0 +1,153 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangBindings { + export { BindingGraph }; + export { Definition }; + export { Reference }; + export { UserFileLocation }; + export { BuiltInLocation }; + export { BindingLocation }; + export { BindingLocationType }; +} +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Represents a location of a symbol (definition or reference) in the binding graph. + * It can either be in a user file, or a built-in in the language. + */ +export type BindingLocation = UserFileLocation | BuiltInLocation; +export enum BindingLocationType { + UserFileLocation = "UserFileLocation", + BuiltInLocation = "BuiltInLocation", +} + +/** + * A giant graph that contains name binding information for all source files within the compilation unit. + * It stores cursors to all definitions and references, and can resolve the edges between them. + */ +export class BindingGraph { + /** + * If the provided cursor points at a definition `Identifier`, it will return the + * corresponding definition. Otherwise, it will return `undefined`. + */ + definitionAt(cursor: Cursor): Definition | undefined; + /** + * If the provided cursor points at a reference `Identifier`, it will return the + * corresponding reference. Otherwise, it will return `undefined`. + */ + referenceAt(cursor: Cursor): Reference | undefined; +} + +/** + * Represents a location of a built-in symbol in the language. + */ +export class BuiltInLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.BuiltInLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): this; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): this is BuiltInLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): undefined; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): false; +} + +/** + * Represents a definition in the binding graph. + */ +export class Definition { + /** + * Returns a unique numerical identifier of the definition. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the definition's name. + * For `contract X {}`, that is the location of the `X` `Identifier` node. + */ + get nameLocation(): BindingLocation; + /** + * Returns the location of the definition's definiens. + * For `contract X {}`, that is the location of the parent `ContractDefinition` node. + */ + get definiensLocation(): BindingLocation; +} + +/** + * Represents a reference in the binding graph. + */ +export class Reference { + /** + * Returns a unique numerical identifier of the reference. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the reference. + * For `new X()`, that is the location of the `X` `Identifier` node. + */ + get location(): BindingLocation; + /** + * Returns a list of all definitions related to this reference. + * Most references have a single definition, but some have multiple, such as when a symbol + * is imported from another file, and renamed (re-defined) in the current file. + */ + definitions(): Definition[]; +} + +/** + * Represents a location of a user-defined symbol in a user file. + */ +export class UserFileLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.UserFileLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): this; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): this is UserFileLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): undefined; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): false; + + /** + * Returns the ID of the file that contains the symbol. + */ + get fileId(): string; + /** + * Returns a cursor to the CST node that contains the symbol. + */ + get cursor(): Cursor; +} diff --git a/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts new file mode 100644 index 0000000000..3c1a6831c1 --- /dev/null +++ b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts @@ -0,0 +1,98 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangCompilation { + export { InternalCompilationBuilder }; + export { CompilationUnit }; + export { File }; +} +import type { BindingGraph } from "./nomic-foundation-slang-bindings.js"; +export { BindingGraph }; +import type { Node } from "./nomic-foundation-slang-cst.js"; +export { Node }; +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Contains information about imports found in an added source file. + */ +export interface AddFileResponse { + /** + * List of cursors to any import paths found in the file. + */ + importPaths: Cursor[]; +} + +/** + * A complete compilation unit is a complete view over all compilation inputs: + * + * - All source files, stored as CSTs. + * - Name binding graph that exposes relationships between definitions and references in these files. + * - Any relevant compilation options. + * + * It also exposes utilities to traverse the compilation unit and query it. + */ +export class CompilationUnit { + /** + * Returns the language version this compilation unit is configured for. + */ + get languageVersion(): string; + /** + * Returns a list of all files in the compilation unit. + */ + files(): File[]; + /** + * Returns the file with the specified ID, if it exists. + */ + file(id: string): File | undefined; + /** + * Calculates name binding information for all source files within the compilation unit. + * Returns a graph that contains all found definitions and their references. + * + * Note: building this graph is an expensive operation. + * It is done lazily on the first access, and cached thereafter. + */ + get bindingGraph(): BindingGraph; +} + +/** + * A single source file in the compilation unit. + */ +export class File { + /** + * Returns the unique identifier of this file. + */ + get id(): string; + /** + * Returns the syntax tree of this file. + */ + get tree(): Node; + /** + * Creates a cursor for traversing the syntax tree of this file. + */ + createTreeCursor(): Cursor; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a transitive list of all files and their imports. + * + * This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + * This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + */ +export class InternalCompilationBuilder { + /** + * Creates a new compilation builder for the specified language version. + */ + static create(languageVersion: string): InternalCompilationBuilder; + /** + * Adds a source file to the compilation unit. + */ + addFile(id: string, contents: string): AddFileResponse; + /** + * Resolves an import in the source file to the destination file. + */ + resolveImport(sourceFileId: string, importPath: Cursor, destinationFileId: string): void; + /** + * Builds and returns the final compilation unit. + */ + build(): CompilationUnit; +} diff --git a/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts index bc99b8e132..6ac5bb7305 100644 --- a/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts +++ b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts @@ -65,19 +65,14 @@ export class Parser { * This represents the starting point for parsing a complete source file. */ static rootKind(): NonterminalKind; - /** - * Returns a list of language versions supported by this parser. - * Each version string represents a specific grammar configuration. - */ - static supportedVersions(): string[]; /** * Creates a new parser instance for the specified language version. */ - static create(version: string): Parser; + static create(languageVersion: string): Parser; /** * Returns the language version this parser instance is configured for. */ - get version(): string; + get languageVersion(): string; /** * Parses the input string starting from the specified nonterminal kind. */ diff --git a/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts new file mode 100644 index 0000000000..0a3e19de1c --- /dev/null +++ b/crates/codegen/runtime/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts @@ -0,0 +1,15 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangUtils { + export { LanguageFacts }; +} + +/** + * Provides information about the supported language versions and the grammar. + */ +export class LanguageFacts { + /** + * Returns a list of language versions supported by Slang, sorted ascendingly. + */ + static supportedVersions(): string[]; +} diff --git a/crates/metaslang/bindings/generated/public_api.txt b/crates/metaslang/bindings/generated/public_api.txt index f98a7e4582..0e7a4e3bb8 100644 --- a/crates/metaslang/bindings/generated/public_api.txt +++ b/crates/metaslang/bindings/generated/public_api.txt @@ -1,6 +1,16 @@ # This file is generated automatically by infrastructure scripts. Please don't edit by hand. pub mod metaslang_bindings +pub enum metaslang_bindings::BindingLocation +pub metaslang_bindings::BindingLocation::BuiltIn(metaslang_bindings::BuiltInLocation) +pub metaslang_bindings::BindingLocation::UserFile(metaslang_bindings::UserFileLocation) +impl metaslang_bindings::BindingLocation +pub fn metaslang_bindings::BindingLocation::built_in() -> Self +pub fn metaslang_bindings::BindingLocation::user_file(file_id: alloc::string::String, cursor: metaslang_cst::cursor::Cursor) -> Self +impl core::clone::Clone for metaslang_bindings::BindingLocation +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) @@ -12,24 +22,32 @@ pub fn metaslang_bindings::FileDescriptor::is_user_path(&self, path: &str) -> bo 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::Bindings -impl metaslang_bindings::Bindings -pub fn metaslang_bindings::Bindings::add_system_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -pub fn metaslang_bindings::Bindings::add_user_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -pub fn metaslang_bindings::Bindings::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::Bindings::all_definitions(&self) -> impl core::iter::traits::iterator::Iterator> + '_ -pub fn metaslang_bindings::Bindings::all_references(&self) -> impl core::iter::traits::iterator::Iterator> + '_ -pub fn metaslang_bindings::Bindings::create(version: semver::Version, binding_rules: &str, path_resolver: alloc::sync::Arc<(dyn metaslang_bindings::PathResolver + core::marker::Sync + core::marker::Send)>) -> Self -pub fn metaslang_bindings::Bindings::definition_at(&self, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> -pub fn metaslang_bindings::Bindings::get_context(&self) -> core::option::Option> -pub fn metaslang_bindings::Bindings::lookup_definition_by_name(&self, name: &str) -> core::option::Option> -pub fn metaslang_bindings::Bindings::reference_at(&self, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> -pub fn metaslang_bindings::Bindings::set_context(&mut self, context: &metaslang_bindings::DefinitionHandle) +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::get_context(&self) -> core::option::Option> +pub fn metaslang_bindings::BindingGraph::lookup_definition_by_name(&self, name: &str) -> core::option::Option> +pub fn metaslang_bindings::BindingGraph::reference_at(&self, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> +pub fn metaslang_bindings::BindingGraph::set_context(&mut self, context: &metaslang_bindings::DefinitionHandle) +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>::get_cursor(&self) -> core::option::Option> -pub fn metaslang_bindings::Definition<'a, KT>::get_definiens_cursor(&self) -> core::option::Option> +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 pub fn metaslang_bindings::Definition<'a, KT>::to_handle(self) -> metaslang_bindings::DefinitionHandle 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> @@ -46,14 +64,24 @@ pub struct metaslang_bindings::DefinitionHandle(_) 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) -> core::option::Option> +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>::jump_to_definition(&self) -> core::result::Result, metaslang_bindings::ResolutionError<'a, KT>> +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 trait metaslang_bindings::PathResolver -pub fn metaslang_bindings::PathResolver::resolve_path(&self, context_path: &str, path_to_resolve: &str) -> core::option::Option +pub struct metaslang_bindings::UserFileLocation +impl metaslang_bindings::UserFileLocation +pub fn metaslang_bindings::UserFileLocation::cursor(&self) -> &metaslang_cst::cursor::Cursor +pub fn metaslang_bindings::UserFileLocation::file_id(&self) -> &str +impl core::clone::Clone for metaslang_bindings::UserFileLocation +pub fn metaslang_bindings::UserFileLocation::clone(&self) -> metaslang_bindings::UserFileLocation +impl core::fmt::Debug for metaslang_bindings::UserFileLocation +pub fn metaslang_bindings::UserFileLocation::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub trait metaslang_bindings::PathResolver +pub fn metaslang_bindings::PathResolver::resolve_path(&self, context_path: &str, path_to_resolve: &metaslang_cst::cursor::Cursor) -> core::option::Option diff --git a/crates/metaslang/bindings/src/builder/functions.rs b/crates/metaslang/bindings/src/builder/functions.rs index 5bdab61182..27ff5d9aa3 100644 --- a/crates/metaslang/bindings/src/builder/functions.rs +++ b/crates/metaslang/bindings/src/builder/functions.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::rc::Rc; use metaslang_cst::kinds::KindTypes; use metaslang_graph_builder::functions::Functions; @@ -8,7 +8,7 @@ use crate::PathResolver; pub fn default_functions( version: Version, - path_resolver: Arc, + path_resolver: Rc>, ) -> Functions { let mut functions = Functions::stdlib(); version::add_version_functions(&mut functions, version); @@ -56,7 +56,7 @@ mod version { } mod resolver { - use std::sync::Arc; + use std::rc::Rc; use metaslang_cst::kinds::KindTypes; use metaslang_graph_builder::functions::{Function, Functions, Parameters}; @@ -67,24 +67,24 @@ mod resolver { pub fn add_functions( functions: &mut Functions, - path_resolver: Arc, + path_resolver: Rc>, ) { functions.add("resolve-path".into(), ResolvePath { path_resolver }); functions.add("is-system-file".into(), IsSystemFile {}); } - struct ResolvePath { - path_resolver: Arc, + struct ResolvePath { + path_resolver: Rc>, } - impl Function for ResolvePath { + impl Function for ResolvePath { fn call( &self, - _graph: &mut Graph, + graph: &mut Graph, parameters: &mut dyn Parameters, ) -> Result { let context_path = parameters.param()?.into_string()?; - let path_to_resolve = parameters.param()?.into_string()?; + let path_to_resolve = &graph[parameters.param()?.as_syntax_node_ref()?]; parameters.finish()?; let context_file_descriptor = FileDescriptor::try_from(&context_path); @@ -101,13 +101,14 @@ mod resolver { let resolved_path = self .path_resolver .as_ref() - .resolve_path(&context_user_path, &path_to_resolve) + .resolve_path(&context_user_path, path_to_resolve) .map_or_else( || { // In case we cannot resolve the path, we return a special value that is unique // per context/path pait. This way, we can still run incrementally and resolve // other symbols in the file: - format!("__SLANG_UNRESOLVED_PATH__{context_path}__{path_to_resolve}__") + let node_id = path_to_resolve.node().id(); + format!("__SLANG_UNRESOLVED_PATH__{context_path}__{node_id}__") }, |resolved_path| FileDescriptor::User(resolved_path).as_string(), ); diff --git a/crates/metaslang/bindings/src/lib.rs b/crates/metaslang/bindings/src/lib.rs index ccc855fca5..6b655f2a33 100644 --- a/crates/metaslang/bindings/src/lib.rs +++ b/crates/metaslang/bindings/src/lib.rs @@ -1,10 +1,11 @@ mod builder; +mod location; mod resolver; use std::collections::{HashMap, HashSet}; use std::fmt::{self, Debug, Display}; use std::hash::Hash; -use std::sync::Arc; +use std::rc::Rc; use builder::BuildResult; use metaslang_cst::cursor::Cursor; @@ -19,6 +20,9 @@ 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 struct DefinitionHandle(GraphHandle); #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -43,7 +47,7 @@ pub(crate) struct ReferenceBindingInfo { parents: Vec, } -pub struct Bindings { +pub struct BindingGraph { graph_builder_file: File, functions: Functions, stack_graph: StackGraph, @@ -111,15 +115,15 @@ impl FileDescriptor { } } -pub trait PathResolver { - fn resolve_path(&self, context_path: &str, path_to_resolve: &str) -> Option; +pub trait PathResolver { + fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option; } -impl Bindings { +impl BindingGraph { pub fn create( version: Version, binding_rules: &str, - path_resolver: Arc, + path_resolver: Rc>, ) -> Self { let graph_builder_file = File::from_str(binding_rules).expect("Bindings stack graph builder parse error"); @@ -254,12 +258,7 @@ impl Bindings { if self.stack_graph[*handle].is_definition() { self.to_definition(*handle) } else { - // TODO: what should we do if the parent reference - // cannot be resolved at this point? - self.to_reference(*handle) - .unwrap() - .jump_to_definition() - .ok() + self.to_reference(*handle)?.jump_to_definition().ok() } }) .collect() @@ -267,7 +266,7 @@ impl Bindings { pub fn lookup_definition_by_name(&self, name: &str) -> Option> { self.all_definitions() - .find(|definition| definition.get_cursor().unwrap().node().unparse() == name) + .find(|definition| definition.get_cursor().node().unparse() == name) } pub fn get_context(&self) -> Option> { @@ -361,20 +360,48 @@ impl<'a, KT: KindTypes + 'static> fmt::Display for DisplayCursor<'a, KT> { #[derive(Clone)] pub struct Definition<'a, KT: KindTypes + 'static> { - owner: &'a Bindings, + owner: &'a BindingGraph, handle: GraphHandle, } impl<'a, KT: KindTypes + 'static> Definition<'a, KT> { - pub fn get_cursor(&self) -> Option> { - self.owner.cursors.get(&self.handle).cloned() + 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 get_definiens_cursor(&self) -> Option> { + 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) - .and_then(|info| info.definiens.clone()) + .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 { @@ -408,19 +435,14 @@ impl<'a, KT: KindTypes + 'static> Definition<'a, KT> { impl Display for Definition<'_, KT> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.get_cursor() { - Some(cursor) => { - write!( - f, - "definition {}", - DisplayCursor { - cursor: &cursor, - file: self.get_file() - } - ) + write!( + f, + "definition {}", + DisplayCursor { + cursor: self.get_cursor(), + file: self.get_file() } - None => write!(f, "{}", self.handle.display(&self.owner.stack_graph)), - } + ) } } @@ -432,8 +454,8 @@ impl Debug for Definition<'_, KT> { impl PartialEq for Definition<'_, KT> { fn eq(&self, other: &Self) -> bool { - let our_owner: *const Bindings = self.owner; - let other_owner: *const Bindings = other.owner; + let our_owner: *const BindingGraph = self.owner; + let other_owner: *const BindingGraph = other.owner; our_owner == other_owner && self.handle == other.handle } } @@ -442,7 +464,7 @@ impl Eq for Definition<'_, KT> {} impl Hash for Definition<'_, KT> { fn hash(&self, state: &mut H) { - let owner: *const Bindings = self.owner; + let owner: *const BindingGraph = self.owner; owner.hash(state); self.handle.hash(state); } @@ -450,7 +472,7 @@ impl Hash for Definition<'_, KT> { #[derive(Clone)] pub struct Reference<'a, KT: KindTypes + 'static> { - owner: &'a Bindings, + owner: &'a BindingGraph, handle: GraphHandle, } @@ -460,8 +482,24 @@ pub enum ResolutionError<'a, KT: KindTypes + 'static> { } impl<'a, KT: KindTypes + 'static> Reference<'a, KT> { - pub fn get_cursor(&self) -> Option> { - self.owner.cursors.get(&self.handle).cloned() + 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 { @@ -499,26 +537,21 @@ impl<'a, KT: KindTypes + 'static> Reference<'a, KT> { impl Display for Reference<'_, KT> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.get_cursor() { - Some(cursor) => { - write!( - f, - "reference {}", - DisplayCursor { - cursor: &cursor, - file: self.get_file() - } - ) + write!( + f, + "reference {}", + DisplayCursor { + cursor: self.get_cursor(), + file: self.get_file() } - None => write!(f, "{}", self.handle.display(&self.owner.stack_graph)), - } + ) } } impl PartialEq for Reference<'_, KT> { fn eq(&self, other: &Self) -> bool { - let our_owner: *const Bindings = self.owner; - let other_owner: *const Bindings = other.owner; + let our_owner: *const BindingGraph = self.owner; + let other_owner: *const BindingGraph = other.owner; our_owner == other_owner && self.handle == other.handle } } diff --git a/crates/metaslang/bindings/src/location/mod.rs b/crates/metaslang/bindings/src/location/mod.rs new file mode 100644 index 0000000000..914a12169d --- /dev/null +++ b/crates/metaslang/bindings/src/location/mod.rs @@ -0,0 +1,40 @@ +use metaslang_cst::cursor::Cursor; +use metaslang_cst::kinds::KindTypes; + +#[derive(Debug, Clone)] +pub enum BindingLocation { + UserFile(UserFileLocation), + BuiltIn(BuiltInLocation), +} + +impl BindingLocation { + pub fn user_file(file_id: String, cursor: Cursor) -> Self { + Self::UserFile(UserFileLocation { file_id, cursor }) + } + + pub fn built_in() -> Self { + Self::BuiltIn(BuiltInLocation {}) + } +} + +#[derive(Debug, Clone)] +pub struct UserFileLocation { + file_id: String, + cursor: Cursor, +} + +impl UserFileLocation { + pub fn file_id(&self) -> &str { + &self.file_id + } + + pub fn cursor(&self) -> &Cursor { + &self.cursor + } +} + +#[derive(Debug, Clone)] +pub struct BuiltInLocation { + // We are not exposing a `file_id` or a `cursor` here, because we don't expose the underlying `built_ins.sol` file yet. + // Still, we are using a dedicated type here to make it easier to map to the corresponding WIT variant. +} diff --git a/crates/metaslang/bindings/src/resolver/mod.rs b/crates/metaslang/bindings/src/resolver/mod.rs index 57c0caf305..7019a73425 100644 --- a/crates/metaslang/bindings/src/resolver/mod.rs +++ b/crates/metaslang/bindings/src/resolver/mod.rs @@ -5,7 +5,7 @@ use metaslang_cst::kinds::KindTypes; use stack_graphs::partial::{PartialPath, PartialPaths}; use stack_graphs::stitching::{ForwardPartialPathStitcher, GraphEdgeCandidates, StitcherConfig}; -use crate::{Bindings, Definition, Reference, ResolutionError, Tag}; +use crate::{BindingGraph, Definition, Reference, ResolutionError, Tag}; mod c3; @@ -33,7 +33,7 @@ mod c3; /// applied to definitions pointing to virtual methods. /// pub(crate) struct Resolver<'a, KT: KindTypes + 'static> { - owner: &'a Bindings, + owner: &'a BindingGraph, reference: Reference<'a, KT>, partials: PartialPaths, results: Vec>, diff --git a/crates/metaslang/graph_builder/generated/public_api.txt b/crates/metaslang/graph_builder/generated/public_api.txt index 4d62231805..3010fc6ffc 100644 --- a/crates/metaslang/graph_builder/generated/public_api.txt +++ b/crates/metaslang/graph_builder/generated/public_api.txt @@ -599,7 +599,7 @@ impl metaslang_graph_builder::functions::Fu pub fn metaslang_graph_builder::functions::stdlib::IsNull::call(&self, _graph: &mut metaslang_graph_builder::graph::Graph, parameters: &mut dyn metaslang_graph_builder::functions::Parameters) -> core::result::Result pub struct metaslang_graph_builder::functions::Functions impl metaslang_graph_builder::functions::Functions -pub fn metaslang_graph_builder::functions::Functions::add(&mut self, name: metaslang_graph_builder::Identifier, function: F) where F: metaslang_graph_builder::functions::Function + core::marker::Send + core::marker::Sync + 'static +pub fn metaslang_graph_builder::functions::Functions::add(&mut self, name: metaslang_graph_builder::Identifier, function: F) where F: metaslang_graph_builder::functions::Function + 'static pub fn metaslang_graph_builder::functions::Functions::call(&self, name: &metaslang_graph_builder::Identifier, graph: &mut metaslang_graph_builder::graph::Graph, parameters: &mut dyn metaslang_graph_builder::functions::Parameters) -> core::result::Result pub fn metaslang_graph_builder::functions::Functions::new() -> metaslang_graph_builder::functions::Functions pub fn metaslang_graph_builder::functions::Functions::stdlib() -> metaslang_graph_builder::functions::Functions diff --git a/crates/metaslang/graph_builder/src/functions.rs b/crates/metaslang/graph_builder/src/functions.rs index 2777d29e0c..04bd964dcf 100644 --- a/crates/metaslang/graph_builder/src/functions.rs +++ b/crates/metaslang/graph_builder/src/functions.rs @@ -8,7 +8,7 @@ //! Functions that can be called by graph DSL files use std::collections::HashMap; -use std::sync::Arc; +use std::rc::Rc; use metaslang_cst::kinds::KindTypes; @@ -86,7 +86,7 @@ where /// A library of named functions. #[derive(Default)] pub struct Functions { - functions: HashMap + Send + Sync>>, + functions: HashMap>>, } impl Functions { @@ -144,9 +144,9 @@ impl Functions { /// Adds a new function to this library. pub fn add(&mut self, name: Identifier, function: F) where - F: Function + Send + Sync + 'static, + F: Function + 'static, { - self.functions.insert(name, Arc::new(function)); + self.functions.insert(name, Rc::new(function)); } /// Calls a named function, returning an error if there is no function with that name. diff --git a/crates/solidity/inputs/language/bindings/rules.msgb b/crates/solidity/inputs/language/bindings/rules.msgb index 0a4b6e4913..4bab41894f 100644 --- a/crates/solidity/inputs/language/bindings/rules.msgb +++ b/crates/solidity/inputs/language/bindings/rules.msgb @@ -163,21 +163,29 @@ inherit .star_extension ;;; Imports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -[ImportClause [_ @path path: [StringLiteral]]] { +[ImportClause + [_ + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + ] +] { ;; This node represents the imported file and the @path.import node is used by ;; all subsequent import rules node @path.import - scan (source-text @path) { - "^\\s*[\"'](.+)[\"']\\s*$" { - let resolved_path = (resolve-path FILE_PATH $1) - attr (@path.import) push_symbol = resolved_path - } - } + + let resolved_path = (resolve-path FILE_PATH @path) + attr (@path.import) push_symbol = resolved_path + edge @path.import -> ROOT_NODE } ;;; `import ` -@import [PathImport @path path: [StringLiteral] .] { +@import [PathImport + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] +] { ;; This is the "lexical" connection, which makes all symbols exported from the ;; imported source unit available for resolution globally at this' source unit ;; scope @@ -186,8 +194,10 @@ inherit .star_extension ;;; `import as ` @import [PathImport - @path path: [StringLiteral] - alias: [ImportAlias @alias [Identifier]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + alias: [ImportAlias @alias [Identifier]] ] { node def attr (def) node_definition = @alias @@ -205,8 +215,10 @@ inherit .star_extension ;;; `import * as from ` @import [NamedImport - alias: [ImportAlias @alias [Identifier]] - @path path: [StringLiteral] + alias: [ImportAlias @alias [Identifier]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] ] { node def attr (def) node_definition = @alias @@ -224,8 +236,10 @@ inherit .star_extension ;;; `import { [as ] ...} from ` @import [ImportDeconstruction - symbols: [ImportDeconstructionSymbols @symbol [ImportDeconstructionSymbol]] - @path path: [StringLiteral] + symbols: [ImportDeconstructionSymbols @symbol [ImportDeconstructionSymbol]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] ] { ;; We define these intermediate nodes for convenience only, to make the ;; queries simpler in the two rules below diff --git a/crates/solidity/inputs/language/src/definition.rs b/crates/solidity/inputs/language/src/definition.rs index 7ff142dfd5..a4feaa287a 100644 --- a/crates/solidity/inputs/language/src/definition.rs +++ b/crates/solidity/inputs/language/src/definition.rs @@ -4,7 +4,7 @@ codegen_language_macros::compile!(Language( name = Solidity, documentation_dir = "crates/solidity/inputs/language/docs", binding_rules_file = "crates/solidity/inputs/language/bindings/rules.msgb", - file_extension = ".sol", + file_extension = ".sol", // TODO: This should be moved to the Solidity-specific 'extensions' sub-module. root_item = SourceUnit, // TODO(#1020): Define the end-of-file trivia explicitly rather than // implicitly reusing the leading trivia in the generater parser code. diff --git a/crates/solidity/outputs/cargo/crate/Cargo.toml b/crates/solidity/outputs/cargo/crate/Cargo.toml index 3519125a2c..7e1508fdf2 100644 --- a/crates/solidity/outputs/cargo/crate/Cargo.toml +++ b/crates/solidity/outputs/cargo/crate/Cargo.toml @@ -32,6 +32,7 @@ categories = [ default = [] __experimental_bindings_api = ["dep:metaslang_bindings"] __private_ariadne_errors = ["dep:ariadne"] +__private_compilation_api = [] __private_testing_utils = [] [build-dependencies] # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ diff --git a/crates/solidity/outputs/cargo/crate/generated/public_api.txt b/crates/solidity/outputs/cargo/crate/generated/public_api.txt index 5fa1c4d0eb..48a9440e5c 100644 --- a/crates/solidity/outputs/cargo/crate/generated/public_api.txt +++ b/crates/solidity/outputs/cargo/crate/generated/public_api.txt @@ -2,12 +2,49 @@ pub mod slang_solidity pub mod slang_solidity::bindings -pub fn slang_solidity::bindings::create_with_resolver(version: semver::Version, resolver: alloc::sync::Arc<(dyn metaslang_bindings::PathResolver + core::marker::Sync + core::marker::Send)>) -> slang_solidity::bindings::Bindings +pub use slang_solidity::bindings::BuiltInLocation +pub use slang_solidity::bindings::PathResolver +pub mod slang_solidity::bindings::built_ins +pub fn slang_solidity::bindings::built_ins::get_built_ins_contents(version: &semver::Version) -> &'static str +pub enum slang_solidity::bindings::BindingGraphInitializationError +pub slang_solidity::bindings::BindingGraphInitializationError::ParserInitialization(slang_solidity::parser::ParserInitializationError) +impl core::convert::From for slang_solidity::bindings::BindingGraphInitializationError +pub fn slang_solidity::bindings::BindingGraphInitializationError::from(source: slang_solidity::parser::ParserInitializationError) -> Self +impl core::error::Error for slang_solidity::bindings::BindingGraphInitializationError +pub fn slang_solidity::bindings::BindingGraphInitializationError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for slang_solidity::bindings::BindingGraphInitializationError +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::get_binding_rules() -> &'static str -pub fn slang_solidity::bindings::get_built_ins(version: &semver::Version) -> &'static str -pub type slang_solidity::bindings::Bindings = metaslang_bindings::Bindings +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 mod slang_solidity::compilation +pub struct slang_solidity::compilation::AddFileResponse +pub slang_solidity::compilation::AddFileResponse::import_paths: alloc::vec::Vec +pub struct slang_solidity::compilation::CompilationUnit +impl slang_solidity::compilation::CompilationUnit +pub fn slang_solidity::compilation::CompilationUnit::binding_graph(&self) -> &core::result::Result, slang_solidity::bindings::BindingGraphInitializationError> +pub fn slang_solidity::compilation::CompilationUnit::file(&self, id: &str) -> core::option::Option> +pub fn slang_solidity::compilation::CompilationUnit::files(&self) -> alloc::vec::Vec> +pub fn slang_solidity::compilation::CompilationUnit::language_version(&self) -> &semver::Version +pub struct slang_solidity::compilation::File +impl slang_solidity::compilation::File +pub fn slang_solidity::compilation::File::create_tree_cursor(&self) -> slang_solidity::cst::Cursor +pub fn slang_solidity::compilation::File::id(&self) -> &str +pub fn slang_solidity::compilation::File::tree(&self) -> &slang_solidity::cst::Node +impl core::clone::Clone for slang_solidity::compilation::File +pub fn slang_solidity::compilation::File::clone(&self) -> slang_solidity::compilation::File +pub struct slang_solidity::compilation::InternalCompilationBuilder +impl slang_solidity::compilation::InternalCompilationBuilder +pub fn slang_solidity::compilation::InternalCompilationBuilder::add_file(&mut self, id: alloc::string::String, contents: &str) -> slang_solidity::compilation::AddFileResponse +pub fn slang_solidity::compilation::InternalCompilationBuilder::build(&self) -> slang_solidity::compilation::CompilationUnit +pub fn slang_solidity::compilation::InternalCompilationBuilder::create(language_version: semver::Version) -> core::result::Result +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::NonterminalKindExtensions @@ -886,6 +923,8 @@ pub fn slang_solidity::diagnostic::render for slang_solidity::bindings::BindingGraphInitializationError +pub fn slang_solidity::bindings::BindingGraphInitializationError::from(source: slang_solidity::parser::ParserInitializationError) -> Self impl core::error::Error for slang_solidity::parser::ParserInitializationError impl core::fmt::Debug for slang_solidity::parser::ParserInitializationError pub fn slang_solidity::parser::ParserInitializationError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result @@ -914,20 +953,24 @@ impl slang_solidity::parser::ParseOutput pub fn slang_solidity::parser::ParseOutput::create_tree_cursor(&self) -> slang_solidity::cst::Cursor pub fn slang_solidity::parser::ParseOutput::errors(&self) -> &alloc::vec::Vec pub fn slang_solidity::parser::ParseOutput::is_valid(&self) -> bool -pub fn slang_solidity::parser::ParseOutput::tree(&self) -> slang_solidity::cst::Node +pub fn slang_solidity::parser::ParseOutput::tree(&self) -> &slang_solidity::cst::Node +impl core::clone::Clone for slang_solidity::parser::ParseOutput +pub fn slang_solidity::parser::ParseOutput::clone(&self) -> slang_solidity::parser::ParseOutput impl core::cmp::PartialEq for slang_solidity::parser::ParseOutput pub fn slang_solidity::parser::ParseOutput::eq(&self, other: &slang_solidity::parser::ParseOutput) -> bool impl core::fmt::Debug for slang_solidity::parser::ParseOutput pub fn slang_solidity::parser::ParseOutput::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for slang_solidity::parser::ParseOutput pub struct slang_solidity::parser::Parser -pub slang_solidity::parser::Parser::version: semver::Version impl slang_solidity::parser::Parser pub const slang_solidity::parser::Parser::ROOT_KIND: slang_solidity::cst::NonterminalKind -pub const slang_solidity::parser::Parser::SUPPORTED_VERSIONS: &'static [semver::Version] -pub fn slang_solidity::parser::Parser::create(version: semver::Version) -> core::result::Result +pub fn slang_solidity::parser::Parser::create(language_version: semver::Version) -> core::result::Result +pub fn slang_solidity::parser::Parser::language_version(&self) -> &semver::Version pub fn slang_solidity::parser::Parser::parse(&self, kind: slang_solidity::cst::NonterminalKind, input: &str) -> slang_solidity::parser::ParseOutput -pub fn slang_solidity::parser::Parser::version(&self) -> &semver::Version impl core::fmt::Debug for slang_solidity::parser::Parser pub fn slang_solidity::parser::Parser::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -pub fn slang_solidity::transform_built_ins_node(node: &slang_solidity::cst::Node) -> slang_solidity::cst::Node +pub mod slang_solidity::utils +pub struct slang_solidity::utils::LanguageFacts +impl slang_solidity::utils::LanguageFacts +pub const slang_solidity::utils::LanguageFacts::NAME: &'static str +pub const slang_solidity::utils::LanguageFacts::SUPPORTED_VERSIONS: &'static [semver::Version] diff --git a/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs b/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs new file mode 100644 index 0000000000..b3ec4aa052 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs @@ -0,0 +1,59 @@ +use std::rc::Rc; + +use metaslang_cst::text_index::TextIndex; +use semver::Version; + +use crate::bindings::built_ins::get_built_ins_contents; +use crate::bindings::BindingGraph; +use crate::cst::{Edge, Node, NonterminalNode, TerminalKind, TerminalNode}; +use crate::parser::{Parser, ParserInitializationError}; + +pub fn add_built_ins( + binding_graph: &mut BindingGraph, + version: Version, +) -> Result<(), ParserInitializationError> { + let source = get_built_ins_contents(&version); + let parser = Parser::create(version)?; + let parse_output = parser.parse(Parser::ROOT_KIND, source); + + let built_ins_cursor = transform(parse_output.tree()).cursor_with_offset(TextIndex::ZERO); + + binding_graph.add_system_file("built_ins.sol", built_ins_cursor); + Ok(()) +} + +fn transform(node: &Node) -> Node { + match node { + Node::Nonterminal(nonterminal) => { + let NonterminalNode { + kind, + text_len, + children, + } = nonterminal.as_ref(); + let children = children + .iter() + .map(|edge| Edge { + label: edge.label, + node: transform(&edge.node), + }) + .collect(); + let nonterminal = Rc::new(NonterminalNode { + kind: *kind, + text_len: *text_len, + children, + }); + Node::Nonterminal(nonterminal) + } + Node::Terminal(terminal) => { + let TerminalNode { kind, text } = terminal.as_ref(); + let terminal = match terminal.as_ref().kind { + TerminalKind::Identifier => Rc::new(TerminalNode { + kind: *kind, + text: text.replace('$', "%"), + }), + _ => Rc::clone(terminal), + }; + Node::Terminal(terminal) + } + } +} diff --git a/crates/solidity/outputs/cargo/crate/src/extensions/compilation/mod.rs b/crates/solidity/outputs/cargo/crate/src/extensions/compilation/mod.rs new file mode 100644 index 0000000000..0ba82a945c --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/extensions/compilation/mod.rs @@ -0,0 +1,109 @@ +use crate::cst::{Cursor, Query}; + +pub struct ImportPathsExtractor { + queries: Vec, +} + +impl ImportPathsExtractor { + pub fn new() -> Self { + Self { + queries: [ + "[PathImport + path: [StringLiteral + @variant ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + ]", + "[NamedImport + path: [StringLiteral + @variant ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + ]", + "[ImportDeconstruction + path: [StringLiteral + @variant ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + ]", + ] + .into_iter() + .map(|text| Query::parse(text).unwrap()) + .collect(), + } + } + + pub fn extract(&self, cursor: Cursor) -> Vec { + cursor + .query(self.queries.clone()) + .flat_map(|query_match| query_match.captures) + .flat_map(|(match_name, cursors)| { + assert_eq!(match_name, "variant"); + cursors + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use semver::Version; + + use crate::parser::Parser; + + #[test] + pub fn path_import() { + run( + r#" + import "foo-double"; + import "bar-double" as bar; + + import 'foo-single'; + import 'bar-single' as bar; + + "#, + &[ + "\"foo-double\"", + "\"bar-double\"", + "\'foo-single\'", + "\'bar-single\'", + ], + ); + } + + #[test] + pub fn named_import() { + run( + r#" + import * as foo from "foo-double"; + + import * as foo from 'foo-single'; + "#, + &["\"foo-double\"", "\'foo-single\'"], + ); + } + + #[test] + pub fn import_deconstruction() { + run( + r#" + import {a, b} from "foo-double"; + + import {a, b} from 'foo-single'; + "#, + &["\"foo-double\"", "\'foo-single\'"], + ); + } + + fn run(source: &str, expected: &[&str]) { + let parser = Parser::create(Version::new(0, 8, 0)).unwrap(); + let parse_output = parser.parse(Parser::ROOT_KIND, source); + + let imports = super::ImportPathsExtractor::new(); + + let actual: Vec<_> = imports + .extract(parse_output.create_tree_cursor()) + .into_iter() + .map(|cursor| cursor.node().unparse()) + .collect(); + + assert_eq!(actual, expected.to_vec()); + } +} diff --git a/crates/solidity/outputs/cargo/crate/src/extensions/mod.rs b/crates/solidity/outputs/cargo/crate/src/extensions/mod.rs new file mode 100644 index 0000000000..78402f8764 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/extensions/mod.rs @@ -0,0 +1,8 @@ +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; + +#[cfg(feature = "__experimental_bindings_api")] +pub mod bindings; 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 c47f067e24..59e4f7f07a 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 @@ -168,21 +168,29 @@ inherit .star_extension ;;; Imports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -[ImportClause [_ @path path: [StringLiteral]]] { +[ImportClause + [_ + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + ] +] { ;; This node represents the imported file and the @path.import node is used by ;; all subsequent import rules node @path.import - scan (source-text @path) { - "^\\s*[\"'](.+)[\"']\\s*$" { - let resolved_path = (resolve-path FILE_PATH $1) - attr (@path.import) push_symbol = resolved_path - } - } + + let resolved_path = (resolve-path FILE_PATH @path) + attr (@path.import) push_symbol = resolved_path + edge @path.import -> ROOT_NODE } ;;; `import ` -@import [PathImport @path path: [StringLiteral] .] { +@import [PathImport + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] +] { ;; This is the "lexical" connection, which makes all symbols exported from the ;; imported source unit available for resolution globally at this' source unit ;; scope @@ -191,8 +199,10 @@ inherit .star_extension ;;; `import as ` @import [PathImport - @path path: [StringLiteral] - alias: [ImportAlias @alias [Identifier]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] + alias: [ImportAlias @alias [Identifier]] ] { node def attr (def) node_definition = @alias @@ -210,8 +220,10 @@ inherit .star_extension ;;; `import * as from ` @import [NamedImport - alias: [ImportAlias @alias [Identifier]] - @path path: [StringLiteral] + alias: [ImportAlias @alias [Identifier]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] ] { node def attr (def) node_definition = @alias @@ -229,8 +241,10 @@ inherit .star_extension ;;; `import { [as ] ...} from ` @import [ImportDeconstruction - symbols: [ImportDeconstructionSymbols @symbol [ImportDeconstructionSymbol]] - @path path: [StringLiteral] + symbols: [ImportDeconstructionSymbols @symbol [ImportDeconstructionSymbol]] + path: [StringLiteral + @path ([DoubleQuotedStringLiteral] | [SingleQuotedStringLiteral]) + ] ] { ;; We define these intermediate nodes for convenience only, to make the ;; queries simpler in the two rules below diff --git a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs index ac045e03e6..c72ecdea44 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs @@ -2,8 +2,9 @@ use semver::Version; +// TODO: This should be moved to the Solidity-specific 'extensions' sub-module. #[allow(unused_variables)] -pub fn get_contents(version: &Version) -> &'static str { +pub fn get_built_ins_contents(version: &Version) -> &'static str { if *version < Version::new(0, 4, 17) { include_str!("./built_ins/0.4.11.sol") } else if *version < Version::new(0, 4, 22) { 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 66c8a8c4da..7803930461 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs @@ -4,31 +4,46 @@ mod binding_rules; #[path = "generated/built_ins.rs"] -mod built_ins; +pub mod built_ins; -use std::sync::Arc; +use std::rc::Rc; -use metaslang_bindings::{self, PathResolver}; use semver::Version; use crate::cst::KindTypes; -pub type Bindings = metaslang_bindings::Bindings; +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 BindingLocation = metaslang_bindings::BindingLocation; +pub type UserFileLocation = metaslang_bindings::UserFileLocation; + +pub use metaslang_bindings::{BuiltInLocation, PathResolver}; + +use crate::parser::ParserInitializationError; + +#[derive(thiserror::Error, Debug)] +pub enum BindingGraphInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} pub fn create_with_resolver( version: Version, - resolver: Arc, -) -> Bindings { - Bindings::create(version, binding_rules::BINDING_RULES_SOURCE, resolver) + resolver: Rc>, +) -> Result { + let mut binding_graph = BindingGraph::create( + version.clone(), + binding_rules::BINDING_RULES_SOURCE, + resolver, + ); + + crate::extensions::bindings::add_built_ins(&mut binding_graph, version)?; + + Ok(binding_graph) } #[cfg(feature = "__private_testing_utils")] pub fn get_binding_rules() -> &'static str { binding_rules::BINDING_RULES_SOURCE } - -pub fn get_built_ins(version: &semver::Version) -> &'static str { - built_ins::get_contents(version) -} diff --git a/crates/solidity/outputs/cargo/crate/src/generated/compilation/file.rs b/crates/solidity/outputs/cargo/crate/src/generated/compilation/file.rs new file mode 100644 index 0000000000..94eee8ad56 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/compilation/file.rs @@ -0,0 +1,47 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::collections::BTreeMap; + +use metaslang_cst::text_index::TextIndex; + +use crate::cst::{Cursor, Node}; + +#[derive(Clone)] +pub struct File { + id: String, + tree: Node, + + resolved_imports: BTreeMap, +} + +impl File { + pub(super) fn new(id: String, tree: Node) -> Self { + Self { + id, + tree, + + resolved_imports: BTreeMap::new(), + } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn tree(&self) -> &Node { + &self.tree + } + + pub fn create_tree_cursor(&self) -> Cursor { + self.tree.clone().cursor_with_offset(TextIndex::ZERO) + } + + pub(super) fn resolve_import(&mut self, import_path: &Cursor, destination_file_id: String) { + self.resolved_imports + .insert(import_path.node().id(), destination_file_id); + } + + pub(super) fn resolved_import(&self, import_path: &Cursor) -> Option<&String> { + self.resolved_imports.get(&import_path.node().id()) + } +} diff --git a/crates/solidity/outputs/cargo/crate/src/generated/compilation/internal_builder.rs b/crates/solidity/outputs/cargo/crate/src/generated/compilation/internal_builder.rs new file mode 100644 index 0000000000..a0a79d7944 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/compilation/internal_builder.rs @@ -0,0 +1,91 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::compilation::{CompilationUnit, File}; +use crate::cst::Cursor; +use crate::extensions::compilation::ImportPathsExtractor; +use crate::parser::{Parser, ParserInitializationError}; + +pub struct InternalCompilationBuilder { + parser: Parser, + imports: ImportPathsExtractor, + files: BTreeMap, +} + +#[derive(thiserror::Error, Debug)] +pub enum CompilationInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} + +impl InternalCompilationBuilder { + pub fn create(language_version: Version) -> Result { + let parser = Parser::create(language_version)?; + + Ok(Self { + parser, + imports: ImportPathsExtractor::new(), + files: BTreeMap::new(), + }) + } + + pub fn add_file(&mut self, id: String, contents: &str) -> AddFileResponse { + if self.files.contains_key(&id) { + // Already added. No need to process it again: + return AddFileResponse { + import_paths: vec![], + }; + } + + let parse_output = self.parser.parse(Parser::ROOT_KIND, contents); + + let import_paths = self.imports.extract(parse_output.create_tree_cursor()); + + let file = File::new(id.clone(), parse_output.tree().clone()); + self.files.insert(id, file); + + AddFileResponse { import_paths } + } + + pub fn resolve_import( + &mut self, + source_file_id: &str, + import_path: &Cursor, + destination_file_id: String, + ) -> Result<(), ResolveImportError> { + self.files + .get_mut(source_file_id) + .ok_or_else(|| ResolveImportError::SourceFileNotFound(source_file_id.to_owned()))? + .resolve_import(import_path, destination_file_id); + + Ok(()) + } + + pub fn build(&self) -> CompilationUnit { + let language_version = self.parser.language_version().to_owned(); + + let files = self + .files + .iter() + .map(|(id, file)| (id.to_owned(), Rc::new(file.to_owned()))) + .collect(); + + CompilationUnit::new(language_version, files) + } +} + +pub struct AddFileResponse { + pub import_paths: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum ResolveImportError { + #[error( + "Source file not found: '{0}'. Make sure to add it first, before resolving its imports." + )] + SourceFileNotFound(String), +} diff --git a/crates/solidity/outputs/cargo/crate/src/generated/compilation/mod.rs b/crates/solidity/outputs/cargo/crate/src/generated/compilation/mod.rs new file mode 100644 index 0000000000..89544c6568 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/compilation/mod.rs @@ -0,0 +1,9 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +mod file; +mod internal_builder; +mod unit; + +pub use file::File; +pub use internal_builder::{AddFileResponse, InternalCompilationBuilder}; +pub use unit::CompilationUnit; diff --git a/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs b/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs new file mode 100644 index 0000000000..a15100e4df --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs @@ -0,0 +1,71 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::cell::OnceCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::bindings::{ + create_with_resolver, BindingGraph, BindingGraphInitializationError, PathResolver, +}; +use crate::compilation::File; +use crate::cst::{Cursor, KindTypes}; + +pub struct CompilationUnit { + language_version: Version, + files: BTreeMap>, + binding_graph: OnceCell, BindingGraphInitializationError>>, +} + +impl CompilationUnit { + pub(super) fn new(language_version: Version, files: BTreeMap>) -> Self { + Self { + language_version, + files, + binding_graph: OnceCell::new(), + } + } + + pub fn language_version(&self) -> &Version { + &self.language_version + } + + pub fn files(&self) -> Vec> { + self.files.values().cloned().collect() + } + + pub fn file(&self, id: &str) -> Option> { + self.files.get(id).cloned() + } + + pub fn binding_graph(&self) -> &Result, BindingGraphInitializationError> { + self.binding_graph.get_or_init(|| { + let resolver = Resolver { + files: self.files.clone(), + }; + + let mut binding_graph = + 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()); + } + + Ok(Rc::new(binding_graph)) + }) + } +} + +struct Resolver { + files: BTreeMap>, +} + +impl PathResolver for Resolver { + fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option { + self.files + .get(context_path)? + .resolved_import(path_to_resolve) + .cloned() + } +} diff --git a/crates/solidity/outputs/cargo/crate/src/generated/mod.rs b/crates/solidity/outputs/cargo/crate/src/generated/mod.rs index 4b9d48d9f9..3c700fafdc 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/mod.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/mod.rs @@ -2,6 +2,12 @@ #[cfg(feature = "__experimental_bindings_api")] pub mod bindings; +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; pub mod cst; pub mod diagnostic; pub mod parser; +pub mod utils; diff --git a/crates/solidity/outputs/cargo/crate/src/generated/parser/generated/parser.rs b/crates/solidity/outputs/cargo/crate/src/generated/parser/generated/parser.rs index de10be92a5..9ea72c7af4 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/parser/generated/parser.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/parser/generated/parser.rs @@ -27,6 +27,7 @@ use crate::parser::scanner_macros::{ scan_not_followed_by, scan_one_or_more, scan_optional, scan_sequence, scan_zero_or_more, }; use crate::parser::ParseOutput; +use crate::utils::LanguageFacts; #[derive(Debug)] pub struct Parser { @@ -65,7 +66,7 @@ pub struct Parser { pub(crate) version_is_at_least_0_8_24: bool, pub(crate) version_is_at_least_0_8_25: bool, pub(crate) version_is_at_least_0_8_27: bool, - pub version: Version, + language_version: Version, } #[derive(thiserror::Error, Debug)] @@ -75,142 +76,61 @@ pub enum ParserInitializationError { } impl Parser { - pub const SUPPORTED_VERSIONS: &'static [Version] = &[ - Version::new(0, 4, 11), - Version::new(0, 4, 12), - Version::new(0, 4, 13), - Version::new(0, 4, 14), - Version::new(0, 4, 15), - Version::new(0, 4, 16), - Version::new(0, 4, 17), - Version::new(0, 4, 18), - Version::new(0, 4, 19), - Version::new(0, 4, 20), - Version::new(0, 4, 21), - Version::new(0, 4, 22), - Version::new(0, 4, 23), - Version::new(0, 4, 24), - Version::new(0, 4, 25), - Version::new(0, 4, 26), - Version::new(0, 5, 0), - Version::new(0, 5, 1), - Version::new(0, 5, 2), - Version::new(0, 5, 3), - Version::new(0, 5, 4), - Version::new(0, 5, 5), - Version::new(0, 5, 6), - Version::new(0, 5, 7), - Version::new(0, 5, 8), - Version::new(0, 5, 9), - Version::new(0, 5, 10), - Version::new(0, 5, 11), - Version::new(0, 5, 12), - Version::new(0, 5, 13), - Version::new(0, 5, 14), - Version::new(0, 5, 15), - Version::new(0, 5, 16), - Version::new(0, 5, 17), - Version::new(0, 6, 0), - Version::new(0, 6, 1), - Version::new(0, 6, 2), - Version::new(0, 6, 3), - Version::new(0, 6, 4), - Version::new(0, 6, 5), - Version::new(0, 6, 6), - Version::new(0, 6, 7), - Version::new(0, 6, 8), - Version::new(0, 6, 9), - Version::new(0, 6, 10), - Version::new(0, 6, 11), - Version::new(0, 6, 12), - Version::new(0, 7, 0), - Version::new(0, 7, 1), - Version::new(0, 7, 2), - Version::new(0, 7, 3), - Version::new(0, 7, 4), - Version::new(0, 7, 5), - Version::new(0, 7, 6), - Version::new(0, 8, 0), - Version::new(0, 8, 1), - Version::new(0, 8, 2), - Version::new(0, 8, 3), - Version::new(0, 8, 4), - Version::new(0, 8, 5), - Version::new(0, 8, 6), - Version::new(0, 8, 7), - Version::new(0, 8, 8), - Version::new(0, 8, 9), - Version::new(0, 8, 10), - Version::new(0, 8, 11), - Version::new(0, 8, 12), - Version::new(0, 8, 13), - Version::new(0, 8, 14), - Version::new(0, 8, 15), - Version::new(0, 8, 16), - Version::new(0, 8, 17), - Version::new(0, 8, 18), - Version::new(0, 8, 19), - Version::new(0, 8, 20), - Version::new(0, 8, 21), - Version::new(0, 8, 22), - Version::new(0, 8, 23), - Version::new(0, 8, 24), - Version::new(0, 8, 25), - Version::new(0, 8, 26), - Version::new(0, 8, 27), - Version::new(0, 8, 28), - ]; - pub const ROOT_KIND: NonterminalKind = NonterminalKind::SourceUnit; - pub fn create(version: Version) -> std::result::Result { - if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { + pub fn create( + language_version: Version, + ) -> std::result::Result { + if LanguageFacts::SUPPORTED_VERSIONS + .binary_search(&language_version) + .is_ok() + { Ok(Self { - version_is_at_least_0_4_11: Version::new(0, 4, 11) <= version, - version_is_at_least_0_4_12: Version::new(0, 4, 12) <= version, - version_is_at_least_0_4_14: Version::new(0, 4, 14) <= version, - version_is_at_least_0_4_16: Version::new(0, 4, 16) <= version, - version_is_at_least_0_4_21: Version::new(0, 4, 21) <= version, - version_is_at_least_0_4_22: Version::new(0, 4, 22) <= version, - version_is_at_least_0_4_25: Version::new(0, 4, 25) <= version, - version_is_at_least_0_5_0: Version::new(0, 5, 0) <= version, - version_is_at_least_0_5_3: Version::new(0, 5, 3) <= version, - version_is_at_least_0_5_5: Version::new(0, 5, 5) <= version, - version_is_at_least_0_5_8: Version::new(0, 5, 8) <= version, - version_is_at_least_0_5_10: Version::new(0, 5, 10) <= version, - version_is_at_least_0_5_12: Version::new(0, 5, 12) <= version, - version_is_at_least_0_5_14: Version::new(0, 5, 14) <= version, - version_is_at_least_0_6_0: Version::new(0, 6, 0) <= version, - version_is_at_least_0_6_2: Version::new(0, 6, 2) <= version, - version_is_at_least_0_6_5: Version::new(0, 6, 5) <= version, - version_is_at_least_0_6_7: Version::new(0, 6, 7) <= version, - version_is_at_least_0_6_8: Version::new(0, 6, 8) <= version, - version_is_at_least_0_6_11: Version::new(0, 6, 11) <= version, - version_is_at_least_0_7_0: Version::new(0, 7, 0) <= version, - version_is_at_least_0_7_1: Version::new(0, 7, 1) <= version, - version_is_at_least_0_7_4: Version::new(0, 7, 4) <= version, - version_is_at_least_0_8_0: Version::new(0, 8, 0) <= version, - version_is_at_least_0_8_4: Version::new(0, 8, 4) <= version, - version_is_at_least_0_8_7: Version::new(0, 8, 7) <= version, - version_is_at_least_0_8_8: Version::new(0, 8, 8) <= version, - version_is_at_least_0_8_13: Version::new(0, 8, 13) <= version, - version_is_at_least_0_8_18: Version::new(0, 8, 18) <= version, - version_is_at_least_0_8_19: Version::new(0, 8, 19) <= version, - version_is_at_least_0_8_22: Version::new(0, 8, 22) <= version, - version_is_at_least_0_8_24: Version::new(0, 8, 24) <= version, - version_is_at_least_0_8_25: Version::new(0, 8, 25) <= version, - version_is_at_least_0_8_27: Version::new(0, 8, 27) <= version, - version, + version_is_at_least_0_4_11: Version::new(0, 4, 11) <= language_version, + version_is_at_least_0_4_12: Version::new(0, 4, 12) <= language_version, + version_is_at_least_0_4_14: Version::new(0, 4, 14) <= language_version, + version_is_at_least_0_4_16: Version::new(0, 4, 16) <= language_version, + version_is_at_least_0_4_21: Version::new(0, 4, 21) <= language_version, + version_is_at_least_0_4_22: Version::new(0, 4, 22) <= language_version, + version_is_at_least_0_4_25: Version::new(0, 4, 25) <= language_version, + version_is_at_least_0_5_0: Version::new(0, 5, 0) <= language_version, + version_is_at_least_0_5_3: Version::new(0, 5, 3) <= language_version, + version_is_at_least_0_5_5: Version::new(0, 5, 5) <= language_version, + version_is_at_least_0_5_8: Version::new(0, 5, 8) <= language_version, + version_is_at_least_0_5_10: Version::new(0, 5, 10) <= language_version, + version_is_at_least_0_5_12: Version::new(0, 5, 12) <= language_version, + version_is_at_least_0_5_14: Version::new(0, 5, 14) <= language_version, + version_is_at_least_0_6_0: Version::new(0, 6, 0) <= language_version, + version_is_at_least_0_6_2: Version::new(0, 6, 2) <= language_version, + version_is_at_least_0_6_5: Version::new(0, 6, 5) <= language_version, + version_is_at_least_0_6_7: Version::new(0, 6, 7) <= language_version, + version_is_at_least_0_6_8: Version::new(0, 6, 8) <= language_version, + version_is_at_least_0_6_11: Version::new(0, 6, 11) <= language_version, + version_is_at_least_0_7_0: Version::new(0, 7, 0) <= language_version, + version_is_at_least_0_7_1: Version::new(0, 7, 1) <= language_version, + version_is_at_least_0_7_4: Version::new(0, 7, 4) <= language_version, + version_is_at_least_0_8_0: Version::new(0, 8, 0) <= language_version, + version_is_at_least_0_8_4: Version::new(0, 8, 4) <= language_version, + version_is_at_least_0_8_7: Version::new(0, 8, 7) <= language_version, + version_is_at_least_0_8_8: Version::new(0, 8, 8) <= language_version, + version_is_at_least_0_8_13: Version::new(0, 8, 13) <= language_version, + version_is_at_least_0_8_18: Version::new(0, 8, 18) <= language_version, + version_is_at_least_0_8_19: Version::new(0, 8, 19) <= language_version, + version_is_at_least_0_8_22: Version::new(0, 8, 22) <= language_version, + version_is_at_least_0_8_24: Version::new(0, 8, 24) <= language_version, + version_is_at_least_0_8_25: Version::new(0, 8, 25) <= language_version, + version_is_at_least_0_8_27: Version::new(0, 8, 27) <= language_version, + language_version, }) } else { Err(ParserInitializationError::UnsupportedLanguageVersion( - version, + language_version, )) } } - pub fn version(&self) -> &Version { - &self.version + pub fn language_version(&self) -> &Version { + &self.language_version } /******************************************** * Parser Functions ********************************************/ diff --git a/crates/solidity/outputs/cargo/crate/src/generated/parser/parse_output.rs b/crates/solidity/outputs/cargo/crate/src/generated/parser/parse_output.rs index 9ec833b5a7..83a71d08f4 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/parser/parse_output.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/parser/parse_output.rs @@ -3,15 +3,15 @@ use crate::cst::{Cursor, Node, TextIndex}; use crate::parser::ParseError; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseOutput { - pub(crate) parse_tree: Node, + pub(crate) tree: Node, pub(crate) errors: Vec, } impl ParseOutput { - pub fn tree(&self) -> Node { - self.parse_tree.clone() + pub fn tree(&self) -> &Node { + &self.tree } pub fn errors(&self) -> &Vec { @@ -24,6 +24,6 @@ impl ParseOutput { /// Creates a cursor that starts at the root of the parse tree. pub fn create_tree_cursor(&self) -> Cursor { - self.parse_tree.clone().cursor_with_offset(TextIndex::ZERO) + self.tree.clone().cursor_with_offset(TextIndex::ZERO) } } diff --git a/crates/solidity/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs b/crates/solidity/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs index c9fc3e44e5..ac5d02b724 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs @@ -94,7 +94,7 @@ where Node::nonterminal(no_match.kind.unwrap(), trivia_nodes) }; ParseOutput { - parse_tree: tree, + tree, errors: vec![ParseError::new( start..start + input.into(), no_match.expected_terminals, @@ -158,18 +158,17 @@ where )); ParseOutput { - parse_tree: Node::nonterminal(topmost_node.kind, new_children), + tree: Node::nonterminal(topmost_node.kind, new_children), errors, } } else { - let parse_tree = Node::Nonterminal(topmost_node); + let tree = Node::Nonterminal(topmost_node); let errors = stream.into_errors(); // Sanity check: Make sure that succesful parse is equivalent to not having any invalid nodes debug_assert_eq!( errors.is_empty(), - parse_tree - .clone() + tree.clone() .cursor_with_offset(TextIndex::ZERO) .remaining_nodes() .all(|edge| edge @@ -178,7 +177,7 @@ where .is_none()) ); - ParseOutput { parse_tree, errors } + ParseOutput { tree, errors } } } } diff --git a/crates/solidity/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs b/crates/solidity/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs new file mode 100644 index 0000000000..e61f6b8089 --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs @@ -0,0 +1,95 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use semver::Version; + +pub struct LanguageFacts; + +impl LanguageFacts { + pub const NAME: &'static str = "Solidity"; + + pub const SUPPORTED_VERSIONS: &'static [Version] = &[ + Version::new(0, 4, 11), + Version::new(0, 4, 12), + Version::new(0, 4, 13), + Version::new(0, 4, 14), + Version::new(0, 4, 15), + Version::new(0, 4, 16), + Version::new(0, 4, 17), + Version::new(0, 4, 18), + Version::new(0, 4, 19), + Version::new(0, 4, 20), + Version::new(0, 4, 21), + Version::new(0, 4, 22), + Version::new(0, 4, 23), + Version::new(0, 4, 24), + Version::new(0, 4, 25), + Version::new(0, 4, 26), + Version::new(0, 5, 0), + Version::new(0, 5, 1), + Version::new(0, 5, 2), + Version::new(0, 5, 3), + Version::new(0, 5, 4), + Version::new(0, 5, 5), + Version::new(0, 5, 6), + Version::new(0, 5, 7), + Version::new(0, 5, 8), + Version::new(0, 5, 9), + Version::new(0, 5, 10), + Version::new(0, 5, 11), + Version::new(0, 5, 12), + Version::new(0, 5, 13), + Version::new(0, 5, 14), + Version::new(0, 5, 15), + Version::new(0, 5, 16), + Version::new(0, 5, 17), + Version::new(0, 6, 0), + Version::new(0, 6, 1), + Version::new(0, 6, 2), + Version::new(0, 6, 3), + Version::new(0, 6, 4), + Version::new(0, 6, 5), + Version::new(0, 6, 6), + Version::new(0, 6, 7), + Version::new(0, 6, 8), + Version::new(0, 6, 9), + Version::new(0, 6, 10), + Version::new(0, 6, 11), + Version::new(0, 6, 12), + Version::new(0, 7, 0), + Version::new(0, 7, 1), + Version::new(0, 7, 2), + Version::new(0, 7, 3), + Version::new(0, 7, 4), + Version::new(0, 7, 5), + Version::new(0, 7, 6), + Version::new(0, 8, 0), + Version::new(0, 8, 1), + Version::new(0, 8, 2), + Version::new(0, 8, 3), + Version::new(0, 8, 4), + Version::new(0, 8, 5), + Version::new(0, 8, 6), + Version::new(0, 8, 7), + Version::new(0, 8, 8), + Version::new(0, 8, 9), + Version::new(0, 8, 10), + Version::new(0, 8, 11), + Version::new(0, 8, 12), + Version::new(0, 8, 13), + Version::new(0, 8, 14), + Version::new(0, 8, 15), + Version::new(0, 8, 16), + Version::new(0, 8, 17), + Version::new(0, 8, 18), + Version::new(0, 8, 19), + Version::new(0, 8, 20), + Version::new(0, 8, 21), + Version::new(0, 8, 22), + Version::new(0, 8, 23), + Version::new(0, 8, 24), + Version::new(0, 8, 25), + Version::new(0, 8, 26), + Version::new(0, 8, 27), + Version::new(0, 8, 28), + ]; +} diff --git a/crates/solidity/outputs/cargo/crate/src/generated/utils/mod.rs b/crates/solidity/outputs/cargo/crate/src/generated/utils/mod.rs new file mode 100644 index 0000000000..57bd4f108e --- /dev/null +++ b/crates/solidity/outputs/cargo/crate/src/generated/utils/mod.rs @@ -0,0 +1,6 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +#[path = "generated/language_facts.rs"] +mod language_facts; + +pub use language_facts::LanguageFacts; diff --git a/crates/solidity/outputs/cargo/crate/src/lib.rs b/crates/solidity/outputs/cargo/crate/src/lib.rs index 515f523f68..bd40dc9c6c 100644 --- a/crates/solidity/outputs/cargo/crate/src/lib.rs +++ b/crates/solidity/outputs/cargo/crate/src/lib.rs @@ -1,46 +1,4 @@ +mod extensions; mod generated; pub use generated::*; - -#[cfg(feature = "__experimental_bindings_api")] -pub fn transform_built_ins_node(node: &generated::cst::Node) -> generated::cst::Node { - use std::rc::Rc; - - use generated::cst::{Edge, Node, NonterminalNode, TerminalNode}; - - use crate::cst::TerminalKind; - - match node { - Node::Nonterminal(nonterminal) => { - let NonterminalNode { - kind, - text_len, - children, - } = nonterminal.as_ref(); - let children = children - .iter() - .map(|edge| Edge { - label: edge.label, - node: transform_built_ins_node(&edge.node), - }) - .collect(); - let nonterminal = Rc::new(NonterminalNode { - kind: *kind, - text_len: *text_len, - children, - }); - Node::Nonterminal(nonterminal) - } - Node::Terminal(terminal) => { - let TerminalNode { kind, text } = terminal.as_ref(); - let terminal = match terminal.as_ref().kind { - TerminalKind::Identifier => Rc::new(TerminalNode { - kind: *kind, - text: text.replace('$', "%"), - }), - _ => Rc::clone(terminal), - }; - Node::Terminal(terminal) - } - } -} diff --git a/crates/solidity/outputs/cargo/tests/src/bindings.rs b/crates/solidity/outputs/cargo/tests/src/bindings.rs deleted file mode 100644 index c346ab6afb..0000000000 --- a/crates/solidity/outputs/cargo/tests/src/bindings.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use semver::Version; -use slang_solidity::bindings::{self, Bindings}; -use slang_solidity::cst::TextIndex; -use slang_solidity::parser::Parser; -use slang_solidity::transform_built_ins_node; - -use crate::resolver::TestsPathResolver; - -pub fn create_bindings(version: &Version) -> Result { - let parser = Parser::create(version.clone())?; - let mut bindings = - bindings::create_with_resolver(version.clone(), Arc::new(TestsPathResolver {})); - - let built_ins_parse_output = parser.parse(Parser::ROOT_KIND, bindings::get_built_ins(version)); - assert!( - built_ins_parse_output.is_valid(), - "built-ins parse without errors" - ); - - let built_ins_cursor = transform_built_ins_node(&built_ins_parse_output.tree()) - .cursor_with_offset(TextIndex::ZERO); - - bindings.add_system_file("built_ins.sol", built_ins_cursor); - Ok(bindings) -} diff --git a/crates/solidity/outputs/cargo/tests/src/bindings_assertions/assertions.rs b/crates/solidity/outputs/cargo/tests/src/bindings_assertions/assertions.rs index 42d533ed5d..94aeca6f2d 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_assertions/assertions.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_assertions/assertions.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use once_cell::sync::Lazy; use regex::Regex; use semver::{Version, VersionReq}; -use slang_solidity::bindings::{Bindings, Definition}; +use slang_solidity::bindings::{BindingGraph, Definition}; use slang_solidity::cst::{Cursor, TerminalKind}; use thiserror::Error; @@ -308,21 +308,25 @@ fn search_asserted_node_backwards(mut cursor: Cursor, anchor_column: usize) -> O None } -/// Checks that the given `assertions` are fulfilled in the given `bindings` for +/// Checks that the given `assertions` are fulfilled in the given `binding_graph` for /// the indicated `version`. Only references can have version requirements, and /// the absence of a requirement means the assertion should hold for all /// language versions. /// pub fn check_assertions( - bindings: &Bindings, + binding_graph: &BindingGraph, assertions: &Assertions<'_>, version: &Version, ) -> Result { let mut failures: Vec = Vec::new(); - check_definitions(bindings, assertions.definitions.values(), &mut failures); + check_definitions( + binding_graph, + assertions.definitions.values(), + &mut failures, + ); check_references( - bindings, + binding_graph, version, assertions.references.iter(), &assertions.definitions, @@ -342,24 +346,24 @@ pub fn check_assertions( } fn check_definitions<'a>( - bindings: &Bindings, + binding_graph: &BindingGraph, definitions: impl Iterator>, failures: &mut Vec, ) { for assertion in definitions { - if let Err(failure) = find_definition(bindings, assertion) { + if let Err(failure) = find_definition(binding_graph, assertion) { failures.push(failure); } } } fn find_definition<'a>( - bindings: &'a Bindings, + binding_graph: &'a BindingGraph, assertion: &DefinitionAssertion<'_>, ) -> Result, String> { let DefinitionAssertion { cursor, .. } = assertion; - let Some(definition) = bindings.definition_at(cursor) else { + let Some(definition) = binding_graph.definition_at(cursor) else { return Err(format!("{assertion} failed: not found")); }; @@ -367,21 +371,23 @@ fn find_definition<'a>( } fn check_references<'a>( - bindings: &Bindings, + binding_graph: &BindingGraph, version: &Version, references: impl Iterator>, definitions: &HashMap>, failures: &mut Vec, ) { for assertion in references { - if let Err(failure) = check_reference_assertion(bindings, definitions, version, assertion) { + if let Err(failure) = + check_reference_assertion(binding_graph, definitions, version, assertion) + { failures.push(failure); } } } fn check_reference_assertion( - bindings: &Bindings, + binding_graph: &BindingGraph, definitions: &HashMap>, version: &Version, assertion: &ReferenceAssertion<'_>, @@ -396,7 +402,7 @@ fn check_reference_assertion( true }; - let resolution = match find_and_resolve_reference(bindings, assertion) { + let resolution = match find_and_resolve_reference(binding_graph, assertion) { Ok(resolution) => resolution, Err(err) => { if version_matches { @@ -411,11 +417,11 @@ fn check_reference_assertion( match (version_matches, id) { (true, None) => { if let Some(resolved_handle) = resolution { - let resolved_cursor = resolved_handle.get_cursor().unwrap(); + let resolved_cursor = resolved_handle.get_cursor(); let resolved_file = resolved_handle.get_file(); return Err(format!( "{assertion} failed: expected not to resolve, but instead resolved to {resolved}", - resolved = DisplayCursor(&resolved_cursor, resolved_file.get_path()) + resolved = DisplayCursor(resolved_cursor, resolved_file.get_path()) )); } } @@ -425,14 +431,15 @@ fn check_reference_assertion( "{assertion} failed: did not resolve or ambiguous resolution" )); }; - let resolved_cursor = resolved_handle.get_cursor().unwrap(); - let expected_handle = lookup_referenced_definition(bindings, definitions, assertion)?; - let expected_cursor = expected_handle.get_cursor().unwrap(); + let resolved_cursor = resolved_handle.get_cursor(); + let expected_handle = + lookup_referenced_definition(binding_graph, definitions, assertion)?; + let expected_cursor = expected_handle.get_cursor(); if expected_cursor != resolved_cursor { return Err(format!( "{assertion} failed: expected resolve to {expected}, but instead resolved to {resolved}", - resolved = DisplayCursor(&resolved_cursor, resolved_handle.get_file().get_path()), - expected = DisplayCursor(&expected_cursor, expected_handle.get_file().get_path()), + resolved = DisplayCursor(resolved_cursor, resolved_handle.get_file().get_path()), + expected = DisplayCursor(expected_cursor, expected_handle.get_file().get_path()), )); } } @@ -445,15 +452,15 @@ fn check_reference_assertion( } (false, Some(_)) => { if let Some(resolved_handle) = resolution { - let resolved_cursor = resolved_handle.get_cursor().unwrap(); + let resolved_cursor = resolved_handle.get_cursor(); let referenced_handle = - lookup_referenced_definition(bindings, definitions, assertion)?; - let referenced_cursor = referenced_handle.get_cursor().unwrap(); + lookup_referenced_definition(binding_graph, definitions, assertion)?; + let referenced_cursor = referenced_handle.get_cursor(); if referenced_cursor == resolved_cursor { return Err(format!( "{assertion} failed: expected to not resolve to {resolved} in this version", resolved = - DisplayCursor(&resolved_cursor, resolved_handle.get_file().get_path()), + DisplayCursor(resolved_cursor, resolved_handle.get_file().get_path()), )); } } @@ -464,12 +471,12 @@ fn check_reference_assertion( } fn find_and_resolve_reference<'a>( - bindings: &'a Bindings, + binding_graph: &'a BindingGraph, assertion: &ReferenceAssertion<'_>, ) -> Result>, String> { let ReferenceAssertion { cursor, .. } = assertion; - let Some(reference) = bindings.reference_at(cursor) else { + let Some(reference) = binding_graph.reference_at(cursor) else { return Err(format!("{assertion} failed: not found")); }; @@ -479,7 +486,7 @@ fn find_and_resolve_reference<'a>( } fn lookup_referenced_definition<'a>( - bindings: &'a Bindings, + binding_graph: &'a BindingGraph, definitions: &HashMap>, assertion: &ReferenceAssertion<'_>, ) -> Result, String> { @@ -490,5 +497,5 @@ fn lookup_referenced_definition<'a>( let Some(definition) = definitions.get(id) else { return Err(format!("{assertion} failed: reference is undefined")); }; - find_definition(bindings, definition) + find_definition(binding_graph, definition) } diff --git a/crates/solidity/outputs/cargo/tests/src/bindings_assertions/runner.rs b/crates/solidity/outputs/cargo/tests/src/bindings_assertions/runner.rs index fc009f9a68..2da4fe4aff 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_assertions/runner.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_assertions/runner.rs @@ -1,17 +1,18 @@ use std::fs; +use std::rc::Rc; use anyhow::Result; use infra_utils::cargo::CargoWorkspace; use semver::Version; -use slang_solidity::diagnostic; use slang_solidity::parser::Parser; +use slang_solidity::{bindings, diagnostic}; -use crate::bindings::create_bindings; use crate::bindings_assertions::assertions::{ check_assertions, collect_assertions_into, Assertions, }; use crate::generated::VERSION_BREAKS; use crate::multi_part_file::{split_multi_file, Part}; +use crate::resolver::TestsPathResolver; pub fn run(group_name: &str, test_name: &str) -> Result<()> { let file_name = format!("{test_name}.sol"); @@ -29,7 +30,8 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { fn check_assertions_with_version(version: &Version, contents: &str) -> Result<()> { let parser = Parser::create(version.clone())?; - let mut bindings = create_bindings(version)?; + let mut binding_graph = + bindings::create_with_resolver(version.clone(), Rc::new(TestsPathResolver {}))?; let mut assertions = Assertions::new(); let mut skipped = 0; @@ -53,7 +55,7 @@ fn check_assertions_with_version(version: &Version, contents: &str) -> Result<() eprintln!("\nParse errors for version {version}\nFile: {file_path}\n{report}"); } - bindings.add_user_file(file_path, parse_output.create_tree_cursor()); + binding_graph.add_user_file(file_path, parse_output.create_tree_cursor()); skipped += collect_assertions_into( &mut assertions, parse_output.create_tree_cursor(), @@ -63,14 +65,14 @@ fn check_assertions_with_version(version: &Version, contents: &str) -> Result<() } if let Some(context) = multi_part.context { - let context_definition = bindings + let context_definition = binding_graph .lookup_definition_by_name(context) .expect("context definition to be found") .to_handle(); - bindings.set_context(&context_definition); + binding_graph.set_context(&context_definition); } - let result = check_assertions(&bindings, &assertions, version); + let result = check_assertions(&binding_graph, &assertions, version); match result { Ok(count) => { 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 7298d965d5..423b7c9910 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs @@ -4,7 +4,7 @@ use std::ops::Range; use anyhow::Result; use ariadne::{Color, Config, FnCache, Label, Report, ReportBuilder, ReportKind, Source}; use metaslang_bindings::ResolutionError; -use slang_solidity::bindings::{Bindings, Definition, Reference}; +use slang_solidity::bindings::{BindingGraph, Definition, Reference}; use slang_solidity::diagnostic; use super::runner::ParsedPart; @@ -12,13 +12,13 @@ use super::runner::ParsedPart; type ReportSpan<'a> = (&'a str, Range); pub(crate) fn render_bindings( - bindings: &Bindings, + binding_graph: &BindingGraph, parsed_parts: &[ParsedPart<'_>], ) -> Result<(String, bool)> { let mut buffer: Vec = Vec::new(); let mut all_resolved = true; - let all_definitions = collect_all_definitions(bindings); + let all_definitions = collect_all_definitions(binding_graph); for part in parsed_parts { if !part.parse_output.is_valid() { @@ -28,7 +28,7 @@ pub(crate) fn render_bindings( buffer.push(b'\n'); } - let part_references = bindings + let part_references = binding_graph .all_references() .filter(|reference| reference.get_file().is_user_path(part.path)); let (bindings_report, part_all_resolved) = @@ -52,10 +52,10 @@ pub(crate) fn render_bindings( // We collect all non built-in definitions in a vector to be able to identify // them by a numeric index -fn collect_all_definitions(bindings: &Bindings) -> Vec> { +fn collect_all_definitions(binding_graph: &BindingGraph) -> Vec> { let mut definitions: Vec> = Vec::new(); - for definition in bindings.all_definitions() { - if definition.get_file().is_user() && definition.get_cursor().is_some() { + for definition in binding_graph.all_definitions() { + if definition.get_file().is_user() { definitions.push(definition); } } @@ -86,15 +86,12 @@ fn build_report_for_part<'a>( .with_config(Config::default().with_color(false)); for (index, definition) in all_definitions.iter().enumerate() { - let Some(cursor) = definition.get_cursor() else { - continue; - }; if !definition.get_file().is_user_path(part.path) { continue; } let range = { - let range = cursor.text_range(); + let range = definition.get_cursor().text_range(); let start = part.contents[..range.start.utf8].chars().count(); let end = part.contents[..range.end.utf8].chars().count(); start..end @@ -107,12 +104,8 @@ fn build_report_for_part<'a>( let mut all_resolved = true; for reference in part_references { - let Some(cursor) = reference.get_cursor() else { - continue; - }; - let range = { - let range = cursor.text_range(); + let range = reference.get_cursor().text_range(); let start = part.contents[..range.start.utf8].chars().count(); let end = part.contents[..range.end.utf8].chars().count(); start..end 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 4d7217163f..35ee7d233b 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs @@ -1,19 +1,21 @@ use std::fs; +use std::rc::Rc; use anyhow::Result; use infra_utils::cargo::CargoWorkspace; use infra_utils::codegen::CodegenFileSystem; use infra_utils::paths::PathExtensions; use metaslang_graph_builder::graph::Graph; +use slang_solidity::bindings; use slang_solidity::cst::KindTypes; use slang_solidity::parser::{ParseOutput, Parser}; use super::graph::graphviz::render as render_graphviz_graph; use super::graph::mermaid::render as render_mermaid_graph; use super::renderer::render_bindings; -use crate::bindings::create_bindings; use crate::generated::VERSION_BREAKS; use crate::multi_part_file::{split_multi_file, Part}; +use crate::resolver::TestsPathResolver; pub(crate) struct ParsedPart<'a> { pub path: &'a str, @@ -42,7 +44,8 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { for version in &VERSION_BREAKS { let parser = Parser::create(version.clone())?; - let mut bindings = create_bindings(version)?; + let mut binding_graph = + bindings::create_with_resolver(version.clone(), Rc::new(TestsPathResolver {}))?; let mut parsed_parts: Vec> = Vec::new(); let multi_part = split_multi_file(&contents); @@ -52,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 = - bindings.add_user_file_returning_graph(path, parse_output.create_tree_cursor()); + let graph = binding_graph + .add_user_file_returning_graph(path, parse_output.create_tree_cursor()); parsed_parts.push(ParsedPart { path, contents, @@ -63,14 +66,14 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { } if let Some(context) = multi_part.context { - let context_definition = bindings + let context_definition = binding_graph .lookup_definition_by_name(context) .expect("context definition to be found") .to_handle(); - bindings.set_context(&context_definition); + binding_graph.set_context(&context_definition); } - let (bindings_output, all_resolved) = render_bindings(&bindings, &parsed_parts)?; + let (bindings_output, all_resolved) = render_bindings(&binding_graph, &parsed_parts)?; let parse_success = parsed_parts.iter().all(|part| part.parse_output.is_valid()); let status = if parse_success && all_resolved { diff --git a/crates/solidity/outputs/cargo/tests/src/built_ins.rs b/crates/solidity/outputs/cargo/tests/src/built_ins.rs index 995602d5cb..f73340ea33 100644 --- a/crates/solidity/outputs/cargo/tests/src/built_ins.rs +++ b/crates/solidity/outputs/cargo/tests/src/built_ins.rs @@ -1,13 +1,14 @@ use anyhow::Result; +use slang_solidity::bindings::built_ins::get_built_ins_contents; +use slang_solidity::diagnostic; use slang_solidity::parser::Parser; -use slang_solidity::{bindings, diagnostic}; use crate::generated::VERSION_BREAKS; #[test] fn test_built_ins_parse_successfully() -> Result<()> { for version in &VERSION_BREAKS { - let built_ins = bindings::get_built_ins(version); + let built_ins = get_built_ins_contents(version); let parser = Parser::create(version.clone())?; let parse_output = parser.parse(Parser::ROOT_KIND, built_ins); diff --git a/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_cursor.rs b/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_cursor.rs index 26d8a6c656..396aa136b7 100644 --- a/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_cursor.rs +++ b/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_cursor.rs @@ -91,6 +91,7 @@ fn using_the_cursor() -> Result<()> { let identifiers: Vec<_> = parse_output .tree() + .clone() .descendants() .filter(|edge| edge.is_terminal_with_kind(TerminalKind::Identifier)) .map(|identifier| identifier.unparse()) @@ -105,6 +106,7 @@ fn using_the_cursor() -> Result<()> { let identifiers: Vec<_> = parse_output .tree() + .clone() .descendants() .filter(|edge| edge.label == Some(EdgeLabel::Name)) .filter(|edge| edge.is_terminal_with_kind(TerminalKind::Identifier)) diff --git a/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_parser.rs b/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_parser.rs index 69fb289bcb..db3ce9390b 100644 --- a/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_parser.rs +++ b/crates/solidity/outputs/cargo/tests/src/doc_examples/using_the_parser.rs @@ -38,9 +38,9 @@ fn using_the_parser() -> Result<()> { // --8<-- [end:assert-is-valid] // --8<-- [start:inspect-tree] - let parse_tree = parse_output.tree(); + let tree = parse_output.tree(); - let contract = parse_tree.as_nonterminal().unwrap(); + let contract = tree.as_nonterminal().unwrap(); assert_eq!(contract.kind, NonterminalKind::ContractDefinition); assert_eq!(contract.children.len(), 7); diff --git a/crates/solidity/outputs/cargo/tests/src/lib.rs b/crates/solidity/outputs/cargo/tests/src/lib.rs index b62d53c1a2..519c795d76 100644 --- a/crates/solidity/outputs/cargo/tests/src/lib.rs +++ b/crates/solidity/outputs/cargo/tests/src/lib.rs @@ -3,7 +3,6 @@ use metaslang_bindings as _; mod binding_rules; -mod bindings; mod bindings_assertions; mod bindings_output; mod built_ins; diff --git a/crates/solidity/outputs/cargo/tests/src/resolver.rs b/crates/solidity/outputs/cargo/tests/src/resolver.rs index d428e24fb2..34c395adcd 100644 --- a/crates/solidity/outputs/cargo/tests/src/resolver.rs +++ b/crates/solidity/outputs/cargo/tests/src/resolver.rs @@ -1,16 +1,22 @@ use metaslang_bindings::PathResolver; +use slang_solidity::cst::{Cursor, KindTypes}; pub struct TestsPathResolver; -impl PathResolver for TestsPathResolver { - fn resolve_path(&self, context_path: &str, path_to_resolve: &str) -> Option { - if is_relative_path(path_to_resolve) { +impl PathResolver for TestsPathResolver { + 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, '"' | '\''))?; + + if is_relative_path(path) { // Relative import: the actual path will be computed using the // context path (ie. the path of the importing source unit) - normalize_path(path_to_resolve, get_parent_path(context_path)) + normalize_path(path, get_parent_path(context_path)) } else { // Direct import: this path will be used as-is - Some(path_to_resolve.to_string()) + Some(path.to_owned()) } } } diff --git a/crates/solidity/outputs/cargo/tests/src/trivia.rs b/crates/solidity/outputs/cargo/tests/src/trivia.rs index 1eadc077cc..85885beac3 100644 --- a/crates/solidity/outputs/cargo/tests/src/trivia.rs +++ b/crates/solidity/outputs/cargo/tests/src/trivia.rs @@ -29,6 +29,7 @@ fn compare_end_of_lines(input: &str, expected: &[&str]) -> Result<()> { let actual = output .tree() + .clone() .descendants() .filter(|edge| edge.is_terminal_with_kind(TerminalKind::EndOfLine)) .map(|eol| eol.unparse()) diff --git a/crates/solidity/outputs/cargo/wasm/Cargo.toml b/crates/solidity/outputs/cargo/wasm/Cargo.toml index d4a6399e87..a653d8509a 100644 --- a/crates/solidity/outputs/cargo/wasm/Cargo.toml +++ b/crates/solidity/outputs/cargo/wasm/Cargo.toml @@ -21,7 +21,10 @@ solidity_language = { workspace = true } paste = { workspace = true } semver = { workspace = true } serde_json = { workspace = true } -slang_solidity = { workspace = true } +slang_solidity = { workspace = true, features = [ + "__experimental_bindings_api", + "__private_compilation_api", +] } wit-bindgen = { workspace = true } [lints] diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/generated/bindings.rs b/crates/solidity/outputs/cargo/wasm/src/generated/generated/bindgen.rs similarity index 100% rename from crates/solidity/outputs/cargo/wasm/src/generated/generated/bindings.rs rename to crates/solidity/outputs/cargo/wasm/src/generated/generated/bindgen.rs diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/generated/config.json b/crates/solidity/outputs/cargo/wasm/src/generated/generated/config.json index 395999cccb..3444eb9169 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/generated/config.json +++ b/crates/solidity/outputs/cargo/wasm/src/generated/generated/config.json @@ -1,5 +1,65 @@ { "mappings": { + "nomic-foundation:slang:bindings:definition.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.name-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.definiens-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:binding-location": { + "Variant": { + "as_direct_union_of_resource_classes": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.file-id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.cursor()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.language-version()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.binding-graph()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.tree()": { + "Function": { + "as_getter": true + } + }, "nomic-foundation:slang:cst:terminal-kind": { "Enum": { "as_typescript_enum": true @@ -105,7 +165,7 @@ "as_iterator": true } }, - "nomic-foundation:slang:parser:parser.version()": { + "nomic-foundation:slang:parser:parser.language-version()": { "Function": { "as_getter": true } diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit new file mode 100644 index 0000000000..1721afc82d --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit @@ -0,0 +1,72 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface bindings { + use cst.{cursor}; + + /// A giant graph that contains name binding information for all source files within the compilation unit. + /// It stores cursors to all definitions and references, and can resolve the edges between them. + resource binding-graph { + /// If the provided cursor points at a definition `Identifier`, it will return the + /// corresponding definition. Otherwise, it will return `undefined`. + definition-at: func(cursor: borrow) -> option; + + /// If the provided cursor points at a reference `Identifier`, it will return the + /// corresponding reference. Otherwise, it will return `undefined`. + reference-at: func(cursor: borrow) -> option; + } + + /// Represents a definition in the binding graph. + resource definition { + /// Returns a unique numerical identifier of the definition. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the definition's name. + /// For `contract X {}`, that is the location of the `X` `Identifier` node. + name-location: func() -> binding-location; + + /// Returns the location of the definition's definiens. + /// For `contract X {}`, that is the location of the parent `ContractDefinition` node. + definiens-location: func() -> binding-location; + } + + /// Represents a reference in the binding graph. + resource reference { + /// Returns a unique numerical identifier of the reference. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the reference. + /// For `new X()`, that is the location of the `X` `Identifier` node. + location: func() -> binding-location; + + /// Returns a list of all definitions related to this reference. + /// Most references have a single definition, but some have multiple, such as when a symbol + /// is imported from another file, and renamed (re-defined) in the current file. + definitions: func() -> list; + } + + /// Represents a location of a symbol (definition or reference) in the binding graph. + /// It can either be in a user file, or a built-in in the language. + variant binding-location { + /// Represents a location of a user-defined symbol in a user file. + user-file(user-file-location), + /// Represents a location of a built-in symbol in the language. + built-in(built-in-location) + } + + /// Represents a location of a user-defined symbol in a user file. + resource user-file-location { + /// Returns the ID of the file that contains the symbol. + file-id: func() -> string; + + /// Returns a cursor to the CST node that contains the symbol. + cursor: func() -> cursor; + } + + /// Represents a location of a built-in symbol in the language. + resource built-in-location { + } +} diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit new file mode 100644 index 0000000000..c9db286d76 --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit @@ -0,0 +1,68 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface compilation { + use bindings.{binding-graph}; + use cst.{node, cursor}; + + /// A builder for creating compilation units. + /// Allows incrementally building a transitive list of all files and their imports. + /// + /// This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + /// This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + resource internal-compilation-builder { + /// Creates a new compilation builder for the specified language version. + create: static func(language-version: string) -> result; + + /// Adds a source file to the compilation unit. + add-file: func(id: string, contents: string) -> add-file-response; + + /// Resolves an import in the source file to the destination file. + resolve-import: func(source-file-id: string, import-path: borrow, destination-file-id: string) -> result<_, string>; + + /// Builds and returns the final compilation unit. + build: func() -> compilation-unit; + } + + /// Contains information about imports found in an added source file. + record add-file-response { + /// List of cursors to any import paths found in the file. + import-paths: list, + } + + /// A complete compilation unit is a complete view over all compilation inputs: + /// + /// - All source files, stored as CSTs. + /// - Name binding graph that exposes relationships between definitions and references in these files. + /// - Any relevant compilation options. + /// + /// It also exposes utilities to traverse the compilation unit and query it. + resource compilation-unit { + /// Returns the language version this compilation unit is configured for. + language-version: func() -> string; + + /// Returns a list of all files in the compilation unit. + files: func() -> list; + + /// Returns the file with the specified ID, if it exists. + file: func(id: string) -> option; + + /// Calculates name binding information for all source files within the compilation unit. + /// Returns a graph that contains all found definitions and their references. + /// + /// Note: building this graph is an expensive operation. + /// It is done lazily on the first access, and cached thereafter. + binding-graph: func() -> result; + } + + /// A single source file in the compilation unit. + resource file { + /// Returns the unique identifier of this file. + id: func() -> string; + + /// Returns the syntax tree of this file. + tree: func() -> node; + + /// Creates a cursor for traversing the syntax tree of this file. + create-tree-cursor: func() -> cursor; + } +} diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/parser.wit b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/parser.wit index bbe8faa7c8..df41c2e0fc 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/parser.wit +++ b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/parser.wit @@ -10,15 +10,11 @@ interface parser { /// This represents the starting point for parsing a complete source file. root-kind: static func() -> nonterminal-kind; - /// Returns a list of language versions supported by this parser. - /// Each version string represents a specific grammar configuration. - supported-versions: static func() -> list; - /// Creates a new parser instance for the specified language version. - create: static func(version: string) -> result; + create: static func(language-version: string) -> result; /// Returns the language version this parser instance is configured for. - version: func() -> string; + language-version: func() -> string; /// Parses the input string starting from the specified nonterminal kind. parse: func(kind: nonterminal-kind, input: string) -> parse-output; diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/utils.wit b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/utils.wit new file mode 100644 index 0000000000..9bb93e160f --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/utils.wit @@ -0,0 +1,9 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface utils { + /// Provides information about the supported language versions and the grammar. + resource language-facts { + /// Returns a list of language versions supported by Slang, sorted ascendingly. + supported-versions: static func() -> list; + } +} diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/world.wit b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/world.wit index c5e1378860..bb9b7b10be 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/world.wit +++ b/crates/solidity/outputs/cargo/wasm/src/generated/interface/generated/world.wit @@ -4,6 +4,9 @@ package nomic-foundation:slang; world slang { export ast; + export bindings; + export compilation; export cst; export parser; + export utils; } diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/mod.rs index c84d868d87..418be83d23 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/mod.rs @@ -1,10 +1,10 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. -#[path = "./generated/bindings.rs"] -mod bindings; +#[path = "./generated/bindgen.rs"] +mod bindgen; mod utils; mod wrappers; struct World; -crate::wasm_crate::bindings::export!(World with_types_in crate::wasm_crate::bindings); +crate::wasm_crate::bindgen::export!(World with_types_in crate::wasm_crate::bindgen); diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs index cf50ecacd2..2ccdcb4777 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs @@ -6,10 +6,10 @@ mod selectors; use crate::wasm_crate::utils::IntoFFI; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::ast::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::ast::{ Guest, GuestSelectors, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Node, NonterminalNodeBorrow, }; } 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 new file mode 100644 index 0000000000..6163cf964f --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs @@ -0,0 +1,176 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use crate::wasm_crate::utils::{define_rc_wrapper, define_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::{ + BindingGraph, BindingGraphBorrow, BindingLocation, BuiltInLocation, BuiltInLocationBorrow, + CursorBorrow, Definition, DefinitionBorrow, Guest, GuestBindingGraph, GuestBuiltInLocation, + GuestDefinition, GuestReference, GuestUserFileLocation, Reference, ReferenceBorrow, + UserFileLocation, UserFileLocationBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::Cursor; +} + +mod rust { + pub use crate::rust_crate::bindings::{ + BindingGraph, BindingLocation, BuiltInLocation, 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 { + type BindingGraph = BindingGraphWrapper; + + type Definition = DefinitionWrapper; + type Reference = ReferenceWrapper; + + type UserFileLocation = UserFileLocationWrapper; + type BuiltInLocation = BuiltInLocationWrapper; +} + +//================================================ +// +// resource binding-graph +// +//================================================ + +define_rc_wrapper! { BindingGraph { + fn definition_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .definition_at(&cursor._borrow_ffi()) + .map(rust::Definition::from) + .map(IntoFFI::_into_ffi) + } + + fn reference_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .reference_at(&cursor._borrow_ffi()) + .map(rust::Reference::from) + .map(IntoFFI::_into_ffi) + } +} } + +//================================================ +// +// resource definition +// +//================================================ + +define_wrapper! { Definition { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn name_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().name_location.clone()._into_ffi() + } + + fn definiens_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().definiens_location.clone()._into_ffi() + } +} } + +//================================================ +// +// resource reference +// +//================================================ + +define_wrapper! { Reference { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn location(&self) -> ffi::BindingLocation { + self._borrow_ffi().location.clone()._into_ffi() + } + + fn definitions(&self) -> Vec { + self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + } +} } + +//================================================ +// +// variant binding-location +// +//================================================ + +impl IntoFFI for rust::BindingLocation { + #[inline] + fn _into_ffi(self) -> ffi::BindingLocation { + match self { + Self::BuiltIn(location) => ffi::BindingLocation::BuiltIn(location._into_ffi()), + Self::UserFile(location) => ffi::BindingLocation::UserFile(location._into_ffi()), + } + } +} + +//================================================ +// +// resource user-file-location +// +//================================================ + +define_wrapper! { UserFileLocation { + fn file_id(&self) -> String { + self._borrow_ffi().file_id().to_owned() + } + + fn cursor(&self) -> ffi::Cursor { + self._borrow_ffi().cursor().to_owned()._into_ffi() + } +} } + +//================================================ +// +// resource built-in-location +// +//================================================ + +define_wrapper! { BuiltInLocation { +} } diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs new file mode 100644 index 0000000000..d168910020 --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs @@ -0,0 +1,122 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::rc::Rc; + +use semver::Version; + +use crate::wasm_crate::utils::{define_rc_wrapper, define_refcell_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::BindingGraph; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::compilation::{ + AddFileResponse, CompilationUnit, CompilationUnitBorrow, CursorBorrow, File, FileBorrow, + Guest, GuestCompilationUnit, GuestFile, GuestInternalCompilationBuilder, + InternalCompilationBuilder, InternalCompilationBuilderBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{Cursor, Node}; +} + +mod rust { + pub use crate::rust_crate::compilation::{ + AddFileResponse, CompilationUnit, File, InternalCompilationBuilder, + }; +} + +impl ffi::Guest for crate::wasm_crate::World { + type InternalCompilationBuilder = InternalCompilationBuilderWrapper; + type CompilationUnit = CompilationUnitWrapper; + type File = FileWrapper; +} + +//================================================ +// +// resource internal-compilation-builder +// +//================================================ + +define_refcell_wrapper! { InternalCompilationBuilder { + fn create(language_version: String) -> Result { + let language_version = Version::parse(&language_version).map_err(|e| e.to_string())?; + + rust::InternalCompilationBuilder::create(language_version) + .map(IntoFFI::_into_ffi) + .map_err(|e| e.to_string()) + } + + fn add_file(&self, id: String, contents: String) -> ffi::AddFileResponse { + self._borrow_mut_ffi() + .add_file(id, &contents) + ._into_ffi() + } + + fn resolve_import(&self, source_file_id: String, import_path: ffi::CursorBorrow<'_>, destination_file_id: String) -> Result<(), String> { + self._borrow_mut_ffi() + .resolve_import(&source_file_id, &import_path._borrow_ffi(), destination_file_id) + .map_err(|e| e.to_string()) + } + + fn build(&self) -> ffi::CompilationUnit { + Rc::new(self._borrow_ffi().build())._into_ffi() + } +} } + +//================================================ +// +// record add-file-response +// +//================================================ + +impl IntoFFI for rust::AddFileResponse { + #[inline] + fn _into_ffi(self) -> ffi::AddFileResponse { + let Self { import_paths } = self; + + ffi::AddFileResponse { + import_paths: import_paths.into_iter().map(IntoFFI::_into_ffi).collect(), + } + } +} + +//================================================ +// +// resource compilation-unit +// +//================================================ + +define_rc_wrapper! { CompilationUnit { + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() + } + + fn files(&self) -> Vec { + self._borrow_ffi().files().into_iter().map(IntoFFI::_into_ffi).collect() + } + + fn file(&self, id: String) -> Option { + self._borrow_ffi().file(&id).map(IntoFFI::_into_ffi) + } + + fn binding_graph(&self) -> Result { + self._borrow_ffi().binding_graph().as_ref().map(Rc::clone).map(IntoFFI::_into_ffi).map_err(|e| e.to_string()) + } +} } + +//================================================ +// +// resource file +// +//================================================ + +define_rc_wrapper! { File { + fn id(&self) -> String { + self._borrow_ffi().id().to_owned() + } + + fn tree(&self) -> ffi::Node { + self._borrow_ffi().tree().to_owned()._into_ffi() + } + + fn create_tree_cursor(&self) -> ffi::Cursor { + self._borrow_ffi().create_tree_cursor()._into_ffi() + } +} } diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs index 444cab2eab..deca6e2bb4 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs @@ -7,7 +7,7 @@ use crate::wasm_crate::utils::{ }; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ AncestorsIterator, AncestorsIteratorBorrow, Cursor, CursorBorrow, CursorIterator, CursorIteratorBorrow, Edge, EdgeLabel, Guest, GuestAncestorsIterator, GuestCursor, GuestCursorIterator, GuestNonterminalNode, GuestQuery, GuestQueryMatchIterator, diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/mod.rs index e05fee5635..6b289c658d 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/mod.rs @@ -1,5 +1,8 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. mod ast; +mod bindings; +mod compilation; mod cst; mod parser; +mod utils; diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs index 311c2b9d8a..7d000977d8 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs @@ -3,10 +3,10 @@ use crate::wasm_crate::utils::{define_wrapper, FromFFI, IntoFFI}; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Cursor, Node, TextRange, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::parser::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::parser::{ Guest, GuestParseError, GuestParseOutput, GuestParser, NonterminalKind, ParseError, ParseErrorBorrow, ParseOutput, ParseOutputBorrow, Parser, ParserBorrow, }; @@ -33,22 +33,15 @@ define_wrapper! { Parser { rust::Parser::ROOT_KIND._into_ffi() } - fn supported_versions() -> Vec { - rust::Parser::SUPPORTED_VERSIONS - .iter() - .map(|v| v.to_string()) - .collect() - } - - fn create(version: String) -> Result { - semver::Version::parse(&version) - .map_err(|_| format!("Invalid semantic version: '{version}'")) + fn create(language_version: String) -> Result { + semver::Version::parse(&language_version) + .map_err(|_| format!("Invalid semantic version: '{language_version}'")) .and_then(|version| rust::Parser::create(version).map_err(|e| e.to_string())) .map(IntoFFI::_into_ffi) } - fn version(&self) -> String { - self._borrow_ffi().version.to_string() + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() } fn parse(&self, kind: ffi::NonterminalKind, input: String) -> ffi::ParseOutput { @@ -80,7 +73,7 @@ define_wrapper! { ParseError { define_wrapper! { ParseOutput { fn tree(&self) -> ffi::Node { - self._borrow_ffi().tree()._into_ffi() + self._borrow_ffi().tree().clone()._into_ffi() } fn errors(&self) -> Vec { diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs new file mode 100644 index 0000000000..b478ca4299 --- /dev/null +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs @@ -0,0 +1,32 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use crate::wasm_crate::utils::define_wrapper; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::utils::{ + Guest, GuestLanguageFacts, LanguageFacts, LanguageFactsBorrow, + }; +} + +mod rust { + pub use crate::rust_crate::utils::LanguageFacts; +} + +impl ffi::Guest for crate::wasm_crate::World { + type LanguageFacts = LanguageFactsWrapper; +} + +//================================================ +// +// resource language-facts +// +//================================================ + +define_wrapper! { LanguageFacts { + fn supported_versions() -> Vec { + rust::LanguageFacts::SUPPORTED_VERSIONS + .iter() + .map(|v| v.to_string()) + .collect() + } +} } diff --git a/crates/solidity/outputs/npm/package/package.json b/crates/solidity/outputs/npm/package/package.json index 1f2a26bfda..c7fba2b62c 100644 --- a/crates/solidity/outputs/npm/package/package.json +++ b/crates/solidity/outputs/npm/package/package.json @@ -46,8 +46,11 @@ "exports": { ".": "./target/generated/index.mjs", "./ast": "./target/generated/ast/index.mjs", + "./bindings": "./target/generated/bindings/index.mjs", + "./compilation": "./target/generated/compilation/index.mjs", "./cst": "./target/generated/cst/index.mjs", - "./parser": "./target/generated/parser/index.mjs" + "./parser": "./target/generated/parser/index.mjs", + "./utils": "./target/generated/utils/index.mjs" }, "__dependencies_comment__": "__SLANG_NPM_PACKAGE_DEPENDENCIES__ (keep in sync)", "dependencies": { diff --git a/crates/solidity/outputs/npm/package/src/generated/bindings/index.mts b/crates/solidity/outputs/npm/package/src/generated/bindings/index.mts new file mode 100644 index 0000000000..7263f31bdc --- /dev/null +++ b/crates/solidity/outputs/npm/package/src/generated/bindings/index.mts @@ -0,0 +1,23 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; + +export const BindingGraph = generated.bindings.BindingGraph; +export type BindingGraph = generated.bindings.BindingGraph; + +export const Definition = generated.bindings.Definition; +export type Definition = generated.bindings.Definition; + +export const Reference = generated.bindings.Reference; +export type Reference = generated.bindings.Reference; + +export type BindingLocation = generated.bindings.BindingLocation; + +export const BindingLocationType = generated.bindings.BindingLocationType; +export type BindingLocationType = generated.bindings.BindingLocationType; + +export const UserFileLocation = generated.bindings.UserFileLocation; +export type UserFileLocation = generated.bindings.UserFileLocation; + +export const BuiltInLocation = generated.bindings.BuiltInLocation; +export type BuiltInLocation = generated.bindings.BuiltInLocation; diff --git a/crates/solidity/outputs/npm/package/src/generated/compilation/builder.mts b/crates/solidity/outputs/npm/package/src/generated/compilation/builder.mts new file mode 100644 index 0000000000..4649389f8b --- /dev/null +++ b/crates/solidity/outputs/npm/package/src/generated/compilation/builder.mts @@ -0,0 +1,114 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import { Cursor } from "../cst/index.mjs"; +import { CompilationUnit } from "./index.mjs"; + +import * as generated from "../../../wasm/index.mjs"; + +const InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; +type InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; + +/** + * User-provided options and callbacks necessary for the `CompilationBuilder` class to perform its job. + */ +export interface CompilationBuilderConfig { + /** + * The language version to parse files with. + */ + languageVersion: string; + + /** + * Callback used by this builder to load the contents of a file. + * + * The user is responsible for fetching the file from the filesystem. + * If the file is not found, the callback should return undefined. + * Any errors thrown by the callback will be propagated to the caller. + */ + readFile: (fileId: string) => Promise; + + /** + * Callback used by this builder to resolve an import path. + * For example, if a source file contains the following statement: + * + * ```solidity + * import {Foo} from "foo.sol"; + * ``` + * + * Then the API will invoke the callback with a cursor pointing to the `"foo.sol"` string literal. + * + * The user is responsible for resolving it to a file in the compilation, and return its ID. + * If the callback returns `undefined`, the import will stay unresolved. + * Any errors thrown by the callback will be propagated to the caller. + */ + resolveImport: (sourceFileId: string, importPath: Cursor) => Promise; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a list of all files and their imports. + */ +export class CompilationBuilder { + private readonly seenFiles: Set = new Set(); + + private constructor( + private readonly internalBuilder: InternalCompilationBuilder, + + /** + * The user-supplied configuration. + */ + public readonly config: CompilationBuilderConfig, + ) {} + + /** + * Creates a new compilation builder for the specified language version. + */ + public static create(config: CompilationBuilderConfig): CompilationBuilder { + const internalBuilder = InternalCompilationBuilder.create(config.languageVersion); + return new CompilationBuilder(internalBuilder, config); + } + + /** + * Adds a source file to the compilation unit. + * Typically, users only need to add the "root" file, which contains the main contract they are trying to analyze. + * Any files that are imported by the root file will be discovered and loaded automatically by the config callbacks. + * + * Adding multiple files (roots) is supported. For example, an IDE can choose to add all NPM dependencies, + * regardless of whether they are imported or not, to be able to query the definitions there. + * + * Adding a file that has already been added is a no-op. + */ + public async addFile(id: string): Promise { + if (this.seenFiles.has(id)) { + return; + } else { + this.seenFiles.add(id); + } + + const contents = await this.config.readFile(id); + if (contents === undefined) { + return; + } + + const { importPaths } = this.internalBuilder.addFile(id, contents); + + await Promise.all( + importPaths.map(async (importPath) => { + const destinationFileId = await this.config.resolveImport(id, importPath); + if (destinationFileId === undefined) { + return; + } + + this.internalBuilder.resolveImport(id, importPath, destinationFileId); + + await this.addFile(destinationFileId); + }), + ); + } + + /** + * Builds and returns the final compilation unit. + */ + public build(): CompilationUnit { + return this.internalBuilder.build(); + } +} diff --git a/crates/solidity/outputs/npm/package/src/generated/compilation/index.mts b/crates/solidity/outputs/npm/package/src/generated/compilation/index.mts new file mode 100644 index 0000000000..832fe073b8 --- /dev/null +++ b/crates/solidity/outputs/npm/package/src/generated/compilation/index.mts @@ -0,0 +1,16 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; +import * as builder from "./builder.mjs"; + +// This is a wrapper around 'generated.compilation.InternalCompilationBuilder': +export const CompilationBuilder = builder.CompilationBuilder; +export type CompilationBuilder = builder.CompilationBuilder; + +export type CompilationBuilderConfig = builder.CompilationBuilderConfig; + +export const CompilationUnit = generated.compilation.CompilationUnit; +export type CompilationUnit = generated.compilation.CompilationUnit; + +export const File = generated.compilation.File; +export type File = generated.compilation.File; diff --git a/crates/solidity/outputs/npm/package/src/generated/cst/index.mts b/crates/solidity/outputs/npm/package/src/generated/cst/index.mts index 9ceb7c6f40..2bb806862e 100644 --- a/crates/solidity/outputs/npm/package/src/generated/cst/index.mts +++ b/crates/solidity/outputs/npm/package/src/generated/cst/index.mts @@ -50,10 +50,9 @@ export type TextIndex = generated.cst.TextIndex; export type TextRange = generated.cst.TextRange; -/* - * Helpers: +/** + * Asserts that this node is a `NonterminalNode` with the provided kind and text. */ - export function assertIsNonterminalNode( node: unknown, kind?: NonterminalKind, @@ -72,6 +71,9 @@ export function assertIsNonterminalNode( } } +/** + * Asserts that this node is a `TerminalKind` with the provided kind and text. + */ export function assertIsTerminalNode(node: unknown, kind?: TerminalKind, text?: string): asserts node is TerminalNode { if (!(node instanceof TerminalNode)) { throw new Error("Node provided is not a TerminalNode."); diff --git a/crates/solidity/outputs/npm/package/src/generated/index.mts b/crates/solidity/outputs/npm/package/src/generated/index.mts index 847cd1d82b..9aa39203ba 100644 --- a/crates/solidity/outputs/npm/package/src/generated/index.mts +++ b/crates/solidity/outputs/npm/package/src/generated/index.mts @@ -1,5 +1,8 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. export * as ast from "./ast/index.mjs"; +export * as bindings from "./bindings/index.mjs"; +export * as compilation from "./compilation/index.mjs"; export * as cst from "./cst/index.mjs"; export * as parser from "./parser/index.mjs"; +export * as utils from "./utils/index.mjs"; diff --git a/crates/solidity/outputs/npm/package/src/generated/utils/index.mts b/crates/solidity/outputs/npm/package/src/generated/utils/index.mts new file mode 100644 index 0000000000..f1b7780fcb --- /dev/null +++ b/crates/solidity/outputs/npm/package/src/generated/utils/index.mts @@ -0,0 +1,6 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; + +export const LanguageFacts = generated.utils.LanguageFacts; +export type LanguageFacts = generated.utils.LanguageFacts; diff --git a/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts new file mode 100644 index 0000000000..b2b0d4dc94 --- /dev/null +++ b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts @@ -0,0 +1,153 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangBindings { + export { BindingGraph }; + export { Definition }; + export { Reference }; + export { UserFileLocation }; + export { BuiltInLocation }; + export { BindingLocation }; + export { BindingLocationType }; +} +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Represents a location of a symbol (definition or reference) in the binding graph. + * It can either be in a user file, or a built-in in the language. + */ +export type BindingLocation = UserFileLocation | BuiltInLocation; +export enum BindingLocationType { + UserFileLocation = "UserFileLocation", + BuiltInLocation = "BuiltInLocation", +} + +/** + * A giant graph that contains name binding information for all source files within the compilation unit. + * It stores cursors to all definitions and references, and can resolve the edges between them. + */ +export class BindingGraph { + /** + * If the provided cursor points at a definition `Identifier`, it will return the + * corresponding definition. Otherwise, it will return `undefined`. + */ + definitionAt(cursor: Cursor): Definition | undefined; + /** + * If the provided cursor points at a reference `Identifier`, it will return the + * corresponding reference. Otherwise, it will return `undefined`. + */ + referenceAt(cursor: Cursor): Reference | undefined; +} + +/** + * Represents a location of a built-in symbol in the language. + */ +export class BuiltInLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.BuiltInLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): this; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): this is BuiltInLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): undefined; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): false; +} + +/** + * Represents a definition in the binding graph. + */ +export class Definition { + /** + * Returns a unique numerical identifier of the definition. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the definition's name. + * For `contract X {}`, that is the location of the `X` `Identifier` node. + */ + get nameLocation(): BindingLocation; + /** + * Returns the location of the definition's definiens. + * For `contract X {}`, that is the location of the parent `ContractDefinition` node. + */ + get definiensLocation(): BindingLocation; +} + +/** + * Represents a reference in the binding graph. + */ +export class Reference { + /** + * Returns a unique numerical identifier of the reference. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the reference. + * For `new X()`, that is the location of the `X` `Identifier` node. + */ + get location(): BindingLocation; + /** + * Returns a list of all definitions related to this reference. + * Most references have a single definition, but some have multiple, such as when a symbol + * is imported from another file, and renamed (re-defined) in the current file. + */ + definitions(): Definition[]; +} + +/** + * Represents a location of a user-defined symbol in a user file. + */ +export class UserFileLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.UserFileLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): this; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): this is UserFileLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): undefined; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): false; + + /** + * Returns the ID of the file that contains the symbol. + */ + get fileId(): string; + /** + * Returns a cursor to the CST node that contains the symbol. + */ + get cursor(): Cursor; +} diff --git a/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts new file mode 100644 index 0000000000..3c1a6831c1 --- /dev/null +++ b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts @@ -0,0 +1,98 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangCompilation { + export { InternalCompilationBuilder }; + export { CompilationUnit }; + export { File }; +} +import type { BindingGraph } from "./nomic-foundation-slang-bindings.js"; +export { BindingGraph }; +import type { Node } from "./nomic-foundation-slang-cst.js"; +export { Node }; +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Contains information about imports found in an added source file. + */ +export interface AddFileResponse { + /** + * List of cursors to any import paths found in the file. + */ + importPaths: Cursor[]; +} + +/** + * A complete compilation unit is a complete view over all compilation inputs: + * + * - All source files, stored as CSTs. + * - Name binding graph that exposes relationships between definitions and references in these files. + * - Any relevant compilation options. + * + * It also exposes utilities to traverse the compilation unit and query it. + */ +export class CompilationUnit { + /** + * Returns the language version this compilation unit is configured for. + */ + get languageVersion(): string; + /** + * Returns a list of all files in the compilation unit. + */ + files(): File[]; + /** + * Returns the file with the specified ID, if it exists. + */ + file(id: string): File | undefined; + /** + * Calculates name binding information for all source files within the compilation unit. + * Returns a graph that contains all found definitions and their references. + * + * Note: building this graph is an expensive operation. + * It is done lazily on the first access, and cached thereafter. + */ + get bindingGraph(): BindingGraph; +} + +/** + * A single source file in the compilation unit. + */ +export class File { + /** + * Returns the unique identifier of this file. + */ + get id(): string; + /** + * Returns the syntax tree of this file. + */ + get tree(): Node; + /** + * Creates a cursor for traversing the syntax tree of this file. + */ + createTreeCursor(): Cursor; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a transitive list of all files and their imports. + * + * This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + * This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + */ +export class InternalCompilationBuilder { + /** + * Creates a new compilation builder for the specified language version. + */ + static create(languageVersion: string): InternalCompilationBuilder; + /** + * Adds a source file to the compilation unit. + */ + addFile(id: string, contents: string): AddFileResponse; + /** + * Resolves an import in the source file to the destination file. + */ + resolveImport(sourceFileId: string, importPath: Cursor, destinationFileId: string): void; + /** + * Builds and returns the final compilation unit. + */ + build(): CompilationUnit; +} diff --git a/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts index bc99b8e132..6ac5bb7305 100644 --- a/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts +++ b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts @@ -65,19 +65,14 @@ export class Parser { * This represents the starting point for parsing a complete source file. */ static rootKind(): NonterminalKind; - /** - * Returns a list of language versions supported by this parser. - * Each version string represents a specific grammar configuration. - */ - static supportedVersions(): string[]; /** * Creates a new parser instance for the specified language version. */ - static create(version: string): Parser; + static create(languageVersion: string): Parser; /** * Returns the language version this parser instance is configured for. */ - get version(): string; + get languageVersion(): string; /** * Parses the input string starting from the specified nonterminal kind. */ diff --git a/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts new file mode 100644 index 0000000000..0a3e19de1c --- /dev/null +++ b/crates/solidity/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts @@ -0,0 +1,15 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangUtils { + export { LanguageFacts }; +} + +/** + * Provides information about the supported language versions and the grammar. + */ +export class LanguageFacts { + /** + * Returns a list of language versions supported by Slang, sorted ascendingly. + */ + static supportedVersions(): string[]; +} diff --git a/crates/solidity/outputs/npm/package/wasm/generated/solidity_cargo_wasm.component.d.ts b/crates/solidity/outputs/npm/package/wasm/generated/solidity_cargo_wasm.component.d.ts index c7614866dd..959f0ad174 100644 --- a/crates/solidity/outputs/npm/package/wasm/generated/solidity_cargo_wasm.component.d.ts +++ b/crates/solidity/outputs/npm/package/wasm/generated/solidity_cargo_wasm.component.d.ts @@ -2,7 +2,13 @@ import { NomicFoundationSlangCst } from "./interfaces/nomic-foundation-slang-cst.js"; import { NomicFoundationSlangAst } from "./interfaces/nomic-foundation-slang-ast.js"; +import { NomicFoundationSlangBindings } from "./interfaces/nomic-foundation-slang-bindings.js"; +import { NomicFoundationSlangCompilation } from "./interfaces/nomic-foundation-slang-compilation.js"; import { NomicFoundationSlangParser } from "./interfaces/nomic-foundation-slang-parser.js"; +import { NomicFoundationSlangUtils } from "./interfaces/nomic-foundation-slang-utils.js"; export * as cst from "./interfaces/nomic-foundation-slang-cst.js"; export * as ast from "./interfaces/nomic-foundation-slang-ast.js"; +export * as bindings from "./interfaces/nomic-foundation-slang-bindings.js"; +export * as compilation from "./interfaces/nomic-foundation-slang-compilation.js"; export * as parser from "./interfaces/nomic-foundation-slang-parser.js"; +export * as utils from "./interfaces/nomic-foundation-slang-utils.js"; diff --git a/crates/solidity/outputs/npm/tests/src/compilation/binding-graph.test.mts b/crates/solidity/outputs/npm/tests/src/compilation/binding-graph.test.mts new file mode 100644 index 0000000000..085833e71a --- /dev/null +++ b/crates/solidity/outputs/npm/tests/src/compilation/binding-graph.test.mts @@ -0,0 +1,123 @@ +import assert from "node:assert"; +import { NonterminalKind, TerminalKind } from "@nomicfoundation/slang/cst"; +import { createBuilder } from "./common.mjs"; +import { BindingLocation } from "@nomicfoundation/slang/bindings"; + +test("binding graph", async () => { + const builder = await createBuilder(); + await builder.addFile("child.sol"); + + const unit = builder.build(); + const cursor = unit.file("child.sol")!.createTreeCursor(); + + // import { Parent } from "./parent.sol"; + // ^^^^^^ + assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); + assert.equal(cursor.node.unparse(), "Parent"); + + { + const definition = unit.bindingGraph.definitionAt(cursor)!; + assertUserFileLocation(definition.nameLocation, "child.sol", TerminalKind.Identifier, 3); + assertUserFileLocation(definition.definiensLocation, "child.sol", NonterminalKind.ImportDeconstructionSymbol, 3); + } + + { + const reference = unit.bindingGraph.referenceAt(cursor)!; + assertUserFileLocation(reference.location, "child.sol", TerminalKind.Identifier, 3); + + const defs = reference.definitions(); + assert.equal(defs.length, 1); + + assertUserFileLocation(defs[0]!.nameLocation, "parent.sol", TerminalKind.Identifier, 3); + assertUserFileLocation(defs[0]!.definiensLocation, "parent.sol", NonterminalKind.ContractDefinition, 2); + } + + // contract Child is Parent { + // ^^^^^ + assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); + assert.equal(cursor.node.unparse(), "Child"); + + { + const definition = unit.bindingGraph.definitionAt(cursor)!; + assertUserFileLocation(definition.nameLocation, "child.sol", TerminalKind.Identifier, 5); + assertUserFileLocation(definition.definiensLocation, "child.sol", NonterminalKind.ContractDefinition, 4); + } + + { + assert.equal(unit.bindingGraph.referenceAt(cursor), undefined); + } + + // contract Child is Parent { + // ^^^^^^ + assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); + assert.equal(cursor.node.unparse(), "Parent"); + + { + assert.equal(unit.bindingGraph.definitionAt(cursor), undefined); + } + + { + const reference = unit.bindingGraph.referenceAt(cursor)!; + assertUserFileLocation(reference.location, "child.sol", TerminalKind.Identifier, 5); + + const defs = reference.definitions(); + assert.equal(defs.length, 2); + + assertUserFileLocation(defs[0]!.nameLocation, "child.sol", TerminalKind.Identifier, 3); + assertUserFileLocation(defs[0]!.definiensLocation, "child.sol", NonterminalKind.ImportDeconstructionSymbol, 3); + + assertUserFileLocation(defs[1]!.nameLocation, "parent.sol", TerminalKind.Identifier, 3); + assertUserFileLocation(defs[1]!.definiensLocation, "parent.sol", NonterminalKind.ContractDefinition, 2); + } + + // function foo() public pure { + // ^^^ + assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); + assert.equal(cursor.node.unparse(), "foo"); + + { + const definition = unit.bindingGraph.definitionAt(cursor)!; + assertUserFileLocation(definition.nameLocation, "child.sol", TerminalKind.Identifier, 6); + assertUserFileLocation(definition.definiensLocation, "child.sol", NonterminalKind.FunctionDefinition, 6); + } + + { + assert.equal(unit.bindingGraph.referenceAt(cursor), undefined); + } + + // assert(true); + // ^^^^^^ + assert(cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); + assert.equal(cursor.node.unparse(), "assert"); + + { + assert.equal(unit.bindingGraph.definitionAt(cursor), undefined); + } + + { + const reference = unit.bindingGraph.referenceAt(cursor)!; + assertUserFileLocation(reference.location, "child.sol", TerminalKind.Identifier, 7); + + const defs = reference.definitions(); + assert.equal(defs.length, 1); + + assert(defs[0]!.nameLocation.isBuiltInLocation()); + assert(defs[0]!.definiensLocation.isBuiltInLocation()); + } + + // Done! No more identifiers in the file. + assert(!cursor.goToNextTerminalWithKind(TerminalKind.Identifier)); +}); + +function assertUserFileLocation( + location: BindingLocation, + fileId: string, + kind: TerminalKind | NonterminalKind, + line: number, +) { + assert(location.isUserFileLocation()); + + assert.equal(location.fileId, fileId); + assert.equal(location.cursor.node.kind, kind); + assert.equal(location.cursor.textRange.start.line, line); +} diff --git a/crates/solidity/outputs/npm/tests/src/compilation/builder.test.mts b/crates/solidity/outputs/npm/tests/src/compilation/builder.test.mts new file mode 100644 index 0000000000..1fde3e41f0 --- /dev/null +++ b/crates/solidity/outputs/npm/tests/src/compilation/builder.test.mts @@ -0,0 +1,44 @@ +import assert from "node:assert"; +import { CompilationBuilder } from "@nomicfoundation/slang/compilation"; +import { NonterminalKind } from "@nomicfoundation/slang/cst"; +import { createBuilder } from "./common.mjs"; + +test("empty builder", async () => { + const builder = await createBuilder(); + + assertFiles(builder, []); +}); + +test("parent file without imports", async () => { + const builder = await createBuilder(); + + await builder.addFile("parent.sol"); + + assertFiles(builder, ["parent.sol"]); +}); + +test("child file with one import", async () => { + const builder = await createBuilder(); + + await builder.addFile("child.sol"); + + assertFiles(builder, ["parent.sol", "child.sol"]); +}); + +function assertFiles(builder: CompilationBuilder, expected: string[]) { + const unit = builder.build(); + + const actual = unit.files().map((file) => { + assert.strictEqual(file.tree.kind, NonterminalKind.SourceUnit); + assert.strictEqual(file.tree.id, file.createTreeCursor().node.id); + + return file.id; + }); + + for (const id of actual) { + const file = unit.file(id)!; + assert.strictEqual(file.id, id); + } + + assert.deepEqual(actual.sort(), expected.sort()); +} diff --git a/crates/solidity/outputs/npm/tests/src/compilation/common.mts b/crates/solidity/outputs/npm/tests/src/compilation/common.mts new file mode 100644 index 0000000000..ea7805da0b --- /dev/null +++ b/crates/solidity/outputs/npm/tests/src/compilation/common.mts @@ -0,0 +1,25 @@ +import path from "node:path"; +import assert from "node:assert"; +import { CompilationBuilder } from "@nomicfoundation/slang/compilation"; +import { readRepoFile } from "../utils/files.mjs"; + +export async function createBuilder(): Promise { + const builder = CompilationBuilder.create({ + languageVersion: "0.8.0", + + readFile: async (fileId) => { + return readRepoFile("crates/solidity/outputs/npm/tests/src/compilation/inputs", fileId); + }, + + resolveImport: async (sourceFileId, importPath) => { + const importLiteral = importPath.node.unparse(); + assert(importLiteral.startsWith('"')); + assert(importLiteral.endsWith('"')); + + const importString = importLiteral.slice(1, -1); + return path.join(sourceFileId, "..", importString); + }, + }); + + return builder; +} diff --git a/crates/solidity/outputs/npm/tests/src/compilation/inputs/child.sol b/crates/solidity/outputs/npm/tests/src/compilation/inputs/child.sol new file mode 100644 index 0000000000..074be93fbd --- /dev/null +++ b/crates/solidity/outputs/npm/tests/src/compilation/inputs/child.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity x.x.x; + +import { Parent } from "./parent.sol"; + +contract Child is Parent { + function foo() public pure { + assert(true); + } +} diff --git a/crates/solidity/outputs/npm/tests/src/compilation/inputs/parent.sol b/crates/solidity/outputs/npm/tests/src/compilation/inputs/parent.sol new file mode 100644 index 0000000000..9e6b2447c1 --- /dev/null +++ b/crates/solidity/outputs/npm/tests/src/compilation/inputs/parent.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity x.x.x; + +contract Parent {} diff --git a/crates/solidity/testing/perf/benches/iai/main.rs b/crates/solidity/testing/perf/benches/iai/main.rs index 1957df012e..e9216a4f29 100644 --- a/crates/solidity/testing/perf/benches/iai/main.rs +++ b/crates/solidity/testing/perf/benches/iai/main.rs @@ -7,8 +7,7 @@ use iai_callgrind::{ library_benchmark, library_benchmark_group, main, Direction, FlamegraphConfig, LibraryBenchmarkConfig, Tool, ValgrindTool, }; -use slang_solidity::bindings::Bindings; -use slang_solidity::parser::ParseOutput; +use slang_solidity::bindings::BindingGraph; use solidity_testing_perf::dataset::SourceFile; use solidity_testing_perf::tests::definitions::Dependencies; use solidity_testing_perf::tests::parser::ParsedFile; @@ -18,6 +17,14 @@ mod __dependencies_used_in_lib__ { } 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)] fn $name(payload: $payload) { @@ -33,12 +40,12 @@ macro_rules! define_benchmark { * * __SLANG_INFRA_BENCHMARKS_LIST__ (keep in sync) */ -define_benchmark!(parser, Vec); -define_benchmark!(cursor, Vec); -define_benchmark!(query, Vec); -define_benchmark!(init_bindings, ParseOutput); -define_benchmark!(definitions, Dependencies); -define_benchmark!(references, Bindings); +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); library_benchmark_group!( name = benchmarks; diff --git a/crates/solidity/testing/perf/src/lib.rs b/crates/solidity/testing/perf/src/lib.rs index 7819ab9ea0..0b25500b67 100644 --- a/crates/solidity/testing/perf/src/lib.rs +++ b/crates/solidity/testing/perf/src/lib.rs @@ -12,6 +12,14 @@ 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] fn $name() { @@ -24,10 +32,10 @@ mod unit_tests { /* * __SLANG_INFRA_BENCHMARKS_LIST__ (keep in sync) */ - define_test!(parser); - define_test!(cursor); - define_test!(query); + define_payload_test!(parser); + define_payload_test!(cursor); + define_payload_test!(query); define_test!(init_bindings); - define_test!(definitions); - define_test!(references); + define_payload_test!(definitions); + define_payload_test!(references); } diff --git a/crates/solidity/testing/perf/src/tests/definitions.rs b/crates/solidity/testing/perf/src/tests/definitions.rs index ecf6f92ccc..a41134c328 100644 --- a/crates/solidity/testing/perf/src/tests/definitions.rs +++ b/crates/solidity/testing/perf/src/tests/definitions.rs @@ -1,23 +1,26 @@ -use slang_solidity::bindings::Bindings; +use slang_solidity::bindings::BindingGraph; use crate::tests::parser::ParsedFile; pub struct Dependencies { - pub bindings: Bindings, + pub binding_graph: BindingGraph, pub files: Vec, } pub fn setup() -> Dependencies { - let bindings = super::init_bindings::run(super::init_bindings::setup()); + let binding_graph = super::init_bindings::run(); let files = super::parser::run(super::parser::setup()); - Dependencies { bindings, files } + Dependencies { + binding_graph, + files, + } } -pub fn run(dependencies: Dependencies) -> Bindings { +pub fn run(dependencies: Dependencies) -> BindingGraph { let mut definition_count = 0_usize; let Dependencies { - mut bindings, + mut binding_graph, files, } = dependencies; @@ -27,8 +30,8 @@ pub fn run(dependencies: Dependencies) -> Bindings { parse_output, } in &files { - bindings.add_user_file(path.to_str().unwrap(), parse_output.create_tree_cursor()); - definition_count += bindings + 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(); @@ -36,5 +39,5 @@ pub fn run(dependencies: Dependencies) -> Bindings { assert_eq!(definition_count, 2322, "Failed to fetch all definitions"); - bindings + binding_graph } diff --git a/crates/solidity/testing/perf/src/tests/init_bindings.rs b/crates/solidity/testing/perf/src/tests/init_bindings.rs index 4dc9af819a..f46ffa186c 100644 --- a/crates/solidity/testing/perf/src/tests/init_bindings.rs +++ b/crates/solidity/testing/perf/src/tests/init_bindings.rs @@ -1,38 +1,24 @@ -use std::sync::Arc; +use std::rc::Rc; use metaslang_bindings::PathResolver; -use slang_solidity::bindings::{create_with_resolver, get_built_ins, Bindings}; -use slang_solidity::cst::TextIndex; -use slang_solidity::parser::{ParseOutput, Parser}; -use slang_solidity::transform_built_ins_node; +use slang_solidity::bindings::{create_with_resolver, BindingGraph}; +use slang_solidity::cst::{Cursor, KindTypes}; use crate::dataset::SOLC_VERSION; -pub fn setup() -> ParseOutput { - let parser = Parser::create(SOLC_VERSION).unwrap(); - - let built_ins = parser.parse(Parser::ROOT_KIND, get_built_ins(&SOLC_VERSION)); - - assert!(built_ins.is_valid(), "built-ins parse without errors"); - - built_ins -} - -pub fn run(built_ins: ParseOutput) -> Bindings { - let mut bindings = create_with_resolver(SOLC_VERSION, Arc::new(NoOpResolver {})); - - let built_ins_cursor = - transform_built_ins_node(&built_ins.tree()).cursor_with_offset(TextIndex::ZERO); - - bindings.add_system_file("built_ins.sol", built_ins_cursor); - - bindings +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: &str) -> Option { - Some(path_to_resolve.to_string()) +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/references.rs b/crates/solidity/testing/perf/src/tests/references.rs index 070f357b4e..fed8fb3bec 100644 --- a/crates/solidity/testing/perf/src/tests/references.rs +++ b/crates/solidity/testing/perf/src/tests/references.rs @@ -1,16 +1,16 @@ -use slang_solidity::bindings::Bindings; +use slang_solidity::bindings::BindingGraph; -pub fn setup() -> Bindings { +pub fn setup() -> BindingGraph { let dependencies = super::definitions::setup(); super::definitions::run(dependencies) } -pub fn run(bindings: Bindings) { +pub fn run(binding_graph: BindingGraph) { let mut reference_count = 0_usize; let mut resolved_references = 0_usize; - for reference in bindings.all_references() { + for reference in binding_graph.all_references() { if reference.get_file().is_system() { // skip built-ins continue; diff --git a/crates/solidity/testing/sanctuary/src/tests.rs b/crates/solidity/testing/sanctuary/src/tests.rs index 6571da0b11..948184697f 100644 --- a/crates/solidity/testing/sanctuary/src/tests.rs +++ b/crates/solidity/testing/sanctuary/src/tests.rs @@ -1,17 +1,18 @@ use std::cmp::min; use std::path::Path; -use std::sync::Arc; +use std::rc::Rc; use anyhow::Result; use infra_utils::paths::PathExtensions; use itertools::Itertools; use metaslang_bindings::PathResolver; use semver::Version; -use slang_solidity::bindings::Bindings; -use slang_solidity::cst::{Cursor, NonterminalKind, TextIndex, TextRange}; +use slang_solidity::bindings; +use slang_solidity::bindings::BindingGraph; +use slang_solidity::cst::{Cursor, KindTypes, NonterminalKind, TextRange}; use slang_solidity::diagnostic::{Diagnostic, Severity}; use slang_solidity::parser::{ParseOutput, Parser}; -use slang_solidity::{bindings, transform_built_ins_node}; +use slang_solidity::utils::LanguageFacts; use crate::datasets::{DataSet, SourceFile}; use crate::events::{Events, TestOutcome}; @@ -152,7 +153,7 @@ fn extract_compiler_version(compiler: &str) -> Option { panic!("Unrecognized compiler/version: '{compiler}'"); }; - if &version < Parser::SUPPORTED_VERSIONS.first().unwrap() { + if &version < LanguageFacts::SUPPORTED_VERSIONS.first().unwrap() { // Version is too early: return None; } @@ -186,9 +187,9 @@ fn run_bindings_check( output: &ParseOutput, ) -> Result> { let mut unresolved = Vec::new(); - let bindings = create_bindings(version, source_id, output)?; + let binding_graph = create_bindings(version, source_id, output)?; - for reference in bindings.all_references() { + for reference in binding_graph.all_references() { if reference.get_file().is_system() { // skip built-ins continue; @@ -196,7 +197,7 @@ fn run_bindings_check( // We're not interested in the exact definition a reference resolves // to, so we lookup all of them and fail if we find none. if reference.definitions().is_empty() { - let cursor = reference.get_cursor().unwrap(); + let cursor = reference.get_cursor().to_owned(); unresolved.push(UnresolvedReference { cursor }); } } @@ -204,37 +205,32 @@ fn run_bindings_check( Ok(unresolved) } -fn create_bindings(version: &Version, source_id: &str, output: &ParseOutput) -> Result { - let mut bindings = bindings::create_with_resolver( +fn create_bindings( + version: &Version, + source_id: &str, + output: &ParseOutput, +) -> Result { + let mut binding_graph = bindings::create_with_resolver( version.clone(), - Arc::new(SingleFileResolver { + Rc::new(SingleFileResolver { source_id: source_id.into(), }), - ); - let parser = Parser::create(version.clone())?; - let built_ins_tree = parser - .parse( - NonterminalKind::SourceUnit, - bindings::get_built_ins(version), - ) - .tree(); - let built_ins_cursor = - transform_built_ins_node(&built_ins_tree).cursor_with_offset(TextIndex::ZERO); + )?; + + binding_graph.add_user_file(source_id, output.create_tree_cursor()); - bindings.add_system_file("built_ins.sol", built_ins_cursor); - bindings.add_user_file(source_id, output.create_tree_cursor()); - Ok(bindings) + Ok(binding_graph) } -/// Bindings `PathResolver` that always resolves to the given `source_id`. +/// The `PathResolver` that always resolves to the given `source_id`. /// This is useful for Sanctuary since all dependencies are concatenated in the /// same file, but the import directives are retained. struct SingleFileResolver { source_id: String, } -impl PathResolver for SingleFileResolver { - fn resolve_path(&self, _context_path: &str, _path_to_resolve: &str) -> Option { +impl PathResolver for SingleFileResolver { + fn resolve_path(&self, _context_path: &str, _path_to_resolve: &Cursor) -> Option { Some(self.source_id.clone()) } } diff --git a/crates/testlang/outputs/cargo/crate/Cargo.toml b/crates/testlang/outputs/cargo/crate/Cargo.toml index eeb4213383..45a8685cba 100644 --- a/crates/testlang/outputs/cargo/crate/Cargo.toml +++ b/crates/testlang/outputs/cargo/crate/Cargo.toml @@ -11,6 +11,7 @@ name = "slang_testlang" default = [] __experimental_bindings_api = ["dep:metaslang_bindings"] __private_ariadne_errors = ["dep:ariadne"] +__private_compilation_api = [] __private_testing_utils = [] [build-dependencies] diff --git a/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs b/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs new file mode 100644 index 0000000000..eee8b7880f --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs @@ -0,0 +1,12 @@ +use semver::Version; + +use crate::bindings::BindingGraph; +use crate::parser::ParserInitializationError; + +#[allow(clippy::needless_pass_by_value)] +pub fn add_built_ins( + _binding_graph: &mut BindingGraph, + _version: Version, +) -> Result<(), ParserInitializationError> { + unreachable!("Built-ins are Solidity-specific") +} diff --git a/crates/testlang/outputs/cargo/crate/src/extensions/compilation/mod.rs b/crates/testlang/outputs/cargo/crate/src/extensions/compilation/mod.rs new file mode 100644 index 0000000000..ddbb6d5a87 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/extensions/compilation/mod.rs @@ -0,0 +1,15 @@ +use crate::cst::Cursor; + +pub struct ImportPathsExtractor; + +impl ImportPathsExtractor { + pub fn new() -> Self { + Self + } + + #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_value)] + pub fn extract(&self, _: Cursor) -> Vec { + unreachable!("Import paths are Solidity-specific") + } +} diff --git a/crates/testlang/outputs/cargo/crate/src/extensions/mod.rs b/crates/testlang/outputs/cargo/crate/src/extensions/mod.rs new file mode 100644 index 0000000000..78402f8764 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/extensions/mod.rs @@ -0,0 +1,8 @@ +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; + +#[cfg(feature = "__experimental_bindings_api")] +pub mod bindings; diff --git a/crates/testlang/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs b/crates/testlang/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs index 0c4a214fc9..b9d12d31f8 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/bindings/generated/built_ins.rs @@ -2,7 +2,8 @@ use semver::Version; +// TODO: This should be moved to the Solidity-specific 'extensions' sub-module. #[allow(unused_variables)] -pub fn get_contents(version: &Version) -> &'static str { +pub fn get_built_ins_contents(version: &Version) -> &'static str { "" } 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 66c8a8c4da..7803930461 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs @@ -4,31 +4,46 @@ mod binding_rules; #[path = "generated/built_ins.rs"] -mod built_ins; +pub mod built_ins; -use std::sync::Arc; +use std::rc::Rc; -use metaslang_bindings::{self, PathResolver}; use semver::Version; use crate::cst::KindTypes; -pub type Bindings = metaslang_bindings::Bindings; +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 BindingLocation = metaslang_bindings::BindingLocation; +pub type UserFileLocation = metaslang_bindings::UserFileLocation; + +pub use metaslang_bindings::{BuiltInLocation, PathResolver}; + +use crate::parser::ParserInitializationError; + +#[derive(thiserror::Error, Debug)] +pub enum BindingGraphInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} pub fn create_with_resolver( version: Version, - resolver: Arc, -) -> Bindings { - Bindings::create(version, binding_rules::BINDING_RULES_SOURCE, resolver) + resolver: Rc>, +) -> Result { + let mut binding_graph = BindingGraph::create( + version.clone(), + binding_rules::BINDING_RULES_SOURCE, + resolver, + ); + + crate::extensions::bindings::add_built_ins(&mut binding_graph, version)?; + + Ok(binding_graph) } #[cfg(feature = "__private_testing_utils")] pub fn get_binding_rules() -> &'static str { binding_rules::BINDING_RULES_SOURCE } - -pub fn get_built_ins(version: &semver::Version) -> &'static str { - built_ins::get_contents(version) -} diff --git a/crates/testlang/outputs/cargo/crate/src/generated/compilation/file.rs b/crates/testlang/outputs/cargo/crate/src/generated/compilation/file.rs new file mode 100644 index 0000000000..94eee8ad56 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/compilation/file.rs @@ -0,0 +1,47 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::collections::BTreeMap; + +use metaslang_cst::text_index::TextIndex; + +use crate::cst::{Cursor, Node}; + +#[derive(Clone)] +pub struct File { + id: String, + tree: Node, + + resolved_imports: BTreeMap, +} + +impl File { + pub(super) fn new(id: String, tree: Node) -> Self { + Self { + id, + tree, + + resolved_imports: BTreeMap::new(), + } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn tree(&self) -> &Node { + &self.tree + } + + pub fn create_tree_cursor(&self) -> Cursor { + self.tree.clone().cursor_with_offset(TextIndex::ZERO) + } + + pub(super) fn resolve_import(&mut self, import_path: &Cursor, destination_file_id: String) { + self.resolved_imports + .insert(import_path.node().id(), destination_file_id); + } + + pub(super) fn resolved_import(&self, import_path: &Cursor) -> Option<&String> { + self.resolved_imports.get(&import_path.node().id()) + } +} diff --git a/crates/testlang/outputs/cargo/crate/src/generated/compilation/internal_builder.rs b/crates/testlang/outputs/cargo/crate/src/generated/compilation/internal_builder.rs new file mode 100644 index 0000000000..a0a79d7944 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/compilation/internal_builder.rs @@ -0,0 +1,91 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::compilation::{CompilationUnit, File}; +use crate::cst::Cursor; +use crate::extensions::compilation::ImportPathsExtractor; +use crate::parser::{Parser, ParserInitializationError}; + +pub struct InternalCompilationBuilder { + parser: Parser, + imports: ImportPathsExtractor, + files: BTreeMap, +} + +#[derive(thiserror::Error, Debug)] +pub enum CompilationInitializationError { + #[error(transparent)] + ParserInitialization(#[from] ParserInitializationError), +} + +impl InternalCompilationBuilder { + pub fn create(language_version: Version) -> Result { + let parser = Parser::create(language_version)?; + + Ok(Self { + parser, + imports: ImportPathsExtractor::new(), + files: BTreeMap::new(), + }) + } + + pub fn add_file(&mut self, id: String, contents: &str) -> AddFileResponse { + if self.files.contains_key(&id) { + // Already added. No need to process it again: + return AddFileResponse { + import_paths: vec![], + }; + } + + let parse_output = self.parser.parse(Parser::ROOT_KIND, contents); + + let import_paths = self.imports.extract(parse_output.create_tree_cursor()); + + let file = File::new(id.clone(), parse_output.tree().clone()); + self.files.insert(id, file); + + AddFileResponse { import_paths } + } + + pub fn resolve_import( + &mut self, + source_file_id: &str, + import_path: &Cursor, + destination_file_id: String, + ) -> Result<(), ResolveImportError> { + self.files + .get_mut(source_file_id) + .ok_or_else(|| ResolveImportError::SourceFileNotFound(source_file_id.to_owned()))? + .resolve_import(import_path, destination_file_id); + + Ok(()) + } + + pub fn build(&self) -> CompilationUnit { + let language_version = self.parser.language_version().to_owned(); + + let files = self + .files + .iter() + .map(|(id, file)| (id.to_owned(), Rc::new(file.to_owned()))) + .collect(); + + CompilationUnit::new(language_version, files) + } +} + +pub struct AddFileResponse { + pub import_paths: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum ResolveImportError { + #[error( + "Source file not found: '{0}'. Make sure to add it first, before resolving its imports." + )] + SourceFileNotFound(String), +} diff --git a/crates/testlang/outputs/cargo/crate/src/generated/compilation/mod.rs b/crates/testlang/outputs/cargo/crate/src/generated/compilation/mod.rs new file mode 100644 index 0000000000..89544c6568 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/compilation/mod.rs @@ -0,0 +1,9 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +mod file; +mod internal_builder; +mod unit; + +pub use file::File; +pub use internal_builder::{AddFileResponse, InternalCompilationBuilder}; +pub use unit::CompilationUnit; diff --git a/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs b/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs new file mode 100644 index 0000000000..a15100e4df --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs @@ -0,0 +1,71 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::cell::OnceCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use semver::Version; + +use crate::bindings::{ + create_with_resolver, BindingGraph, BindingGraphInitializationError, PathResolver, +}; +use crate::compilation::File; +use crate::cst::{Cursor, KindTypes}; + +pub struct CompilationUnit { + language_version: Version, + files: BTreeMap>, + binding_graph: OnceCell, BindingGraphInitializationError>>, +} + +impl CompilationUnit { + pub(super) fn new(language_version: Version, files: BTreeMap>) -> Self { + Self { + language_version, + files, + binding_graph: OnceCell::new(), + } + } + + pub fn language_version(&self) -> &Version { + &self.language_version + } + + pub fn files(&self) -> Vec> { + self.files.values().cloned().collect() + } + + pub fn file(&self, id: &str) -> Option> { + self.files.get(id).cloned() + } + + pub fn binding_graph(&self) -> &Result, BindingGraphInitializationError> { + self.binding_graph.get_or_init(|| { + let resolver = Resolver { + files: self.files.clone(), + }; + + let mut binding_graph = + 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()); + } + + Ok(Rc::new(binding_graph)) + }) + } +} + +struct Resolver { + files: BTreeMap>, +} + +impl PathResolver for Resolver { + fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option { + self.files + .get(context_path)? + .resolved_import(path_to_resolve) + .cloned() + } +} diff --git a/crates/testlang/outputs/cargo/crate/src/generated/mod.rs b/crates/testlang/outputs/cargo/crate/src/generated/mod.rs index 4b9d48d9f9..3c700fafdc 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/mod.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/mod.rs @@ -2,6 +2,12 @@ #[cfg(feature = "__experimental_bindings_api")] pub mod bindings; +#[cfg(all( + feature = "__experimental_bindings_api", + feature = "__private_compilation_api" +))] +pub mod compilation; pub mod cst; pub mod diagnostic; pub mod parser; +pub mod utils; diff --git a/crates/testlang/outputs/cargo/crate/src/generated/parser/generated/parser.rs b/crates/testlang/outputs/cargo/crate/src/generated/parser/generated/parser.rs index ef1e4ba83a..588993e736 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/parser/generated/parser.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/parser/generated/parser.rs @@ -27,12 +27,13 @@ use crate::parser::scanner_macros::{ scan_not_followed_by, scan_one_or_more, scan_optional, scan_sequence, scan_zero_or_more, }; use crate::parser::ParseOutput; +use crate::utils::LanguageFacts; #[derive(Debug)] pub struct Parser { #[allow(dead_code)] pub(crate) version_is_at_least_1_0_0: bool, - pub version: Version, + language_version: Version, } #[derive(thiserror::Error, Debug)] @@ -42,30 +43,28 @@ pub enum ParserInitializationError { } impl Parser { - pub const SUPPORTED_VERSIONS: &'static [Version] = &[ - Version::new(1, 0, 0), - Version::new(1, 0, 1), - Version::new(1, 1, 0), - Version::new(1, 1, 1), - ]; - pub const ROOT_KIND: NonterminalKind = NonterminalKind::SourceUnit; - pub fn create(version: Version) -> std::result::Result { - if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { + pub fn create( + language_version: Version, + ) -> std::result::Result { + if LanguageFacts::SUPPORTED_VERSIONS + .binary_search(&language_version) + .is_ok() + { Ok(Self { - version_is_at_least_1_0_0: Version::new(1, 0, 0) <= version, - version, + version_is_at_least_1_0_0: Version::new(1, 0, 0) <= language_version, + language_version, }) } else { Err(ParserInitializationError::UnsupportedLanguageVersion( - version, + language_version, )) } } - pub fn version(&self) -> &Version { - &self.version + pub fn language_version(&self) -> &Version { + &self.language_version } /******************************************** * Parser Functions ********************************************/ diff --git a/crates/testlang/outputs/cargo/crate/src/generated/parser/parse_output.rs b/crates/testlang/outputs/cargo/crate/src/generated/parser/parse_output.rs index 9ec833b5a7..83a71d08f4 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/parser/parse_output.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/parser/parse_output.rs @@ -3,15 +3,15 @@ use crate::cst::{Cursor, Node, TextIndex}; use crate::parser::ParseError; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseOutput { - pub(crate) parse_tree: Node, + pub(crate) tree: Node, pub(crate) errors: Vec, } impl ParseOutput { - pub fn tree(&self) -> Node { - self.parse_tree.clone() + pub fn tree(&self) -> &Node { + &self.tree } pub fn errors(&self) -> &Vec { @@ -24,6 +24,6 @@ impl ParseOutput { /// Creates a cursor that starts at the root of the parse tree. pub fn create_tree_cursor(&self) -> Cursor { - self.parse_tree.clone().cursor_with_offset(TextIndex::ZERO) + self.tree.clone().cursor_with_offset(TextIndex::ZERO) } } diff --git a/crates/testlang/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs b/crates/testlang/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs index c9fc3e44e5..ac5d02b724 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/parser/parser_support/parser_function.rs @@ -94,7 +94,7 @@ where Node::nonterminal(no_match.kind.unwrap(), trivia_nodes) }; ParseOutput { - parse_tree: tree, + tree, errors: vec![ParseError::new( start..start + input.into(), no_match.expected_terminals, @@ -158,18 +158,17 @@ where )); ParseOutput { - parse_tree: Node::nonterminal(topmost_node.kind, new_children), + tree: Node::nonterminal(topmost_node.kind, new_children), errors, } } else { - let parse_tree = Node::Nonterminal(topmost_node); + let tree = Node::Nonterminal(topmost_node); let errors = stream.into_errors(); // Sanity check: Make sure that succesful parse is equivalent to not having any invalid nodes debug_assert_eq!( errors.is_empty(), - parse_tree - .clone() + tree.clone() .cursor_with_offset(TextIndex::ZERO) .remaining_nodes() .all(|edge| edge @@ -178,7 +177,7 @@ where .is_none()) ); - ParseOutput { parse_tree, errors } + ParseOutput { tree, errors } } } } diff --git a/crates/testlang/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs b/crates/testlang/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs new file mode 100644 index 0000000000..9dea6c75c1 --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/utils/generated/language_facts.rs @@ -0,0 +1,16 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use semver::Version; + +pub struct LanguageFacts; + +impl LanguageFacts { + pub const NAME: &'static str = "Testlang"; + + pub const SUPPORTED_VERSIONS: &'static [Version] = &[ + Version::new(1, 0, 0), + Version::new(1, 0, 1), + Version::new(1, 1, 0), + Version::new(1, 1, 1), + ]; +} diff --git a/crates/testlang/outputs/cargo/crate/src/generated/utils/mod.rs b/crates/testlang/outputs/cargo/crate/src/generated/utils/mod.rs new file mode 100644 index 0000000000..57bd4f108e --- /dev/null +++ b/crates/testlang/outputs/cargo/crate/src/generated/utils/mod.rs @@ -0,0 +1,6 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +#[path = "generated/language_facts.rs"] +mod language_facts; + +pub use language_facts::LanguageFacts; diff --git a/crates/testlang/outputs/cargo/crate/src/lib.rs b/crates/testlang/outputs/cargo/crate/src/lib.rs index 086cd6d549..bd40dc9c6c 100644 --- a/crates/testlang/outputs/cargo/crate/src/lib.rs +++ b/crates/testlang/outputs/cargo/crate/src/lib.rs @@ -1,3 +1,4 @@ +mod extensions; mod generated; pub use generated::*; diff --git a/crates/testlang/outputs/cargo/tests/src/versions/mod.rs b/crates/testlang/outputs/cargo/tests/src/versions/mod.rs index 079d19dfb4..b5eb2c1ec8 100644 --- a/crates/testlang/outputs/cargo/tests/src/versions/mod.rs +++ b/crates/testlang/outputs/cargo/tests/src/versions/mod.rs @@ -1,9 +1,9 @@ use semver::Version; -use slang_testlang::parser::Parser; +use slang_testlang::utils::LanguageFacts; #[test] fn list_supported_versions() { - let versions = Parser::SUPPORTED_VERSIONS; + let versions = LanguageFacts::SUPPORTED_VERSIONS; assert!(!versions.is_empty()); assert!(!versions.contains(&Version::new(0, 0, 0))); diff --git a/crates/testlang/outputs/cargo/wasm/Cargo.toml b/crates/testlang/outputs/cargo/wasm/Cargo.toml index cb5dc8e8e9..6a96dc2211 100644 --- a/crates/testlang/outputs/cargo/wasm/Cargo.toml +++ b/crates/testlang/outputs/cargo/wasm/Cargo.toml @@ -21,7 +21,10 @@ testlang_language = { workspace = true } paste = { workspace = true } semver = { workspace = true } serde_json = { workspace = true } -slang_testlang = { workspace = true } +slang_testlang = { workspace = true, features = [ + "__experimental_bindings_api", + "__private_compilation_api", +] } wit-bindgen = { workspace = true } [lints] diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/generated/bindings.rs b/crates/testlang/outputs/cargo/wasm/src/generated/generated/bindgen.rs similarity index 100% rename from crates/testlang/outputs/cargo/wasm/src/generated/generated/bindings.rs rename to crates/testlang/outputs/cargo/wasm/src/generated/generated/bindgen.rs diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/generated/config.json b/crates/testlang/outputs/cargo/wasm/src/generated/generated/config.json index 395999cccb..3444eb9169 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/generated/config.json +++ b/crates/testlang/outputs/cargo/wasm/src/generated/generated/config.json @@ -1,5 +1,65 @@ { "mappings": { + "nomic-foundation:slang:bindings:definition.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.name-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:definition.definiens-location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:reference.location()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:binding-location": { + "Variant": { + "as_direct_union_of_resource_classes": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.file-id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:bindings:user-file-location.cursor()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.language-version()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:compilation-unit.binding-graph()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.id()": { + "Function": { + "as_getter": true + } + }, + "nomic-foundation:slang:compilation:file.tree()": { + "Function": { + "as_getter": true + } + }, "nomic-foundation:slang:cst:terminal-kind": { "Enum": { "as_typescript_enum": true @@ -105,7 +165,7 @@ "as_iterator": true } }, - "nomic-foundation:slang:parser:parser.version()": { + "nomic-foundation:slang:parser:parser.language-version()": { "Function": { "as_getter": true } diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit new file mode 100644 index 0000000000..1721afc82d --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/bindings.wit @@ -0,0 +1,72 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface bindings { + use cst.{cursor}; + + /// A giant graph that contains name binding information for all source files within the compilation unit. + /// It stores cursors to all definitions and references, and can resolve the edges between them. + resource binding-graph { + /// If the provided cursor points at a definition `Identifier`, it will return the + /// corresponding definition. Otherwise, it will return `undefined`. + definition-at: func(cursor: borrow) -> option; + + /// If the provided cursor points at a reference `Identifier`, it will return the + /// corresponding reference. Otherwise, it will return `undefined`. + reference-at: func(cursor: borrow) -> option; + } + + /// Represents a definition in the binding graph. + resource definition { + /// Returns a unique numerical identifier of the definition. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the definition's name. + /// For `contract X {}`, that is the location of the `X` `Identifier` node. + name-location: func() -> binding-location; + + /// Returns the location of the definition's definiens. + /// For `contract X {}`, that is the location of the parent `ContractDefinition` node. + definiens-location: func() -> binding-location; + } + + /// Represents a reference in the binding graph. + resource reference { + /// Returns a unique numerical identifier of the reference. + /// It is only valid for the lifetime of the binding graph. + /// It can change between multiple graphs, even for the same source code input. + id: func() -> u32; + + /// Returns the location of the reference. + /// For `new X()`, that is the location of the `X` `Identifier` node. + location: func() -> binding-location; + + /// Returns a list of all definitions related to this reference. + /// Most references have a single definition, but some have multiple, such as when a symbol + /// is imported from another file, and renamed (re-defined) in the current file. + definitions: func() -> list; + } + + /// Represents a location of a symbol (definition or reference) in the binding graph. + /// It can either be in a user file, or a built-in in the language. + variant binding-location { + /// Represents a location of a user-defined symbol in a user file. + user-file(user-file-location), + /// Represents a location of a built-in symbol in the language. + built-in(built-in-location) + } + + /// Represents a location of a user-defined symbol in a user file. + resource user-file-location { + /// Returns the ID of the file that contains the symbol. + file-id: func() -> string; + + /// Returns a cursor to the CST node that contains the symbol. + cursor: func() -> cursor; + } + + /// Represents a location of a built-in symbol in the language. + resource built-in-location { + } +} diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit new file mode 100644 index 0000000000..c9db286d76 --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/compilation.wit @@ -0,0 +1,68 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface compilation { + use bindings.{binding-graph}; + use cst.{node, cursor}; + + /// A builder for creating compilation units. + /// Allows incrementally building a transitive list of all files and their imports. + /// + /// This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + /// This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + resource internal-compilation-builder { + /// Creates a new compilation builder for the specified language version. + create: static func(language-version: string) -> result; + + /// Adds a source file to the compilation unit. + add-file: func(id: string, contents: string) -> add-file-response; + + /// Resolves an import in the source file to the destination file. + resolve-import: func(source-file-id: string, import-path: borrow, destination-file-id: string) -> result<_, string>; + + /// Builds and returns the final compilation unit. + build: func() -> compilation-unit; + } + + /// Contains information about imports found in an added source file. + record add-file-response { + /// List of cursors to any import paths found in the file. + import-paths: list, + } + + /// A complete compilation unit is a complete view over all compilation inputs: + /// + /// - All source files, stored as CSTs. + /// - Name binding graph that exposes relationships between definitions and references in these files. + /// - Any relevant compilation options. + /// + /// It also exposes utilities to traverse the compilation unit and query it. + resource compilation-unit { + /// Returns the language version this compilation unit is configured for. + language-version: func() -> string; + + /// Returns a list of all files in the compilation unit. + files: func() -> list; + + /// Returns the file with the specified ID, if it exists. + file: func(id: string) -> option; + + /// Calculates name binding information for all source files within the compilation unit. + /// Returns a graph that contains all found definitions and their references. + /// + /// Note: building this graph is an expensive operation. + /// It is done lazily on the first access, and cached thereafter. + binding-graph: func() -> result; + } + + /// A single source file in the compilation unit. + resource file { + /// Returns the unique identifier of this file. + id: func() -> string; + + /// Returns the syntax tree of this file. + tree: func() -> node; + + /// Creates a cursor for traversing the syntax tree of this file. + create-tree-cursor: func() -> cursor; + } +} diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/parser.wit b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/parser.wit index bbe8faa7c8..df41c2e0fc 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/parser.wit +++ b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/parser.wit @@ -10,15 +10,11 @@ interface parser { /// This represents the starting point for parsing a complete source file. root-kind: static func() -> nonterminal-kind; - /// Returns a list of language versions supported by this parser. - /// Each version string represents a specific grammar configuration. - supported-versions: static func() -> list; - /// Creates a new parser instance for the specified language version. - create: static func(version: string) -> result; + create: static func(language-version: string) -> result; /// Returns the language version this parser instance is configured for. - version: func() -> string; + language-version: func() -> string; /// Parses the input string starting from the specified nonterminal kind. parse: func(kind: nonterminal-kind, input: string) -> parse-output; diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/utils.wit b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/utils.wit new file mode 100644 index 0000000000..9bb93e160f --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/utils.wit @@ -0,0 +1,9 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +interface utils { + /// Provides information about the supported language versions and the grammar. + resource language-facts { + /// Returns a list of language versions supported by Slang, sorted ascendingly. + supported-versions: static func() -> list; + } +} diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/world.wit b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/world.wit index c5e1378860..bb9b7b10be 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/world.wit +++ b/crates/testlang/outputs/cargo/wasm/src/generated/interface/generated/world.wit @@ -4,6 +4,9 @@ package nomic-foundation:slang; world slang { export ast; + export bindings; + export compilation; export cst; export parser; + export utils; } diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/mod.rs index c84d868d87..418be83d23 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/mod.rs @@ -1,10 +1,10 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. -#[path = "./generated/bindings.rs"] -mod bindings; +#[path = "./generated/bindgen.rs"] +mod bindgen; mod utils; mod wrappers; struct World; -crate::wasm_crate::bindings::export!(World with_types_in crate::wasm_crate::bindings); +crate::wasm_crate::bindgen::export!(World with_types_in crate::wasm_crate::bindgen); diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs index cf50ecacd2..2ccdcb4777 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/ast/mod.rs @@ -6,10 +6,10 @@ mod selectors; use crate::wasm_crate::utils::IntoFFI; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::ast::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::ast::{ Guest, GuestSelectors, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Node, NonterminalNodeBorrow, }; } 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 new file mode 100644 index 0000000000..6163cf964f --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs @@ -0,0 +1,176 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use crate::wasm_crate::utils::{define_rc_wrapper, define_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::{ + BindingGraph, BindingGraphBorrow, BindingLocation, BuiltInLocation, BuiltInLocationBorrow, + CursorBorrow, Definition, DefinitionBorrow, Guest, GuestBindingGraph, GuestBuiltInLocation, + GuestDefinition, GuestReference, GuestUserFileLocation, Reference, ReferenceBorrow, + UserFileLocation, UserFileLocationBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::Cursor; +} + +mod rust { + pub use crate::rust_crate::bindings::{ + BindingGraph, BindingLocation, BuiltInLocation, 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 { + type BindingGraph = BindingGraphWrapper; + + type Definition = DefinitionWrapper; + type Reference = ReferenceWrapper; + + type UserFileLocation = UserFileLocationWrapper; + type BuiltInLocation = BuiltInLocationWrapper; +} + +//================================================ +// +// resource binding-graph +// +//================================================ + +define_rc_wrapper! { BindingGraph { + fn definition_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .definition_at(&cursor._borrow_ffi()) + .map(rust::Definition::from) + .map(IntoFFI::_into_ffi) + } + + fn reference_at(&self, cursor: ffi::CursorBorrow<'_>) -> Option { + self._borrow_ffi() + .reference_at(&cursor._borrow_ffi()) + .map(rust::Reference::from) + .map(IntoFFI::_into_ffi) + } +} } + +//================================================ +// +// resource definition +// +//================================================ + +define_wrapper! { Definition { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn name_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().name_location.clone()._into_ffi() + } + + fn definiens_location(&self) -> ffi::BindingLocation { + self._borrow_ffi().definiens_location.clone()._into_ffi() + } +} } + +//================================================ +// +// resource reference +// +//================================================ + +define_wrapper! { Reference { + fn id(&self) -> u32 { + self._borrow_ffi().id.try_into().unwrap() + } + + fn location(&self) -> ffi::BindingLocation { + self._borrow_ffi().location.clone()._into_ffi() + } + + fn definitions(&self) -> Vec { + self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + } +} } + +//================================================ +// +// variant binding-location +// +//================================================ + +impl IntoFFI for rust::BindingLocation { + #[inline] + fn _into_ffi(self) -> ffi::BindingLocation { + match self { + Self::BuiltIn(location) => ffi::BindingLocation::BuiltIn(location._into_ffi()), + Self::UserFile(location) => ffi::BindingLocation::UserFile(location._into_ffi()), + } + } +} + +//================================================ +// +// resource user-file-location +// +//================================================ + +define_wrapper! { UserFileLocation { + fn file_id(&self) -> String { + self._borrow_ffi().file_id().to_owned() + } + + fn cursor(&self) -> ffi::Cursor { + self._borrow_ffi().cursor().to_owned()._into_ffi() + } +} } + +//================================================ +// +// resource built-in-location +// +//================================================ + +define_wrapper! { BuiltInLocation { +} } diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs new file mode 100644 index 0000000000..d168910020 --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/compilation/mod.rs @@ -0,0 +1,122 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::rc::Rc; + +use semver::Version; + +use crate::wasm_crate::utils::{define_rc_wrapper, define_refcell_wrapper, IntoFFI}; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::bindings::BindingGraph; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::compilation::{ + AddFileResponse, CompilationUnit, CompilationUnitBorrow, CursorBorrow, File, FileBorrow, + Guest, GuestCompilationUnit, GuestFile, GuestInternalCompilationBuilder, + InternalCompilationBuilder, InternalCompilationBuilderBorrow, + }; + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{Cursor, Node}; +} + +mod rust { + pub use crate::rust_crate::compilation::{ + AddFileResponse, CompilationUnit, File, InternalCompilationBuilder, + }; +} + +impl ffi::Guest for crate::wasm_crate::World { + type InternalCompilationBuilder = InternalCompilationBuilderWrapper; + type CompilationUnit = CompilationUnitWrapper; + type File = FileWrapper; +} + +//================================================ +// +// resource internal-compilation-builder +// +//================================================ + +define_refcell_wrapper! { InternalCompilationBuilder { + fn create(language_version: String) -> Result { + let language_version = Version::parse(&language_version).map_err(|e| e.to_string())?; + + rust::InternalCompilationBuilder::create(language_version) + .map(IntoFFI::_into_ffi) + .map_err(|e| e.to_string()) + } + + fn add_file(&self, id: String, contents: String) -> ffi::AddFileResponse { + self._borrow_mut_ffi() + .add_file(id, &contents) + ._into_ffi() + } + + fn resolve_import(&self, source_file_id: String, import_path: ffi::CursorBorrow<'_>, destination_file_id: String) -> Result<(), String> { + self._borrow_mut_ffi() + .resolve_import(&source_file_id, &import_path._borrow_ffi(), destination_file_id) + .map_err(|e| e.to_string()) + } + + fn build(&self) -> ffi::CompilationUnit { + Rc::new(self._borrow_ffi().build())._into_ffi() + } +} } + +//================================================ +// +// record add-file-response +// +//================================================ + +impl IntoFFI for rust::AddFileResponse { + #[inline] + fn _into_ffi(self) -> ffi::AddFileResponse { + let Self { import_paths } = self; + + ffi::AddFileResponse { + import_paths: import_paths.into_iter().map(IntoFFI::_into_ffi).collect(), + } + } +} + +//================================================ +// +// resource compilation-unit +// +//================================================ + +define_rc_wrapper! { CompilationUnit { + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() + } + + fn files(&self) -> Vec { + self._borrow_ffi().files().into_iter().map(IntoFFI::_into_ffi).collect() + } + + fn file(&self, id: String) -> Option { + self._borrow_ffi().file(&id).map(IntoFFI::_into_ffi) + } + + fn binding_graph(&self) -> Result { + self._borrow_ffi().binding_graph().as_ref().map(Rc::clone).map(IntoFFI::_into_ffi).map_err(|e| e.to_string()) + } +} } + +//================================================ +// +// resource file +// +//================================================ + +define_rc_wrapper! { File { + fn id(&self) -> String { + self._borrow_ffi().id().to_owned() + } + + fn tree(&self) -> ffi::Node { + self._borrow_ffi().tree().to_owned()._into_ffi() + } + + fn create_tree_cursor(&self) -> ffi::Cursor { + self._borrow_ffi().create_tree_cursor()._into_ffi() + } +} } diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs index 444cab2eab..deca6e2bb4 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/cst/mod.rs @@ -7,7 +7,7 @@ use crate::wasm_crate::utils::{ }; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ AncestorsIterator, AncestorsIteratorBorrow, Cursor, CursorBorrow, CursorIterator, CursorIteratorBorrow, Edge, EdgeLabel, Guest, GuestAncestorsIterator, GuestCursor, GuestCursorIterator, GuestNonterminalNode, GuestQuery, GuestQueryMatchIterator, diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/mod.rs index e05fee5635..6b289c658d 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/mod.rs @@ -1,5 +1,8 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. mod ast; +mod bindings; +mod compilation; mod cst; mod parser; +mod utils; diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs index 311c2b9d8a..7d000977d8 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/parser/mod.rs @@ -3,10 +3,10 @@ use crate::wasm_crate::utils::{define_wrapper, FromFFI, IntoFFI}; mod ffi { - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::cst::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::cst::{ Cursor, Node, TextRange, }; - pub use crate::wasm_crate::bindings::exports::nomic_foundation::slang::parser::{ + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::parser::{ Guest, GuestParseError, GuestParseOutput, GuestParser, NonterminalKind, ParseError, ParseErrorBorrow, ParseOutput, ParseOutputBorrow, Parser, ParserBorrow, }; @@ -33,22 +33,15 @@ define_wrapper! { Parser { rust::Parser::ROOT_KIND._into_ffi() } - fn supported_versions() -> Vec { - rust::Parser::SUPPORTED_VERSIONS - .iter() - .map(|v| v.to_string()) - .collect() - } - - fn create(version: String) -> Result { - semver::Version::parse(&version) - .map_err(|_| format!("Invalid semantic version: '{version}'")) + fn create(language_version: String) -> Result { + semver::Version::parse(&language_version) + .map_err(|_| format!("Invalid semantic version: '{language_version}'")) .and_then(|version| rust::Parser::create(version).map_err(|e| e.to_string())) .map(IntoFFI::_into_ffi) } - fn version(&self) -> String { - self._borrow_ffi().version.to_string() + fn language_version(&self) -> String { + self._borrow_ffi().language_version().to_string() } fn parse(&self, kind: ffi::NonterminalKind, input: String) -> ffi::ParseOutput { @@ -80,7 +73,7 @@ define_wrapper! { ParseError { define_wrapper! { ParseOutput { fn tree(&self) -> ffi::Node { - self._borrow_ffi().tree()._into_ffi() + self._borrow_ffi().tree().clone()._into_ffi() } fn errors(&self) -> Vec { diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs new file mode 100644 index 0000000000..b478ca4299 --- /dev/null +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/utils/mod.rs @@ -0,0 +1,32 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use crate::wasm_crate::utils::define_wrapper; + +mod ffi { + pub use crate::wasm_crate::bindgen::exports::nomic_foundation::slang::utils::{ + Guest, GuestLanguageFacts, LanguageFacts, LanguageFactsBorrow, + }; +} + +mod rust { + pub use crate::rust_crate::utils::LanguageFacts; +} + +impl ffi::Guest for crate::wasm_crate::World { + type LanguageFacts = LanguageFactsWrapper; +} + +//================================================ +// +// resource language-facts +// +//================================================ + +define_wrapper! { LanguageFacts { + fn supported_versions() -> Vec { + rust::LanguageFacts::SUPPORTED_VERSIONS + .iter() + .map(|v| v.to_string()) + .collect() + } +} } diff --git a/crates/testlang/outputs/npm/package/package.json b/crates/testlang/outputs/npm/package/package.json index d70fa9afff..b7d2c74da4 100644 --- a/crates/testlang/outputs/npm/package/package.json +++ b/crates/testlang/outputs/npm/package/package.json @@ -6,8 +6,11 @@ "exports": { ".": "./target/generated/index.mjs", "./ast": "./target/generated/ast/index.mjs", + "./bindings": "./target/generated/bindings/index.mjs", + "./compilation": "./target/generated/compilation/index.mjs", "./cst": "./target/generated/cst/index.mjs", - "./parser": "./target/generated/parser/index.mjs" + "./parser": "./target/generated/parser/index.mjs", + "./utils": "./target/generated/utils/index.mjs" }, "__dependencies_comment__": "__SLANG_NPM_PACKAGE_DEPENDENCIES__ (keep in sync)", "dependencies": { diff --git a/crates/testlang/outputs/npm/package/src/generated/bindings/index.mts b/crates/testlang/outputs/npm/package/src/generated/bindings/index.mts new file mode 100644 index 0000000000..7263f31bdc --- /dev/null +++ b/crates/testlang/outputs/npm/package/src/generated/bindings/index.mts @@ -0,0 +1,23 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; + +export const BindingGraph = generated.bindings.BindingGraph; +export type BindingGraph = generated.bindings.BindingGraph; + +export const Definition = generated.bindings.Definition; +export type Definition = generated.bindings.Definition; + +export const Reference = generated.bindings.Reference; +export type Reference = generated.bindings.Reference; + +export type BindingLocation = generated.bindings.BindingLocation; + +export const BindingLocationType = generated.bindings.BindingLocationType; +export type BindingLocationType = generated.bindings.BindingLocationType; + +export const UserFileLocation = generated.bindings.UserFileLocation; +export type UserFileLocation = generated.bindings.UserFileLocation; + +export const BuiltInLocation = generated.bindings.BuiltInLocation; +export type BuiltInLocation = generated.bindings.BuiltInLocation; diff --git a/crates/testlang/outputs/npm/package/src/generated/compilation/builder.mts b/crates/testlang/outputs/npm/package/src/generated/compilation/builder.mts new file mode 100644 index 0000000000..4649389f8b --- /dev/null +++ b/crates/testlang/outputs/npm/package/src/generated/compilation/builder.mts @@ -0,0 +1,114 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import { Cursor } from "../cst/index.mjs"; +import { CompilationUnit } from "./index.mjs"; + +import * as generated from "../../../wasm/index.mjs"; + +const InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; +type InternalCompilationBuilder = generated.compilation.InternalCompilationBuilder; + +/** + * User-provided options and callbacks necessary for the `CompilationBuilder` class to perform its job. + */ +export interface CompilationBuilderConfig { + /** + * The language version to parse files with. + */ + languageVersion: string; + + /** + * Callback used by this builder to load the contents of a file. + * + * The user is responsible for fetching the file from the filesystem. + * If the file is not found, the callback should return undefined. + * Any errors thrown by the callback will be propagated to the caller. + */ + readFile: (fileId: string) => Promise; + + /** + * Callback used by this builder to resolve an import path. + * For example, if a source file contains the following statement: + * + * ```solidity + * import {Foo} from "foo.sol"; + * ``` + * + * Then the API will invoke the callback with a cursor pointing to the `"foo.sol"` string literal. + * + * The user is responsible for resolving it to a file in the compilation, and return its ID. + * If the callback returns `undefined`, the import will stay unresolved. + * Any errors thrown by the callback will be propagated to the caller. + */ + resolveImport: (sourceFileId: string, importPath: Cursor) => Promise; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a list of all files and their imports. + */ +export class CompilationBuilder { + private readonly seenFiles: Set = new Set(); + + private constructor( + private readonly internalBuilder: InternalCompilationBuilder, + + /** + * The user-supplied configuration. + */ + public readonly config: CompilationBuilderConfig, + ) {} + + /** + * Creates a new compilation builder for the specified language version. + */ + public static create(config: CompilationBuilderConfig): CompilationBuilder { + const internalBuilder = InternalCompilationBuilder.create(config.languageVersion); + return new CompilationBuilder(internalBuilder, config); + } + + /** + * Adds a source file to the compilation unit. + * Typically, users only need to add the "root" file, which contains the main contract they are trying to analyze. + * Any files that are imported by the root file will be discovered and loaded automatically by the config callbacks. + * + * Adding multiple files (roots) is supported. For example, an IDE can choose to add all NPM dependencies, + * regardless of whether they are imported or not, to be able to query the definitions there. + * + * Adding a file that has already been added is a no-op. + */ + public async addFile(id: string): Promise { + if (this.seenFiles.has(id)) { + return; + } else { + this.seenFiles.add(id); + } + + const contents = await this.config.readFile(id); + if (contents === undefined) { + return; + } + + const { importPaths } = this.internalBuilder.addFile(id, contents); + + await Promise.all( + importPaths.map(async (importPath) => { + const destinationFileId = await this.config.resolveImport(id, importPath); + if (destinationFileId === undefined) { + return; + } + + this.internalBuilder.resolveImport(id, importPath, destinationFileId); + + await this.addFile(destinationFileId); + }), + ); + } + + /** + * Builds and returns the final compilation unit. + */ + public build(): CompilationUnit { + return this.internalBuilder.build(); + } +} diff --git a/crates/testlang/outputs/npm/package/src/generated/compilation/index.mts b/crates/testlang/outputs/npm/package/src/generated/compilation/index.mts new file mode 100644 index 0000000000..832fe073b8 --- /dev/null +++ b/crates/testlang/outputs/npm/package/src/generated/compilation/index.mts @@ -0,0 +1,16 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; +import * as builder from "./builder.mjs"; + +// This is a wrapper around 'generated.compilation.InternalCompilationBuilder': +export const CompilationBuilder = builder.CompilationBuilder; +export type CompilationBuilder = builder.CompilationBuilder; + +export type CompilationBuilderConfig = builder.CompilationBuilderConfig; + +export const CompilationUnit = generated.compilation.CompilationUnit; +export type CompilationUnit = generated.compilation.CompilationUnit; + +export const File = generated.compilation.File; +export type File = generated.compilation.File; diff --git a/crates/testlang/outputs/npm/package/src/generated/cst/index.mts b/crates/testlang/outputs/npm/package/src/generated/cst/index.mts index 9ceb7c6f40..2bb806862e 100644 --- a/crates/testlang/outputs/npm/package/src/generated/cst/index.mts +++ b/crates/testlang/outputs/npm/package/src/generated/cst/index.mts @@ -50,10 +50,9 @@ export type TextIndex = generated.cst.TextIndex; export type TextRange = generated.cst.TextRange; -/* - * Helpers: +/** + * Asserts that this node is a `NonterminalNode` with the provided kind and text. */ - export function assertIsNonterminalNode( node: unknown, kind?: NonterminalKind, @@ -72,6 +71,9 @@ export function assertIsNonterminalNode( } } +/** + * Asserts that this node is a `TerminalKind` with the provided kind and text. + */ export function assertIsTerminalNode(node: unknown, kind?: TerminalKind, text?: string): asserts node is TerminalNode { if (!(node instanceof TerminalNode)) { throw new Error("Node provided is not a TerminalNode."); diff --git a/crates/testlang/outputs/npm/package/src/generated/index.mts b/crates/testlang/outputs/npm/package/src/generated/index.mts index 847cd1d82b..9aa39203ba 100644 --- a/crates/testlang/outputs/npm/package/src/generated/index.mts +++ b/crates/testlang/outputs/npm/package/src/generated/index.mts @@ -1,5 +1,8 @@ // This file is generated automatically by infrastructure scripts. Please don't edit by hand. export * as ast from "./ast/index.mjs"; +export * as bindings from "./bindings/index.mjs"; +export * as compilation from "./compilation/index.mjs"; export * as cst from "./cst/index.mjs"; export * as parser from "./parser/index.mjs"; +export * as utils from "./utils/index.mjs"; diff --git a/crates/testlang/outputs/npm/package/src/generated/utils/index.mts b/crates/testlang/outputs/npm/package/src/generated/utils/index.mts new file mode 100644 index 0000000000..f1b7780fcb --- /dev/null +++ b/crates/testlang/outputs/npm/package/src/generated/utils/index.mts @@ -0,0 +1,6 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +import * as generated from "../../../wasm/index.mjs"; + +export const LanguageFacts = generated.utils.LanguageFacts; +export type LanguageFacts = generated.utils.LanguageFacts; diff --git a/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts new file mode 100644 index 0000000000..b2b0d4dc94 --- /dev/null +++ b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-bindings.d.ts @@ -0,0 +1,153 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangBindings { + export { BindingGraph }; + export { Definition }; + export { Reference }; + export { UserFileLocation }; + export { BuiltInLocation }; + export { BindingLocation }; + export { BindingLocationType }; +} +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Represents a location of a symbol (definition or reference) in the binding graph. + * It can either be in a user file, or a built-in in the language. + */ +export type BindingLocation = UserFileLocation | BuiltInLocation; +export enum BindingLocationType { + UserFileLocation = "UserFileLocation", + BuiltInLocation = "BuiltInLocation", +} + +/** + * A giant graph that contains name binding information for all source files within the compilation unit. + * It stores cursors to all definitions and references, and can resolve the edges between them. + */ +export class BindingGraph { + /** + * If the provided cursor points at a definition `Identifier`, it will return the + * corresponding definition. Otherwise, it will return `undefined`. + */ + definitionAt(cursor: Cursor): Definition | undefined; + /** + * If the provided cursor points at a reference `Identifier`, it will return the + * corresponding reference. Otherwise, it will return `undefined`. + */ + referenceAt(cursor: Cursor): Reference | undefined; +} + +/** + * Represents a location of a built-in symbol in the language. + */ +export class BuiltInLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.BuiltInLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): this; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): this is BuiltInLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): undefined; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): false; +} + +/** + * Represents a definition in the binding graph. + */ +export class Definition { + /** + * Returns a unique numerical identifier of the definition. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the definition's name. + * For `contract X {}`, that is the location of the `X` `Identifier` node. + */ + get nameLocation(): BindingLocation; + /** + * Returns the location of the definition's definiens. + * For `contract X {}`, that is the location of the parent `ContractDefinition` node. + */ + get definiensLocation(): BindingLocation; +} + +/** + * Represents a reference in the binding graph. + */ +export class Reference { + /** + * Returns a unique numerical identifier of the reference. + * It is only valid for the lifetime of the binding graph. + * It can change between multiple graphs, even for the same source code input. + */ + get id(): number; + /** + * Returns the location of the reference. + * For `new X()`, that is the location of the `X` `Identifier` node. + */ + get location(): BindingLocation; + /** + * Returns a list of all definitions related to this reference. + * Most references have a single definition, but some have multiple, such as when a symbol + * is imported from another file, and renamed (re-defined) in the current file. + */ + definitions(): Definition[]; +} + +/** + * Represents a location of a user-defined symbol in a user file. + */ +export class UserFileLocation { + /** + * The variant of `BindingLocationType` that corresponds to this class. + */ + readonly type = BindingLocationType.UserFileLocation; + + /** + * Coerce this variant to a `UserFileLocation`, or `undefined` if this is not the correct type. + */ + asUserFileLocation(): this; + + /** + * Return `true` if this object is an instance of `UserFileLocation`. + */ + isUserFileLocation(): this is UserFileLocation; + + /** + * Coerce this variant to a `BuiltInLocation`, or `undefined` if this is not the correct type. + */ + asBuiltInLocation(): undefined; + + /** + * Return `true` if this object is an instance of `BuiltInLocation`. + */ + isBuiltInLocation(): false; + + /** + * Returns the ID of the file that contains the symbol. + */ + get fileId(): string; + /** + * Returns a cursor to the CST node that contains the symbol. + */ + get cursor(): Cursor; +} diff --git a/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts new file mode 100644 index 0000000000..3c1a6831c1 --- /dev/null +++ b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-compilation.d.ts @@ -0,0 +1,98 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangCompilation { + export { InternalCompilationBuilder }; + export { CompilationUnit }; + export { File }; +} +import type { BindingGraph } from "./nomic-foundation-slang-bindings.js"; +export { BindingGraph }; +import type { Node } from "./nomic-foundation-slang-cst.js"; +export { Node }; +import type { Cursor } from "./nomic-foundation-slang-cst.js"; +export { Cursor }; +/** + * Contains information about imports found in an added source file. + */ +export interface AddFileResponse { + /** + * List of cursors to any import paths found in the file. + */ + importPaths: Cursor[]; +} + +/** + * A complete compilation unit is a complete view over all compilation inputs: + * + * - All source files, stored as CSTs. + * - Name binding graph that exposes relationships between definitions and references in these files. + * - Any relevant compilation options. + * + * It also exposes utilities to traverse the compilation unit and query it. + */ +export class CompilationUnit { + /** + * Returns the language version this compilation unit is configured for. + */ + get languageVersion(): string; + /** + * Returns a list of all files in the compilation unit. + */ + files(): File[]; + /** + * Returns the file with the specified ID, if it exists. + */ + file(id: string): File | undefined; + /** + * Calculates name binding information for all source files within the compilation unit. + * Returns a graph that contains all found definitions and their references. + * + * Note: building this graph is an expensive operation. + * It is done lazily on the first access, and cached thereafter. + */ + get bindingGraph(): BindingGraph; +} + +/** + * A single source file in the compilation unit. + */ +export class File { + /** + * Returns the unique identifier of this file. + */ + get id(): string; + /** + * Returns the syntax tree of this file. + */ + get tree(): Node; + /** + * Creates a cursor for traversing the syntax tree of this file. + */ + createTreeCursor(): Cursor; +} + +/** + * A builder for creating compilation units. + * Allows incrementally building a transitive list of all files and their imports. + * + * This is an internal API, and exposed via a public `CompilationBuilder` wrapper class written in TypeScript. + * This allows storing/invoking user supplied callbacks in TypeScript, rather than Rust, which has its limitations. + */ +export class InternalCompilationBuilder { + /** + * Creates a new compilation builder for the specified language version. + */ + static create(languageVersion: string): InternalCompilationBuilder; + /** + * Adds a source file to the compilation unit. + */ + addFile(id: string, contents: string): AddFileResponse; + /** + * Resolves an import in the source file to the destination file. + */ + resolveImport(sourceFileId: string, importPath: Cursor, destinationFileId: string): void; + /** + * Builds and returns the final compilation unit. + */ + build(): CompilationUnit; +} diff --git a/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts index bc99b8e132..6ac5bb7305 100644 --- a/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts +++ b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-parser.d.ts @@ -65,19 +65,14 @@ export class Parser { * This represents the starting point for parsing a complete source file. */ static rootKind(): NonterminalKind; - /** - * Returns a list of language versions supported by this parser. - * Each version string represents a specific grammar configuration. - */ - static supportedVersions(): string[]; /** * Creates a new parser instance for the specified language version. */ - static create(version: string): Parser; + static create(languageVersion: string): Parser; /** * Returns the language version this parser instance is configured for. */ - get version(): string; + get languageVersion(): string; /** * Parses the input string starting from the specified nonterminal kind. */ diff --git a/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts new file mode 100644 index 0000000000..0a3e19de1c --- /dev/null +++ b/crates/testlang/outputs/npm/package/wasm/generated/interfaces/nomic-foundation-slang-utils.d.ts @@ -0,0 +1,15 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +export namespace NomicFoundationSlangUtils { + export { LanguageFacts }; +} + +/** + * Provides information about the supported language versions and the grammar. + */ +export class LanguageFacts { + /** + * Returns a list of language versions supported by Slang, sorted ascendingly. + */ + static supportedVersions(): string[]; +} diff --git a/crates/testlang/outputs/npm/package/wasm/generated/testlang_cargo_wasm.component.d.ts b/crates/testlang/outputs/npm/package/wasm/generated/testlang_cargo_wasm.component.d.ts index c7614866dd..959f0ad174 100644 --- a/crates/testlang/outputs/npm/package/wasm/generated/testlang_cargo_wasm.component.d.ts +++ b/crates/testlang/outputs/npm/package/wasm/generated/testlang_cargo_wasm.component.d.ts @@ -2,7 +2,13 @@ import { NomicFoundationSlangCst } from "./interfaces/nomic-foundation-slang-cst.js"; import { NomicFoundationSlangAst } from "./interfaces/nomic-foundation-slang-ast.js"; +import { NomicFoundationSlangBindings } from "./interfaces/nomic-foundation-slang-bindings.js"; +import { NomicFoundationSlangCompilation } from "./interfaces/nomic-foundation-slang-compilation.js"; import { NomicFoundationSlangParser } from "./interfaces/nomic-foundation-slang-parser.js"; +import { NomicFoundationSlangUtils } from "./interfaces/nomic-foundation-slang-utils.js"; export * as cst from "./interfaces/nomic-foundation-slang-cst.js"; export * as ast from "./interfaces/nomic-foundation-slang-ast.js"; +export * as bindings from "./interfaces/nomic-foundation-slang-bindings.js"; +export * as compilation from "./interfaces/nomic-foundation-slang-compilation.js"; export * as parser from "./interfaces/nomic-foundation-slang-parser.js"; +export * as utils from "./interfaces/nomic-foundation-slang-utils.js"; diff --git a/crates/testlang/outputs/npm/tests/src/cst/cursor.test.mts b/crates/testlang/outputs/npm/tests/src/cst/cursor.test.mts index a719094355..1a66818131 100644 --- a/crates/testlang/outputs/npm/tests/src/cst/cursor.test.mts +++ b/crates/testlang/outputs/npm/tests/src/cst/cursor.test.mts @@ -101,9 +101,9 @@ test("use cursor goToNext()", () => { test("access the node using its name", () => { const source = "tree [A [B C] D];"; const parser = Parser.create("1.0.0"); - const parseTree = parser.parse(NonterminalKind.SourceUnit, source); + const parseOutput = parser.parse(NonterminalKind.SourceUnit, source); - const cursor = parseTree.createTreeCursor(); + const cursor = parseOutput.createTreeCursor(); let names: string[] = []; while (cursor.goToNextNonterminalWithKind(NonterminalKind.TreeNode)) { diff --git a/crates/testlang/outputs/npm/tests/src/language/language-facts.test.mts b/crates/testlang/outputs/npm/tests/src/language/language-facts.test.mts new file mode 100644 index 0000000000..0708b4deae --- /dev/null +++ b/crates/testlang/outputs/npm/tests/src/language/language-facts.test.mts @@ -0,0 +1,10 @@ +import { LanguageFacts } from "@slang-private/testlang-npm-package/utils"; + +test("list supported versions", () => { + const versions = LanguageFacts.supportedVersions(); + + expect(versions.length).toBeGreaterThan(0); + + expect(versions.includes("1.0.0")).toBeTruthy(); + expect(versions.includes("0.0.0")).toBeFalsy(); +}); diff --git a/crates/testlang/outputs/npm/tests/src/parser/parse.test.mts b/crates/testlang/outputs/npm/tests/src/parser/parse.test.mts index c5d824d417..4453018134 100644 --- a/crates/testlang/outputs/npm/tests/src/parser/parse.test.mts +++ b/crates/testlang/outputs/npm/tests/src/parser/parse.test.mts @@ -10,10 +10,10 @@ test("parse terminal", () => { const source = "About_time"; const parser = Parser.create("1.0.0"); - const parseTree = parser.parse(NonterminalKind.TreeNodeChild, source).tree; - assertIsNonterminalNode(parseTree, NonterminalKind.TreeNodeChild); + const tree = parser.parse(NonterminalKind.TreeNodeChild, source).tree; + assertIsNonterminalNode(tree, NonterminalKind.TreeNodeChild); - const children = parseTree.children(); + const children = tree.children(); expect(children).toHaveLength(1); assertIsTerminalNode(children[0]!.node, TerminalKind.DelimitedIdentifier, "About_time"); @@ -23,10 +23,10 @@ test("parse nonterminal", () => { const source = `tree [A [B C] D];`; const parser = Parser.create("1.0.0"); - const parseTree = parser.parse(NonterminalKind.SourceUnit, source).tree; - assertIsNonterminalNode(parseTree, NonterminalKind.SourceUnit); + const tree = parser.parse(NonterminalKind.SourceUnit, source).tree; + assertIsNonterminalNode(tree, NonterminalKind.SourceUnit); - const children = parseTree.children(); + const children = tree.children(); expect(children).toHaveLength(1); assertIsNonterminalNode(children[0]!.node, NonterminalKind.SourceUnitMembers); diff --git a/crates/testlang/outputs/npm/tests/src/parser/versions.test.mts b/crates/testlang/outputs/npm/tests/src/parser/versions.test.mts index b07dc37e07..4f239c9ef3 100644 --- a/crates/testlang/outputs/npm/tests/src/parser/versions.test.mts +++ b/crates/testlang/outputs/npm/tests/src/parser/versions.test.mts @@ -1,14 +1,5 @@ import { Parser } from "@slang-private/testlang-npm-package/parser"; -test("list supported versions", () => { - const versions = Parser.supportedVersions(); - - expect(versions.length).toBeGreaterThan(0); - - expect(versions.includes("1.0.0")).toBeTruthy(); - expect(versions.includes("0.0.0")).toBeFalsy(); -}); - test("invalid semantic version", () => { expect(() => Parser.create("foo_bar")).toThrow("Invalid semantic version: 'foo_bar'"); }); diff --git a/crates/testlang/outputs/npm/tests/src/utils/language-facts.test.mts b/crates/testlang/outputs/npm/tests/src/utils/language-facts.test.mts new file mode 100644 index 0000000000..0708b4deae --- /dev/null +++ b/crates/testlang/outputs/npm/tests/src/utils/language-facts.test.mts @@ -0,0 +1,10 @@ +import { LanguageFacts } from "@slang-private/testlang-npm-package/utils"; + +test("list supported versions", () => { + const versions = LanguageFacts.supportedVersions(); + + expect(versions.length).toBeGreaterThan(0); + + expect(versions.includes("1.0.0")).toBeTruthy(); + expect(versions.includes("0.0.0")).toBeFalsy(); +}); diff --git a/documentation/public/user-guide/concepts.md b/documentation/public/user-guide/concepts.md index 121f6fc3a9..41124c852a 100644 --- a/documentation/public/user-guide/concepts.md +++ b/documentation/public/user-guide/concepts.md @@ -10,7 +10,7 @@ The earliest Solidity version we support is `0.4.11`, and we plan on supporting From a `Parser` object, you can analyze any source text according to the nonterminals of that specific version. Providing an accurate language version is important, as it affects the shape of the syntax tree, and possible errors produced. -You can use the `Parser::getSupportedVersions()` API to get a list of all supported versions for the current Slang release. +You can use the `LanguageFacts::supportedVersions()` API to get a list of all supported versions for the current Slang release. The `Parser::parse()` API is the main entry point for the parser, and to generate concrete syntax trees (CSTs) that can be used for further analysis. Each `parse()` operation accepts the input source code, and a `NonterminalKind` variant.