From f6a2bf9a35bd326af118f31a91349437056351c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= Date: Wed, 10 Jul 2024 12:29:10 -0400 Subject: [PATCH] First implementation of the Bindings API (#1011) This PR adds the beginnings of a Bindings API using `stack-graphs` for the resolution: - add a new crate `metaslang_bindings` that contains: - a builder to construct a stack graph from a CST given the binding rules for the language, based on the tree-sitter-stack-graph crate - a `Bindings` struct where the Bindings API will be implemented and exposes methods to add process and add parsed source files into the underlying stack graph, query definitions and references found in the stack graph, and jump from a reference to a definition - adds a command to the `slang_solidity` CLI to test the new API - adds an incomplete `rules.msgb` for Solidity - removes the `build-graph` command from `slang_solidity` - adds a new `bindings` command to list the definitions and references found in a source file All the new API and functionality is still hidden under the `__experimental_bindings_api` feature flag. With this PR, given the sample file `lexical.sol`: ```solidity contract Foo { uint y; function bar(uint z) returns (uint) { uint x = 10; return x + y + z; } } ``` Running `cargo run --all-features --bin slang_solidity -- bindings -v 0.8.22 lexical.sol` will produce the output: ``` All definitions found: `Foo` defined at 9..12 in lexical.sol `y` defined at 22..23 in lexical.sol `bar` defined at 39..42 in lexical.sol `z` defined at 48..49 in lexical.sol `x` defined at 77..78 in lexical.sol All references found: `x` referenced at 96..97 in lexical.sol -> `x` defined at 77..78 in lexical.sol `y` referenced at 100..101 in lexical.sol -> `y` defined at 22..23 in lexical.sol `z` referenced at 104..105 in lexical.sol -> `z` defined at 48..49 in lexical.sol ``` --------- Co-authored-by: Omar Tawfik <15987992+OmarTawfik@users.noreply.github.com> --- Cargo.lock | 248 ++++- Cargo.toml | 2 + crates/codegen/runtime/cargo/Cargo.toml | 4 +- .../runtime/cargo/src/runtime/bindings/mod.rs | 12 +- .../src/runtime/cli/commands/bindings.rs | 86 ++ .../src/runtime/cli/commands/build_graph.rs | 57 -- .../cargo/src/runtime/cli/commands/mod.rs | 6 +- .../runtime/cargo/src/runtime/cli/mod.rs | 26 +- .../cli/src/commands/publish/cargo/mod.rs | 1 + crates/metaslang/bindings/Cargo.toml | 34 + crates/metaslang/bindings/LICENSE | 22 + crates/metaslang/bindings/README.md | 22 + .../bindings/src/builder/cancellation.rs | 123 +++ crates/metaslang/bindings/src/builder/mod.rs | 884 ++++++++++++++++++ crates/metaslang/bindings/src/lib.rs | 140 +++ .../inputs/language/bindings/rules.msgb | 364 +++++++- .../outputs/cargo/slang_solidity/Cargo.toml | 4 +- .../bindings/generated/binding_rules.rs | 364 +++++++- .../src/generated/bindings/mod.rs | 12 +- .../src/generated/cli/commands/bindings.rs | 88 ++ .../src/generated/cli/commands/build_graph.rs | 59 -- .../src/generated/cli/commands/mod.rs | 6 +- .../slang_solidity/src/generated/cli/mod.rs | 26 +- .../outputs/cargo/slang_solidity/src/main.rs | 2 +- .../solidity/outputs/cargo/tests/Cargo.toml | 2 +- .../outputs/cargo/slang_testlang/Cargo.toml | 4 +- .../src/generated/bindings/mod.rs | 12 +- .../src/generated/cli/commands/bindings.rs | 88 ++ .../src/generated/cli/commands/build_graph.rs | 59 -- .../src/generated/cli/commands/mod.rs | 6 +- .../slang_testlang/src/generated/cli/mod.rs | 26 +- .../testlang/outputs/cargo/tests/Cargo.toml | 1 + .../outputs/cargo/tests/src/graph/mod.rs | 4 +- 33 files changed, 2492 insertions(+), 302 deletions(-) create mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/commands/bindings.rs delete mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs create mode 100644 crates/metaslang/bindings/Cargo.toml create mode 100644 crates/metaslang/bindings/LICENSE create mode 100644 crates/metaslang/bindings/README.md create mode 100644 crates/metaslang/bindings/src/builder/cancellation.rs create mode 100644 crates/metaslang/bindings/src/builder/mod.rs create mode 100644 crates/metaslang/bindings/src/lib.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/bindings.rs delete mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs create mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/bindings.rs delete mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs diff --git a/Cargo.lock b/Cargo.lock index e89432c435..8b65c1c98d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -211,6 +223,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.5.0" @@ -310,7 +328,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -337,14 +355,14 @@ dependencies = [ "codegen_language_internal_macros", "indexmap", "infra_utils", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "semver", "serde", "strum", "strum_macros", - "syn", + "syn 2.0.68", "thiserror", ] @@ -352,10 +370,10 @@ dependencies = [ name = "codegen_language_internal_macros" version = "0.15.1" dependencies = [ - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -385,8 +403,8 @@ dependencies = [ "ariadne", "clap", "codegen_runtime_generator", + "metaslang_bindings", "metaslang_cst", - "metaslang_graph_builder", "napi", "napi-derive", "semver", @@ -448,7 +466,7 @@ dependencies = [ "codegen_ebnf", "codegen_language_definition", "infra_utils", - "itertools", + "itertools 0.13.0", "serde", ] @@ -481,6 +499,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "controlled-option" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95abc95db25411571f40a8b0af30a3c386f3927fe6f1460c70e1f49f01bac3ac" +dependencies = [ + "controlled-option-macros", +] + +[[package]] +name = "controlled-option-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305024255a6c456333e130da2559b7aedd5c2e15388f51dae69f7507517cfbfb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -565,7 +603,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f34ba9a9bcb8645379e9de8cb3ecfcf4d1c85ba66d90deb3259206fa5aa193b" dependencies = [ "quote", - "syn", + "syn 2.0.68", +] + +[[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.68", ] [[package]] @@ -576,7 +648,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -616,6 +688,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumset" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "env_filter" version = "0.1.0" @@ -691,6 +784,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.28" @@ -741,6 +840,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -983,6 +1091,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -1048,7 +1162,7 @@ dependencies = [ "clap", "clap_complete", "infra_utils", - "itertools", + "itertools 0.13.0", "markdown", "regex", "semver", @@ -1070,7 +1184,7 @@ dependencies = [ "cargo-emit", "console", "ignore", - "itertools", + "itertools 0.13.0", "num-format", "rayon", "regex", @@ -1097,6 +1211,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1161,6 +1284,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lsp-positions" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa61ce94f83d24eba829bbba054b40e9996d8531c87670799fd0c43dea97be37" +dependencies = [ + "memchr", + "unicode-segmentation", +] + [[package]] name = "markdown" version = "0.3.0" @@ -1187,6 +1320,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metaslang_bindings" +version = "0.15.1" +dependencies = [ + "metaslang_cst", + "metaslang_graph_builder", + "once_cell", + "semver", + "stack-graphs", + "thiserror", +] + [[package]] name = "metaslang_cst" version = "0.15.1" @@ -1278,7 +1423,7 @@ dependencies = [ "napi-derive-backend", "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -1293,7 +1438,7 @@ dependencies = [ "quote", "regex", "semver", - "syn", + "syn 2.0.68", ] [[package]] @@ -1396,7 +1541,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -1462,7 +1607,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -1531,7 +1676,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -1588,6 +1733,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1876,7 +2027,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -1967,8 +2118,8 @@ dependencies = [ "clap", "codegen_runtime_generator", "infra_utils", + "metaslang_bindings", "metaslang_cst", - "metaslang_graph_builder", "semver", "serde", "serde_json", @@ -2001,8 +2152,8 @@ dependencies = [ "anyhow", "codegen_runtime_generator", "infra_utils", + "metaslang_bindings", "metaslang_cst", - "metaslang_graph_builder", "semver", "serde", "strum", @@ -2107,7 +2258,7 @@ dependencies = [ "console", "indicatif", "infra_utils", - "itertools", + "itertools 0.13.0", "once_cell", "rayon", "semver", @@ -2133,7 +2284,7 @@ dependencies = [ "console", "indicatif", "infra_utils", - "itertools", + "itertools 0.13.0", "rayon", "reqwest", "semver", @@ -2150,6 +2301,24 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stack-graphs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af63a628b6666b3ce870cac60623d71d1575a9ccc5b2f0a2f83143c3e7da9a3a" +dependencies = [ + "bitvec", + "controlled-option", + "either", + "enumset", + "fxhash", + "itertools 0.10.5", + "libc", + "lsp-positions", + "smallvec", + "thiserror", +] + [[package]] name = "string-interner" version = "0.17.0" @@ -2183,7 +2352,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.68", ] [[package]] @@ -2192,6 +2361,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.68" @@ -2230,6 +2410,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.10.1" @@ -2287,6 +2473,7 @@ dependencies = [ name = "testlang_cargo_tests" version = "0.15.1" dependencies = [ + "metaslang_graph_builder", "semver", "slang_testlang", ] @@ -2327,7 +2514,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] @@ -2706,7 +2893,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -2740,7 +2927,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2952,6 +3139,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "0.5.1" @@ -2975,7 +3171,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.68", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3e828c8c16..a54828f6d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/infra/cli", "crates/infra/utils", + "crates/metaslang/bindings", "crates/metaslang/cst", "crates/metaslang/graph_builder", @@ -61,6 +62,7 @@ codegen_testing = { path = "crates/codegen/testing", version = "0.15.1" } infra_cli = { path = "crates/infra/cli", version = "0.15.1" } infra_utils = { path = "crates/infra/utils", version = "0.15.1" } +metaslang_bindings = { path = "crates/metaslang/bindings", version = "0.15.1" } metaslang_graph_builder = { path = "crates/metaslang/graph_builder", version = "0.15.1" } metaslang_cst = { path = "crates/metaslang/cst", version = "0.15.1" } diff --git a/crates/codegen/runtime/cargo/Cargo.toml b/crates/codegen/runtime/cargo/Cargo.toml index 4a48e196da..3ede85ec15 100644 --- a/crates/codegen/runtime/cargo/Cargo.toml +++ b/crates/codegen/runtime/cargo/Cargo.toml @@ -14,8 +14,8 @@ codegen_runtime_generator = { workspace = true } [dependencies] ariadne = { workspace = true, optional = true } clap = { workspace = true, optional = true } +metaslang_bindings = { workspace = true, optional = true } metaslang_cst = { workspace = true } -metaslang_graph_builder = { workspace = true, optional = true } napi = { workspace = true, optional = true } napi-derive = { workspace = true, optional = true } semver = { workspace = true } @@ -34,7 +34,7 @@ cli = ["dep:clap", "dep:serde_json", "__private_ariadne"] # Only used by the `slang_solidity` CLI __private_ariadne = ["dep:ariadne"] # For internal development only -__experimental_bindings_api = ["dep:metaslang_graph_builder"] +__experimental_bindings_api = ["dep:metaslang_bindings"] [lints] workspace = true diff --git a/crates/codegen/runtime/cargo/src/runtime/bindings/mod.rs b/crates/codegen/runtime/cargo/src/runtime/bindings/mod.rs index c3976caf8f..e45b819a3a 100644 --- a/crates/codegen/runtime/cargo/src/runtime/bindings/mod.rs +++ b/crates/codegen/runtime/cargo/src/runtime/bindings/mod.rs @@ -1,10 +1,14 @@ #[path = "generated/binding_rules.rs"] mod binding_rules; -use metaslang_graph_builder::ast; -pub use metaslang_graph_builder::functions::Functions; -pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; +use metaslang_bindings; +use semver::Version; use crate::cst::KindTypes; -pub type File = ast::File; +pub type Bindings = metaslang_bindings::Bindings; +pub type Handle<'a> = metaslang_bindings::Handle<'a, KindTypes>; + +pub fn create(version: Version) -> Bindings { + Bindings::create(version, binding_rules::BINDING_RULES_SOURCE) +} diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/bindings.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/bindings.rs new file mode 100644 index 0000000000..3b842d14ab --- /dev/null +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/bindings.rs @@ -0,0 +1,86 @@ +use core::fmt; + +use semver::Version; + +use super::CommandError; +use crate::bindings::{self, Bindings, Handle}; +use crate::cursor::Cursor; + +pub fn execute(file_path_string: &str, version: Version) -> Result<(), CommandError> { + let mut bindings = bindings::create(version.clone()); + let parse_output = super::parse::parse_source_file(file_path_string, version, |_| ())?; + let tree_cursor = parse_output.create_tree_cursor(); + + bindings.add_file(file_path_string, tree_cursor); + + print_definitions(&bindings); + print_references(&bindings); + + Ok(()) +} + +fn print_definitions(bindings: &Bindings) { + println!("\nAll definitions found:"); + for definition in bindings.all_definitions() { + println!("{}", DisplayDefinition(&definition)); + } +} + +fn print_references(bindings: &Bindings) { + println!("\nAll references found:"); + for reference in bindings.all_references() { + println!("{}", DisplayReference(&reference)); + if let Some(def) = reference.jump_to_definition() { + println!(" -> {}", DisplayDefinition(&def)); + } else { + println!(" -> No definition found"); + } + } +} + +struct DisplayRange<'a>(&'a Cursor); + +impl<'a> fmt::Display for DisplayRange<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let range = self.0.text_range(); + write!(f, "{}..{}", range.start, range.end) + } +} + +struct DisplayDefinition<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayDefinition<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` defined at {location} in {file}") + } else { + write!( + f, + "Definition without available cursor: {definition:?} in {file}", + definition = self.0 + ) + } + } +} + +struct DisplayReference<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayReference<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` referenced at {location} in {file}") + } else { + write!( + f, + "Reference without available cursor: {reference:?} in {file}", + reference = self.0 + ) + } + } +} diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs deleted file mode 100644 index 37970d5d30..0000000000 --- a/crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::fs; -use std::path::PathBuf; - -use semver::Version; - -use super::parse::parse_source_file; -use super::CommandError; -use crate::bindings::{ - ExecutionConfig, File as GraphBuilderFile, Functions, NoCancellation, Variables, -}; - -pub fn execute( - file_path_string: &str, - version: Version, - msgb_path_string: &str, - output_json: bool, - debug: bool, -) -> Result<(), CommandError> { - let parse_output = parse_source_file(file_path_string, version, |_| ())?; - let msgb = parse_graph_builder(msgb_path_string)?; - - let functions = Functions::stdlib(); - let variables = Variables::new(); - let mut execution_config = ExecutionConfig::new(&functions, &variables); - if debug { - execution_config = execution_config.debug_attributes( - "_location".into(), - "_variable".into(), - "_match".into(), - ); - } - - let tree = parse_output.create_tree_cursor(); - let graph = msgb.execute(&tree, &execution_config, &NoCancellation)?; - - if output_json { - graph.display_json(None)?; - } else { - print!("{}", graph.pretty_print()); - } - - Ok(()) -} - -fn parse_graph_builder(msgb_path_string: &str) -> Result { - let msgb_path = PathBuf::from(&msgb_path_string) - .canonicalize() - .map_err(|_| CommandError::FileNotFound(msgb_path_string.to_string()))?; - - let msgb_source = fs::read_to_string(&msgb_path)?; - GraphBuilderFile::from_str(&msgb_source).map_err(|parser_error| { - let error_message = parser_error - .display_pretty(&msgb_path, &msgb_source) - .to_string(); - CommandError::ParseFailed(error_message) - }) -} diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs index 92ec8e7c87..83168715ac 100644 --- a/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[cfg(feature = "__experimental_bindings_api")] -pub mod build_graph; +pub mod bindings; pub mod parse; #[derive(Debug, Error)] @@ -17,8 +17,4 @@ pub enum CommandError { #[error("Parsing failed: {0}")] ParseFailed(String), - - #[cfg(feature = "__experimental_bindings_api")] - #[error(transparent)] - ExecutionFailed(#[from] crate::bindings::ExecutionError), } diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs b/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs index 6243b22f86..370ff6a9cb 100644 --- a/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs +++ b/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs @@ -21,27 +21,15 @@ pub enum Commands { json: bool, }, - // This is only intended for internal development + /// This is only intended for internal development #[cfg(feature = "__experimental_bindings_api")] - /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) - BuildGraph { + Bindings { /// File path to the source file to parse file_path: String, /// The language version to use for parsing #[arg(short, long)] version: Version, - - /// The graph buider (.msgb) file to use - msgb_path: String, - - /// Print the graph as JSON - #[clap(long)] - json: bool, - - /// Include debug info (location, variable and match) in the built graph - #[clap(long)] - debug: bool, }, } @@ -54,13 +42,9 @@ impl Commands { json, } => commands::parse::execute(&file_path, version, json), #[cfg(feature = "__experimental_bindings_api")] - Commands::BuildGraph { - file_path, - version, - msgb_path, - json, - debug, - } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + Commands::Bindings { file_path, version } => { + commands::bindings::execute(&file_path, version) + } }; match command_result { Ok(()) => ExitCode::SUCCESS, diff --git a/crates/infra/cli/src/commands/publish/cargo/mod.rs b/crates/infra/cli/src/commands/publish/cargo/mod.rs index 803225f186..f67cbad9b6 100644 --- a/crates/infra/cli/src/commands/publish/cargo/mod.rs +++ b/crates/infra/cli/src/commands/publish/cargo/mod.rs @@ -15,6 +15,7 @@ const USER_FACING_CRATES: &[&str] = &[ // Sorted by dependency order (from dependencies to dependents): "metaslang_cst", "metaslang_graph_builder", + "metaslang_bindings", "slang_solidity", ]; diff --git a/crates/metaslang/bindings/Cargo.toml b/crates/metaslang/bindings/Cargo.toml new file mode 100644 index 0000000000..a97e0d04bd --- /dev/null +++ b/crates/metaslang/bindings/Cargo.toml @@ -0,0 +1,34 @@ +[package] +version.workspace = true +rust-version.workspace = true +edition.workspace = true +publish = true + +name = "metaslang_bindings" +description = "Computes semantic language bindings from parsed source code" +homepage = "https://nomicfoundation.github.io/slang/" +repository = "https://github.com/NomicFoundation/slang/" +authors = [ + "Nomic Foundation ", + "Antony Blakey ", + "Igor Matuszewski ", + "Omar Tawfik ", +] + +readme = "README.md" +license = "MIT" +keywords = ["parser"] +categories = ["compilers", "parsing", "parser-implementations"] + +[dependencies] +metaslang_cst = { workspace = true } +metaslang_graph_builder = { workspace = true } +once_cell = { workspace = true } +semver = { workspace = true } +stack-graphs = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] + +[lints] +workspace = true diff --git a/crates/metaslang/bindings/LICENSE b/crates/metaslang/bindings/LICENSE new file mode 100644 index 0000000000..8ff8c8bb7c --- /dev/null +++ b/crates/metaslang/bindings/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 stack-graphs authors +Copyright (c) 2024 Nomic Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/metaslang/bindings/README.md b/crates/metaslang/bindings/README.md new file mode 100644 index 0000000000..30cf663c4b --- /dev/null +++ b/crates/metaslang/bindings/README.md @@ -0,0 +1,22 @@ + + +# metaslang_bindings + +The `metaslang_bindings` library allows computing language semantic bindings +by building stack graphs (see crate `stack-graphs`) from graphs constructed +using the `metaslang_graph_builder` library from source code parsed by parsers +generated by `metaslang`. + +# metaslang_bindings + + + +[![release](https://img.shields.io/github/v/tag/NomicFoundation/slang?label=GitHub%20Release&logo=github&sort=semver&logoColor=white)](https://github.com/NomicFoundation/slang/releases) +[![crate](https://img.shields.io/crates/v/metaslang_bindings?label=Rust%20Crate&logo=rust&logoColor=white)](https://crates.io/crates/metaslang_bindings) + +## Solidity compiler tooling by [@NomicFoundation](https://github.com/NomicFoundation) + +
+ +> ❗ This project is still in alpha, and is under active development. +> If you are planning on using it, please reach out to us on [Telegram](https://t.me/+pxApdT-Ssn5hMTFh) so we can help you get started. diff --git a/crates/metaslang/bindings/src/builder/cancellation.rs b/crates/metaslang/bindings/src/builder/cancellation.rs new file mode 100644 index 0000000000..7cb6c9140a --- /dev/null +++ b/crates/metaslang/bindings/src/builder/cancellation.rs @@ -0,0 +1,123 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2021, stack-graphs authors. +// Copyright © 2024, slang authors. +// Licensed under MIT license +// Please see the LICENSE file in the root of this crate for license details. +// ------------------------------------------------------------------------------------------------ + +use std::ops::BitOr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use metaslang_graph_builder::{ + CancellationError as GraphCancellationError, CancellationFlag as GraphCancellationFlag, +}; +use thiserror::Error; + +/// Trait to signal that the execution is cancelled +pub trait CancellationFlag: Sync { + fn check(&self, at: &'static str) -> Result<(), CancellationError>; +} + +#[derive(Clone, Debug, Error)] +#[error("Cancelled at \"{0}\"")] +pub struct CancellationError(pub &'static str); + +impl stack_graphs::CancellationFlag for &dyn CancellationFlag { + fn check(&self, at: &'static str) -> Result<(), stack_graphs::CancellationError> { + CancellationFlag::check(*self, at).map_err(|err| stack_graphs::CancellationError(err.0)) + } +} + +impl GraphCancellationFlag for &dyn CancellationFlag { + fn check(&self, at: &'static str) -> Result<(), GraphCancellationError> { + CancellationFlag::check(*self, at).map_err(|err| GraphCancellationError(err.0)) + } +} + +impl<'a> BitOr for &'a dyn CancellationFlag { + type Output = OrCancellationFlag<'a>; + fn bitor(self, rhs: Self) -> Self::Output { + OrCancellationFlag(self, rhs) + } +} + +pub struct OrCancellationFlag<'a>(&'a dyn CancellationFlag, &'a dyn CancellationFlag); + +impl CancellationFlag for OrCancellationFlag<'_> { + fn check(&self, at: &'static str) -> Result<(), CancellationError> { + self.0.check(at)?; + self.1.check(at)?; + Ok(()) + } +} + +pub struct NoCancellation; + +impl CancellationFlag for NoCancellation { + fn check(&self, _at: &'static str) -> Result<(), CancellationError> { + Ok(()) + } +} + +pub struct CancelAfterDuration { + start: Instant, + limit: Duration, +} + +impl CancelAfterDuration { + #[allow(dead_code)] + pub fn new(limit: Duration) -> Self { + Self { + start: Instant::now(), + limit, + } + } + + #[allow(dead_code)] + pub fn from_option(limit: Option) -> Box { + match limit { + Some(limit) => Box::new(Self::new(limit)), + None => Box::new(NoCancellation), + } + } +} + +impl CancellationFlag for CancelAfterDuration { + fn check(&self, at: &'static str) -> Result<(), CancellationError> { + if self.start.elapsed().ge(&self.limit) { + return Err(CancellationError(at)); + } + Ok(()) + } +} + +#[derive(Clone)] +pub struct AtomicCancellationFlag { + flag: Arc, +} + +impl AtomicCancellationFlag { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + flag: Arc::new(AtomicBool::new(false)), + } + } + + #[allow(dead_code)] + pub fn cancel(&self) { + self.flag.store(true, Ordering::Relaxed); + } +} + +impl CancellationFlag for AtomicCancellationFlag { + fn check(&self, at: &'static str) -> Result<(), CancellationError> { + if self.flag.load(Ordering::Relaxed) { + return Err(CancellationError(at)); + } + Ok(()) + } +} diff --git a/crates/metaslang/bindings/src/builder/mod.rs b/crates/metaslang/bindings/src/builder/mod.rs new file mode 100644 index 0000000000..e8017f74d0 --- /dev/null +++ b/crates/metaslang/bindings/src/builder/mod.rs @@ -0,0 +1,884 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2021, stack-graphs authors. +// Copyright © 2024, slang authors. +// Licensed under MIT license +// Please see the LICENSE file in the root of this crate for license details. +// ------------------------------------------------------------------------------------------------ + +//! This module lets you construct [stack graphs][] using this crate's [graph construction DSL][]. +//! The graph DSL lets you construct arbitrary graph structures from the parsed syntax tree of a +//! source file. If you construct a graph using the vocabulary of attributes described below, then +//! the result of executing the graph DSL will be a valid stack graph, which we can then use for +//! name binding lookups. +//! +//! ## Prerequisites +//! +//! [stack graphs]: https://docs.rs/stack-graphs/*/ +//! [graph construction DSL]: https://docs.rs/metaslang_graph_builder/*/ +//! +//! To process a particular source language, you'll need to first get the CST from the source, using +//! the parser constructed from the language definition that uses the `metaslang_cst` crate. +//! +//! You will then need to create _stack graph construction rules_ for your language. These rules +//! are implemented using metaslang's [graph construction DSL][], which is based from tree-sitter's +//! graph construction DSL. They define the particular stack graph nodes and edges that should be +//! created for each part of the parsed syntax tree of a source file. +//! +//! ## Graph DSL vocabulary +//! +//! **Please note**: This documentation assumes you are already familiar with stack graphs, and how +//! to use different stack graph node types, and the connectivity between nodes, to implement the +//! name binding semantics of your language. We assume that you know what kind of stack graph you +//! want to produce; this documentation focuses only on the mechanics of _how_ to create that stack +//! graph content. +//! +//! As mentioned above, your stack graph construction rules should create stack graph nodes and +//! edges from the parsed content of a source file. You will use MSGB [stanzas][] to match on +//! different parts of the parsed syntax tree, and create stack graph content for each match. +//! +//! ### Creating stack graph nodes +//! +//! To create a stack graph node for each identifier in a Solidity file, you could use the following +//! MSGB stanza: +//! +//! ``` skip +//! [Identifier] { +//! node new_node +//! } +//! ``` +//! +//! (Here, `node` is a MSGB statement that creates a new node, and `new_node` is the name of a local +//! variable that the new node is assigned to, letting you refer to the new node in the rest of the +//! stanza.) +//! +//! [stanzas]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/index.html#high-level-structure +//! +//! By default, this new node will be a _scope node_. If you need to create a different kind of stack +//! graph node, set the `type` attribute on the new node: +//! +//! ``` skip +//! [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol" +//! } +//! ``` +//! +//! The valid `type` values are: +//! +//! - `drop_scopes`: a _drop scopes_ node +//! - `pop_symbol`: a _pop symbol_ node +//! - `pop_scoped_symbol`: a _pop scoped symbol_ node +//! - `push_symbol`: a _push symbol_ node +//! - `push_scoped_symbol`: a _push scoped symbol_ node +//! - `scope`: a _scope_ node +//! +//! A node without an explicit `type` attribute is assumed to be of type `scope`. +//! +//! Certain node types — `pop_symbol`, `pop_scoped_symbol`, `push_symbol` and `push_scoped_symbol` — +//! also require you to provide a `symbol` attribute. Its value must be a string, but will typically +//! come from the content of a parsed syntax node using the [`source-text`][] function and a syntax +//! capture: +//! +//! [`source-text`]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/functions/index.html#source-text +//! +//! ``` skip +//! @id [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol", symbol = (source-text @id) +//! } +//! ``` +//! +//! Node types `pop_symbol` and `pop_scoped_symbol` allow an optional `is_definition` attribute, +//! which marks that node as a proper definition. Node types `push_symbol` and `push_scoped_symbol` +//! allow an optional `is_reference` attribute, which marks the node as a proper reference. When +//! `is_definition` or `is_reference` are set, the `source_node` attribute is required. +//! +//! ``` skip +//! @id [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol", symbol = (source-text @id), is_reference, source_node = @id +//! } +//! ``` +//! +//! A _push scoped symbol_ node requires a `scope` attribute. Its value must be a reference to an +//! `exported` node that you've already created. (This is the exported scope node that will be +//! pushed onto the scope stack.) For instance: +//! +//! ``` skip +//! @id [Identifier] { +//! node new_exported_scope_node +//! attr (new_exported_scope_node) is_exported +//! node new_push_scoped_symbol_node +//! attr (new_push_scoped_symbol_node) +//! type = "push_scoped_symbol", +//! symbol = (source-text @id), +//! scope = new_exported_scope_node +//! } +//! ``` +//! +//! Nodes of type `scope` allow an optional `is_exported` attribute, that is required to use the +//! scope in a `push_scoped_symbol` node. +//! +//! +//! ### Annotating nodes with location information +//! +//! You can annotate any stack graph node that you create with location information, identifying +//! the portion of the source file that the node "belongs to". This is _required_ for definition +//! and reference nodes, since the location information determines which parts of the source file +//! the user can _click on_, and the _destination_ of any code navigation queries the user makes. +//! To do this, add a `source_node` attribute, whose value is a syntax node capture: +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! } +//! ``` +//! +//! Note how in this example, we use a different syntax node for the _target_ of the definition +//! (the entirety of the function definition) and for the _name_ of the definition (the content of +//! the function's `name`). +//! +//! Adding the `empty_source_span` attribute will use an empty source span located at the start of +//! the span of the `source_node`. This can be useful when a proper reference or definition is +//! desired, and thus `source_node` is required, but the span of the available source node is too +//! large. For example, a module definition which is located at the start of the program, but does +//! span the whole program: +//! +//! ``` skip +//! @unit [SourceUnit] { +//! ; ... +//! node mod_def +//! attr mod_def type = "pop_symbol", symbol = mod_name, is_definition, source_node = @unit, empty_source_span +//! ; ... +//! } +//! ``` +//! +//! ### Annotating nodes with syntax type information +//! +//! You can annotate any stack graph node with information about its syntax type. To do this, add a +//! `syntax_type` attribute, whose value is a string indicating the syntax type. +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! node def +//! ; ... +//! attr (def) syntax_type = "function" +//! } +//! ``` +//! +//! ### Annotating definitions with definiens information +//! +//! You cannot annotate definitions with a definiens, which is the thing the definition covers. For +//! example, for a function definition, the definiens would be the function body. To do this, add a +//! `definiens_node` attribute, whose value is a syntax node that spans the definiens. +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ... @body [FunctionBody] ...] { +//! node def +//! ; ... +//! attr (def) definiens_node = @body +//! } +//! ``` +//! +//! Definiens are optional and setting them to `#null` explicitly is allowed. +//! +//! ### Connecting stack graph nodes with edges +//! +//! To connect two stack graph nodes, use the `edge` statement to add an edge between them: +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! node body +//! edge def -> body +//! } +//! ``` +//! +//! To implement shadowing (which determines which definitions are selected when multiple are available), +//! you can add a `precedence` attribute to each edge to indicate which paths are prioritized: +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! node body +//! edge def -> body +//! attr (def -> body) precedence = 1 +//! } +//! ``` +//! +//! (If you don't specify a `precedence`, the default is 0.) +//! +//! ### Referring to the singleton nodes +//! +//! The _root node_ and _jump to scope node_ are singleton nodes that always exist for all stack +//! graphs. You can refer to them using the `ROOT_NODE` and `JUMP_TO_SCOPE_NODE` global variables: +//! +//! ``` skip +//! global ROOT_NODE +//! +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! edge ROOT_NODE -> def +//! } +//! ``` +//! +//! ### Attaching debug information to nodes +//! +//! It is possible to attach extra information to nodes for debugging purposes. This is done by adding +//! `debug_*` attributes to nodes. Each attribute defines a debug entry, with the key derived from the +//! attribute name, and the value the string representation of the attribute value. For example, mark +//! a scope node with a kind as follows: +//! +//! ``` skip +//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] { +//! ; ... +//! node param_scope +//! attr (param_scope) debug_kind = "param_scope" +//! ; ... +//! } +//! ``` +//! + +mod cancellation; + +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +pub use cancellation::{CancellationFlag, NoCancellation}; +use metaslang_cst::cursor::Cursor; +use metaslang_cst::KindTypes; +use metaslang_graph_builder::ast::File as GraphBuilderFile; +use metaslang_graph_builder::functions::Functions; +use metaslang_graph_builder::graph::{Edge, Graph, GraphNode, GraphNodeRef, Value}; +use metaslang_graph_builder::{ExecutionConfig, ExecutionError, Variables}; +use once_cell::sync::Lazy; +use semver::Version; +use stack_graphs::arena::Handle; +use stack_graphs::graph::{File, Node, NodeID, StackGraph}; +use thiserror::Error; + +// Node type values +static DROP_SCOPES_TYPE: &str = "drop_scopes"; +static POP_SCOPED_SYMBOL_TYPE: &str = "pop_scoped_symbol"; +static POP_SYMBOL_TYPE: &str = "pop_symbol"; +static PUSH_SCOPED_SYMBOL_TYPE: &str = "push_scoped_symbol"; +static PUSH_SYMBOL_TYPE: &str = "push_symbol"; +static SCOPE_TYPE: &str = "scope"; + +// Node attribute names +static DEBUG_ATTR_PREFIX: &str = "debug_"; +static DEFINIENS_NODE_ATTR: &str = "definiens_node"; +static EMPTY_SOURCE_SPAN_ATTR: &str = "empty_source_span"; +static IS_DEFINITION_ATTR: &str = "is_definition"; +static IS_ENDPOINT_ATTR: &str = "is_endpoint"; +static IS_EXPORTED_ATTR: &str = "is_exported"; +static IS_REFERENCE_ATTR: &str = "is_reference"; +static SCOPE_ATTR: &str = "scope"; +static SOURCE_NODE_ATTR: &str = "source_node"; +static SYMBOL_ATTR: &str = "symbol"; +static SYNTAX_TYPE_ATTR: &str = "syntax_type"; +static TYPE_ATTR: &str = "type"; + +// Expected attributes per node type +static POP_SCOPED_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + SYMBOL_ATTR, + IS_DEFINITION_ATTR, + DEFINIENS_NODE_ATTR, + SYNTAX_TYPE_ATTR, + ]) +}); +static POP_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + SYMBOL_ATTR, + IS_DEFINITION_ATTR, + DEFINIENS_NODE_ATTR, + SYNTAX_TYPE_ATTR, + ]) +}); +static PUSH_SCOPED_SYMBOL_ATTRS: Lazy> = + Lazy::new(|| HashSet::from([TYPE_ATTR, SYMBOL_ATTR, SCOPE_ATTR, IS_REFERENCE_ATTR])); +static PUSH_SYMBOL_ATTRS: Lazy> = + Lazy::new(|| HashSet::from([TYPE_ATTR, SYMBOL_ATTR, IS_REFERENCE_ATTR])); +static SCOPE_ATTRS: Lazy> = + Lazy::new(|| HashSet::from([TYPE_ATTR, IS_EXPORTED_ATTR, IS_ENDPOINT_ATTR])); + +// Edge attribute names +static PRECEDENCE_ATTR: &str = "precedence"; + +// Global variables +/// Name of the variable used to pass the root node. +pub const ROOT_NODE_VAR: &str = "ROOT_NODE"; +/// Name of the variable used to pass the file path. +pub const FILE_PATH_VAR: &str = "FILE_PATH"; +/// Version of the language being processed, to apply different semantic rules +pub const VERSION_VAR: &str = "VERSION"; + +pub struct Globals<'a> { + pub version: &'a Version, + pub file_path: &'a str, +} + +pub struct Builder<'a, KT: KindTypes> { + msgb: &'a GraphBuilderFile, + functions: &'a Functions, + stack_graph: &'a mut StackGraph, + file: Handle, + tree_cursor: Cursor, + graph: Graph, + remapped_nodes: HashMap, + injected_node_count: usize, +} + +impl<'a, KT: KindTypes + 'static> Builder<'a, KT> { + pub fn new( + msgb: &'a GraphBuilderFile, + functions: &'a Functions, + stack_graph: &'a mut StackGraph, + file: Handle, + tree_cursor: Cursor, + ) -> Self { + Builder { + msgb, + functions, + stack_graph, + file, + tree_cursor, + graph: Graph::new(), + remapped_nodes: HashMap::new(), + injected_node_count: 0, + } + } + + fn build_global_variables(&mut self, globals: &Globals<'_>) -> Variables<'a> { + let mut variables = Variables::new(); + variables + .add(FILE_PATH_VAR.into(), globals.file_path.into()) + .expect("failed to add FILE_PATH variable"); + variables + .add(VERSION_VAR.into(), globals.version.to_string().into()) + .expect("failed to add VERSION_VAR variable"); + + let root_node = self.inject_node(NodeID::root()); + variables + .add(ROOT_NODE_VAR.into(), root_node.into()) + .expect("Failed to set ROOT_NODE"); + + variables + } + + /// Executes this builder. + pub fn build( + &mut self, + globals: &Globals<'_>, + cancellation_flag: &dyn CancellationFlag, + on_added_node: impl FnMut(Handle, &Cursor), + ) -> Result<(), BuildError> { + let variables = self.build_global_variables(globals); + + let config = ExecutionConfig::new(self.functions, &variables) + .lazy(true) + .debug_attributes( + [DEBUG_ATTR_PREFIX, "msgb_location"] + .concat() + .as_str() + .into(), + [DEBUG_ATTR_PREFIX, "msgb_variable"] + .concat() + .as_str() + .into(), + [DEBUG_ATTR_PREFIX, "msgb_match_node"] + .concat() + .as_str() + .into(), + ); + + self.msgb.execute_into( + &mut self.graph, + &self.tree_cursor, + &config, + &(cancellation_flag as &dyn CancellationFlag), + )?; + + self.load(cancellation_flag, on_added_node) + } + + /// Create a graph node to represent the stack graph node. It is the callers responsibility to + /// ensure the stack graph node exists. + pub fn inject_node(&mut self, id: NodeID) -> GraphNodeRef { + let node = self.graph.add_graph_node(); + self.remapped_nodes.insert(node.index(), id); + self.injected_node_count += 1; + node + } +} + +/// An error that can occur while loading a stack graph from a TSG file +#[derive(Debug, Error)] +pub enum BuildError { + #[error("{0}")] + Cancelled(&'static str), + #[error("Missing ‘type’ attribute on graph node")] + MissingNodeType(GraphNodeRef), + #[error("Missing ‘symbol’ attribute on graph node")] + MissingSymbol(GraphNodeRef), + #[error("Missing ‘scope’ attribute on graph node")] + MissingScope(GraphNodeRef), + #[error("Unknown ‘{0}’ flag type {1}")] + UnknownFlagType(String, String), + #[error("Unknown node type {0}")] + UnknownNodeType(String), + #[error("Unknown symbol type {0}")] + UnknownSymbolType(String), + #[error(transparent)] + ExecutionError(ExecutionError), + #[error("Error converting shorthand ‘{0}’ on {1} with value {2}")] + ConversionError(String, String, String), + #[error("Expected exported symbol scope in {0}, got {1}")] + SymbolScopeError(String, String), +} + +impl From for BuildError { + fn from(value: stack_graphs::CancellationError) -> Self { + Self::Cancelled(value.0) + } +} + +impl From for BuildError { + fn from(value: ExecutionError) -> Self { + match value { + ExecutionError::Cancelled(err) => Self::Cancelled(err.0), + err => Self::ExecutionError(err), + } + } +} + +impl BuildError { + pub fn display_pretty<'a>( + &'a self, + source_path: &'a Path, + source: &'a str, + tsg_path: &'a Path, + tsg: &'a str, + ) -> impl std::fmt::Display + 'a { + DisplayBuildErrorPretty { + error: self, + source_path, + source, + tsg_path, + tsg, + } + } +} + +struct DisplayBuildErrorPretty<'a> { + error: &'a BuildError, + source_path: &'a Path, + source: &'a str, + tsg_path: &'a Path, + tsg: &'a str, +} + +impl std::fmt::Display for DisplayBuildErrorPretty<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.error { + BuildError::ExecutionError(err) => write!( + f, + "{}", + err.display_pretty(self.source_path, self.source, self.tsg_path, self.tsg) + ), + err => err.fmt(f), + } + } +} + +impl<'a, KT: KindTypes + 'static> Builder<'a, KT> { + fn load( + &mut self, + cancellation_flag: &dyn CancellationFlag, + mut on_added_node: impl FnMut(Handle, &Cursor), + ) -> Result<(), BuildError> { + let cancellation_flag: &dyn stack_graphs::CancellationFlag = &cancellation_flag; + + // By default graph ids are used for stack graph local_ids. A remapping is computed + // for local_ids that already exist in the graph---all other graph ids are mapped to + // the same local_id. See [`self.node_id_for_index`] for more details. + let mut next_local_id = u32::try_from(self.graph.node_count() - self.injected_node_count) + .expect("nodes local_id to fit in u32"); + for node in self.stack_graph.nodes_for_file(self.file) { + let local_id = self.stack_graph[node].id().local_id(); + let index = (local_id as usize) + self.injected_node_count; + // find next available local_id for which no stack graph node exists yet + while self + .stack_graph + .node_for_id(NodeID::new_in_file(self.file, next_local_id)) + .is_some() + { + next_local_id += 1; + } + // remap graph node index to the available stack graph node local_id + if self + .remapped_nodes + .insert(index, NodeID::new_in_file(self.file, next_local_id)) + .is_some() + { + panic!("index already remapped"); + } + } + + // First create a stack graph node for each TSG node. (The skip(...) is because the first + // DSL nodes that we create are the proxies for the injected stack graph nodes.) + for node_ref in self.graph.iter_nodes().skip(self.injected_node_count) { + cancellation_flag.check("loading graph nodes")?; + let node_type = self.get_node_type(node_ref)?; + let handle = match node_type { + NodeType::DropScopes => self.load_drop_scopes(node_ref), + NodeType::PopScopedSymbol => self.load_pop_scoped_symbol(node_ref)?, + NodeType::PopSymbol => self.load_pop_symbol(node_ref)?, + NodeType::PushScopedSymbol => self.load_push_scoped_symbol(node_ref)?, + NodeType::PushSymbol => self.load_push_symbol(node_ref)?, + NodeType::Scope => self.load_scope(node_ref)?, + }; + self.load_source_info(node_ref, handle)?; + self.load_node_debug_info(node_ref, handle); + + // For every added graph node which links to a corresponding source + // node, invoke the callback so our caller can link the newly built + // node with the matching CST cursor. + let node = &self.graph[node_ref]; + if let Some(source_node) = node.attributes.get(SOURCE_NODE_ATTR) { + let syntax_node_ref = source_node.as_syntax_node_ref()?; + let source_node = &self.graph[syntax_node_ref]; + on_added_node(handle, source_node); + } + } + + for node in self.stack_graph.nodes_for_file(self.file) { + self.verify_node(node)?; + } + + // Then add stack graph edges for each TSG edge. Note that we _don't_ skip(...) here because + // there might be outgoing nodes from the “root” node that we need to process. + // (Technically the caller could add outgoing nodes from “jump to scope” as well, but those + // are invalid according to the stack graph semantics and will never be followed. + for source_ref in self.graph.iter_nodes() { + let source = &self.graph[source_ref]; + let source_node_id = self.node_id_for_graph_node(source_ref); + let source_handle = self.stack_graph.node_for_id(source_node_id).unwrap(); + for (sink_ref, edge) in source.iter_edges() { + cancellation_flag.check("loading graph edges")?; + let precedence = match edge.attributes.get(PRECEDENCE_ATTR) { + Some(precedence) => precedence.as_integer()?, + None => 0, + } + .try_into() + .map_err(|_| ExecutionError::ExpectedInteger("integer does not fit".to_string()))?; + let sink_node_id = self.node_id_for_graph_node(sink_ref); + let sink_handle = self.stack_graph.node_for_id(sink_node_id).unwrap(); + self.stack_graph + .add_edge(source_handle, sink_handle, precedence); + Self::load_edge_debug_info(self.stack_graph, source_handle, sink_handle, edge); + } + } + + Ok(()) + } + + fn get_node_type(&self, node_ref: GraphNodeRef) -> Result { + let node = &self.graph[node_ref]; + let node_type = match node.attributes.get(TYPE_ATTR) { + Some(node_type) => node_type.as_str()?, + None => return Ok(NodeType::Scope), + }; + if node_type == DROP_SCOPES_TYPE { + Ok(NodeType::DropScopes) + } else if node_type == POP_SCOPED_SYMBOL_TYPE { + Ok(NodeType::PopScopedSymbol) + } else if node_type == POP_SYMBOL_TYPE { + Ok(NodeType::PopSymbol) + } else if node_type == PUSH_SCOPED_SYMBOL_TYPE { + Ok(NodeType::PushScopedSymbol) + } else if node_type == PUSH_SYMBOL_TYPE { + Ok(NodeType::PushSymbol) + } else if node_type == SCOPE_TYPE { + Ok(NodeType::Scope) + } else { + Err(BuildError::UnknownNodeType(node_type.to_string())) + } + } + + fn verify_node(&self, node: Handle) -> Result<(), BuildError> { + if let Node::PushScopedSymbol(node) = &self.stack_graph[node] { + let scope = &self.stack_graph[self.stack_graph.node_for_id(node.scope).unwrap()]; + if !scope.is_exported_scope() { + return Err(BuildError::SymbolScopeError( + format!("{}", node.display(self.stack_graph)), + format!("{}", scope.display(self.stack_graph)), + )); + } + } + Ok(()) + } +} + +enum NodeType { + DropScopes, + PopSymbol, + PopScopedSymbol, + PushSymbol, + PushScopedSymbol, + Scope, +} + +impl<'a, KT: KindTypes> Builder<'a, KT> { + /// Get the `NodeID` corresponding to a `Graph` node. + /// + /// By default, graph nodes get their index shifted by [`self.injected_node_count`] as their + /// `local_id`, unless they have a corresponding entry in the [`self.remapped_nodes`] map. This + /// is the case if: + /// 1. The node was injected, in which case it is mapped to the `NodeID` of the injected node. + /// 2. The node's default `local_id` clashes with a preexisting node, in which case it is mapped to + /// an available `local_id` beyond the range of default `local_ids`. + fn node_id_for_graph_node(&self, node_ref: GraphNodeRef) -> NodeID { + let index = node_ref.index(); + self.remapped_nodes.get(&index).map_or_else( + || { + NodeID::new_in_file( + self.file, + u32::try_from(index - self.injected_node_count) + .expect("local_id to fit in u32"), + ) + }, + |id| *id, + ) + } + + fn load_drop_scopes(&mut self, node_ref: GraphNodeRef) -> Handle { + let id = self.node_id_for_graph_node(node_ref); + self.stack_graph.add_drop_scopes_node(id).unwrap() + } + + fn load_pop_scoped_symbol( + &mut self, + node_ref: GraphNodeRef, + ) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; + Self::verify_attributes(node, POP_SCOPED_SYMBOL_TYPE, &POP_SCOPED_SYMBOL_ATTRS); + let node_handle = self + .stack_graph + .add_pop_scoped_symbol_node(id, symbol, is_definition) + .unwrap(); + if is_definition { + self.load_definiens_info(node_ref, node_handle)?; + } + Ok(node_handle) + } + + fn load_pop_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; + Self::verify_attributes(node, POP_SYMBOL_TYPE, &POP_SYMBOL_ATTRS); + let node_handle = self + .stack_graph + .add_pop_symbol_node(id, symbol, is_definition) + .unwrap(); + if is_definition { + self.load_definiens_info(node_ref, node_handle)?; + } + Ok(node_handle) + } + + fn load_push_scoped_symbol( + &mut self, + node_ref: GraphNodeRef, + ) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let scope = match node.attributes.get(SCOPE_ATTR) { + Some(scope) => self.node_id_for_graph_node(scope.as_graph_node_ref()?), + None => return Err(BuildError::MissingScope(node_ref)), + }; + let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; + Self::verify_attributes(node, PUSH_SCOPED_SYMBOL_TYPE, &PUSH_SCOPED_SYMBOL_ATTRS); + Ok(self + .stack_graph + .add_push_scoped_symbol_node(id, symbol, scope, is_reference) + .unwrap()) + } + + fn load_push_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; + Self::verify_attributes(node, PUSH_SYMBOL_TYPE, &PUSH_SYMBOL_ATTRS); + Ok(self + .stack_graph + .add_push_symbol_node(id, symbol, is_reference) + .unwrap()) + } + + fn load_scope(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let id = self.node_id_for_graph_node(node_ref); + let is_exported = + Self::load_flag(node, IS_EXPORTED_ATTR)? || Self::load_flag(node, IS_ENDPOINT_ATTR)?; + Self::verify_attributes(node, SCOPE_TYPE, &SCOPE_ATTRS); + Ok(self.stack_graph.add_scope_node(id, is_exported).unwrap()) + } + + fn load_symbol(value: &Value) -> Result { + match value { + Value::Integer(i) => Ok(i.to_string()), + Value::String(s) => Ok(s.clone()), + _ => Err(BuildError::UnknownSymbolType(value.to_string())), + } + } + + fn load_flag(node: &GraphNode, attribute: &str) -> Result { + match node.attributes.get(attribute) { + Some(value) => value + .as_boolean() + .map_err(|_| BuildError::UnknownFlagType(attribute.to_string(), value.to_string())), + None => Ok(false), + } + } + + fn load_source_info( + &mut self, + node_ref: GraphNodeRef, + node_handle: Handle, + ) -> Result<(), BuildError> { + let node = &self.graph[node_ref]; + + if let Some(source_node) = node.attributes.get(SOURCE_NODE_ATTR) { + let syntax_node_ref = source_node.as_syntax_node_ref()?; + let source_node = &self.graph[syntax_node_ref]; + let mut source_range = source_node.text_range(); + if match node.attributes.get(EMPTY_SOURCE_SPAN_ATTR) { + Some(empty_source_span) => empty_source_span.as_boolean()?, + None => false, + } { + source_range.end = source_range.start; + } + let _source_info = self.stack_graph.source_info_mut(node_handle); + // TODO: map node's TextRange into a Span + // source_info.span = text_range_into_span(source_range); + } + + if let Some(syntax_type) = node.attributes.get(SYNTAX_TYPE_ATTR) { + let syntax_type = syntax_type.as_str()?; + let syntax_type = self.stack_graph.add_string(syntax_type); + let source_info = self.stack_graph.source_info_mut(node_handle); + source_info.syntax_type = syntax_type.into(); + } + + Ok(()) + } + + // TODO: we can probably remove this and all references to definiens since + // we're not gonna need it? + fn load_definiens_info( + &mut self, + node_ref: GraphNodeRef, + node_handle: Handle, + ) -> Result<(), BuildError> { + let node = &self.graph[node_ref]; + let definiens_node = match node.attributes.get(DEFINIENS_NODE_ATTR) { + Some(Value::Null) => return Ok(()), + Some(definiens_node) => &self.graph[definiens_node.as_syntax_node_ref()?], + None => return Ok(()), + }; + let _definiens_range = definiens_node.text_range(); + let _source_info = self.stack_graph.source_info_mut(node_handle); + // TODO: map node's TextRange into a Span + // source_info.definiens_span = text_range_into_span(definiens_range); + Ok(()) + } + + fn load_node_debug_info(&mut self, node_ref: GraphNodeRef, node_handle: Handle) { + let node = &self.graph[node_ref]; + for (name, value) in node.attributes.iter() { + let name = name.to_string(); + if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { + let value = match value { + Value::String(value) => value.clone(), + value => value.to_string(), + }; + let key = self.stack_graph.add_string(name_without_prefix); + let value = self.stack_graph.add_string(&value); + self.stack_graph + .node_debug_info_mut(node_handle) + .add(key, value); + } + } + } + + fn load_edge_debug_info( + stack_graph: &mut StackGraph, + source_handle: Handle, + sink_handle: Handle, + edge: &Edge, + ) { + for (name, value) in edge.attributes.iter() { + let name = name.to_string(); + if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { + let value = match value { + Value::String(value) => value.clone(), + value => value.to_string(), + }; + let key = stack_graph.add_string(name_without_prefix); + let value = stack_graph.add_string(&value); + stack_graph + .edge_debug_info_mut(source_handle, sink_handle) + .add(key, value); + } + } + } + + fn verify_attributes( + node: &GraphNode, + node_type: &str, + allowed_attributes: &HashSet<&'static str>, + ) { + for (id, _) in node.attributes.iter() { + let id = id.as_str(); + if !allowed_attributes.contains(id) + && id != SOURCE_NODE_ATTR + && id != EMPTY_SOURCE_SPAN_ATTR + && !id.starts_with(DEBUG_ATTR_PREFIX) + { + eprintln!("Unexpected attribute {id} on node of type {node_type}"); + } + } + } +} diff --git a/crates/metaslang/bindings/src/lib.rs b/crates/metaslang/bindings/src/lib.rs new file mode 100644 index 0000000000..8ceaca1309 --- /dev/null +++ b/crates/metaslang/bindings/src/lib.rs @@ -0,0 +1,140 @@ +pub mod builder; + +use std::collections::{BTreeSet, HashMap}; +use std::fmt; +use std::fmt::Debug; +use std::iter::once; + +use metaslang_cst::cursor::Cursor; +use metaslang_cst::KindTypes; +use metaslang_graph_builder::ast::File; +use metaslang_graph_builder::functions::Functions; +use semver::Version; +use stack_graphs::graph::StackGraph; +use stack_graphs::partial::PartialPaths; +use stack_graphs::stitching::{ForwardPartialPathStitcher, GraphEdgeCandidates, StitcherConfig}; + +type Builder<'a, KT> = builder::Builder<'a, KT>; +type GraphHandle = stack_graphs::arena::Handle; + +pub struct Bindings { + version: Version, + graph_builder_file: File, + functions: Functions, + stack_graph: StackGraph, + cursors: HashMap>, +} + +impl Bindings { + pub fn create(version: Version, binding_rules: &str) -> Self { + let graph_builder_file = + File::from_str(binding_rules).expect("Bindings stack graph builder parse error"); + let stack_graph = StackGraph::new(); + let functions = Functions::stdlib(); + let cursors = HashMap::new(); + + Self { + version, + graph_builder_file, + functions, + stack_graph, + cursors, + } + } + + pub fn add_file(&mut self, file_path: &str, tree_cursor: Cursor) { + let globals = builder::Globals { + version: &self.version, + file_path, + }; + let file = self.stack_graph.get_or_create_file(file_path); + + let mut builder = Builder::new( + &self.graph_builder_file, + &self.functions, + &mut self.stack_graph, + file, + tree_cursor, + ); + builder + .build(&globals, &builder::NoCancellation, |handle, cursor| { + self.cursors.insert(handle, cursor.clone()); + }) + .expect("Internal error while building bindings"); + } + + pub fn all_definitions(&self) -> impl Iterator> + '_ { + self.stack_graph + .iter_nodes() + .filter(|handle| self.stack_graph[*handle].is_definition()) + .map(|handle| Handle { + owner: self, + handle, + }) + } + + pub fn all_references(&self) -> impl Iterator> + '_ { + self.stack_graph + .iter_nodes() + .filter(|handle| self.stack_graph[*handle].is_reference()) + .map(|handle| Handle { + owner: self, + handle, + }) + } +} + +pub struct Handle<'a, KT: KindTypes + 'static> { + owner: &'a Bindings, + handle: GraphHandle, +} + +impl Handle<'_, KT> { + pub fn is_definition(&self) -> bool { + self.owner.stack_graph[self.handle].is_definition() + } + + pub fn is_reference(&self) -> bool { + self.owner.stack_graph[self.handle].is_reference() + } + + pub fn get_cursor(&self) -> Option> { + self.owner.cursors.get(&self.handle).cloned() + } + + pub fn get_file(&self) -> Option<&str> { + self.owner.stack_graph[self.handle] + .file() + .map(|file| self.owner.stack_graph[file].name()) + } + + pub fn jump_to_definition(&self) -> Option { + let mut paths = PartialPaths::new(); + let mut results = BTreeSet::new(); + if self.is_reference() { + ForwardPartialPathStitcher::find_all_complete_partial_paths( + &mut GraphEdgeCandidates::new(&self.owner.stack_graph, &mut paths, None), + once(self.handle), + StitcherConfig::default(), + &stack_graphs::NoCancellation, + |_graph, _paths, path| { + results.insert(path.end_node); + }, + ) + .expect("should never be cancelled"); + } + if results.len() > 1 { + println!("WARN: More than one definition found for {self:?}"); + } + results.first().map(|handle| Handle { + owner: self.owner, + handle: *handle, + }) + } +} + +impl Debug for Handle<'_, KT> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("BindingsHandle").field(&self.handle).finish() + } +} diff --git a/crates/solidity/inputs/language/bindings/rules.msgb b/crates/solidity/inputs/language/bindings/rules.msgb index 4254c58268..9466adb5f6 100644 --- a/crates/solidity/inputs/language/bindings/rules.msgb +++ b/crates/solidity/inputs/language/bindings/rules.msgb @@ -1 +1,363 @@ -// TODO(#982): add rules for Solidity here... +attribute node_definition = node => type = "pop_symbol", node_symbol = node, is_definition +attribute node_reference = node => type = "push_symbol", node_symbol = node, is_reference +attribute node_symbol = node => symbol = (source-text node), source_node = node +attribute pop_symbol = symbol => type = "pop_symbol", symbol = symbol +attribute push_symbol = symbol => type = "push_symbol", symbol = symbol +attribute symbol_definition = symbol => type = "pop_symbol", symbol = symbol, is_definition +attribute symbol_reference = symbol => type = "push_symbol", symbol = symbol, is_reference + +;; Generalities +;; - we will define two nodes for all meaningful CST nodes +;; - a lexical_scope node which will connect "upwards" towards the root of the CST +;; - a defs node to access the definitions reachable from each node (usually connecting "downwards") +;; - the pair will not be created for every CST node, as there is a lot of redundancy in the tree +;; - identifier nodes that are part of the definition of an artifact +;; will create graph nodes with the node_definition attributes +;; - identifier nodes that are references will create graph nodes with the node_reference attributes + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Source unit (aka .sol file) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@source_unit [SourceUnit] { + node @source_unit.lexical_scope + node @source_unit.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Contract definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@contract [ContractDefinition] { + node @contract.lexical_scope + node @contract.defs +} + +@contract [ContractDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @contract.lexical_scope -> def + edge @contract.defs -> def +} + +;; Connect the contract to its containing source unit +@source_unit [SourceUnit ... [SourceUnitMembers + ... + [SourceUnitMember @contract [ContractDefinition]] + ... +] ...] { + edge @source_unit.defs -> @contract.defs + edge @contract.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Interface definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@interface [InterfaceDefinition] { + node @interface.lexical_scope + node @interface.defs +} + +@interface [InterfaceDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @interface.lexical_scope -> def + edge @interface.defs -> def +} + +;; Connect the interface to its containing source unit +@source_unit [SourceUnit [SourceUnitMembers + ... + [SourceUnitMember @interface [InterfaceDefinition]] + ... +]] { + edge @source_unit.defs -> @interface.defs + edge @interface.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Library definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@library [LibraryDefinition] { + node @library.lexical_scope + node @library.defs +} + +@library [LibraryDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @library.lexical_scope -> def + edge @library.defs -> def +} + +;; Connect the library to its containing source unit +@source_unit [SourceUnit [SourceUnitMembers + ... + [SourceUnitMember @library [LibraryDefinition]] + ... +]] { + edge @source_unit.defs -> @library.defs + edge @library.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Function definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@function [FunctionDefinition] { + node @function.lexical_scope + node @function.defs +} + +@function [FunctionDefinition ... name: [FunctionName ... @name [Identifier] ...] ...] { + node def + attr (def) node_definition = @name + + edge @function.lexical_scope -> def + edge @function.defs -> def +} + +@param [Parameter] { + node @param.lexical_scope + node @param.defs +} + +@param [Parameter ... @name [Identifier]] { + node def + attr (def) node_definition = @name + + edge @param.lexical_scope -> def + edge @param.defs -> def +} + +;; Connect the parameters to the functions they belong to +@function [FunctionDefinition + ... + parameters: [_ ... parameters: [Parameters ... @param item: [Parameter] ...] ...] + ... +] { + edge @function.lexical_scope -> @param.defs + edge @function.defs -> @param.defs +} + +;; Connect the function to the contract/interface/library they belong to +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @function variant: [FunctionDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @function.defs + edge @unit_member.defs -> @function.defs + edge @function.lexical_scope -> @unit_member.lexical_scope +} + +@body [FunctionBody] { + node @body.lexical_scope + node @body.defs +} + +;; Connect the function body to the function definition +@function [FunctionDefinition ... @body body: [FunctionBody] ...] { + edge @body.lexical_scope -> @function.lexical_scope + edge @function.defs -> @body.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Blocks +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@block [Block] { + node @block.lexical_scope + node @block.defs +} + +@stmt [Statement] { + node @stmt.lexical_scope + node @stmt.defs +} + +@block [Block ... statements: [_ ... @stmt [Statement]...] ...] { + edge @stmt.lexical_scope -> @block.lexical_scope + edge @block.defs -> @stmt.defs +} + +@body [FunctionBody @block variant: [Block]] { + edge @block.lexical_scope -> @body.lexical_scope + edge @body.defs -> @block.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Declaration Statements +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@stmt [Statement [VariableDeclarationStatement ... @name name: [Identifier] ...]] { + node def + attr (def) node_definition = @name + + edge @stmt.lexical_scope -> def + edge @stmt.defs -> def +} + +@stmt [Statement [TupleDeconstructionStatement + ... + [TupleDeconstructionElements + ... + item: [_ member: [_ variant: [_ ... @name name: [Identifier]]]] + ... + ] + ... +]] { + node def + attr (def) node_definition = @name + + edge @stmt.lexical_scope -> def + edge @stmt.defs -> def +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; State Variables +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@state_var [StateVariableDefinition] { + node @state_var.lexical_scope + node @state_var.defs +} + +@state_var [StateVariableDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @state_var.lexical_scope -> def + edge @state_var.defs -> def +} + +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @state_var variant: [StateVariableDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @state_var.defs + edge @unit_member.defs -> @state_var.defs + edge @state_var.lexical_scope -> @unit_member.lexical_scope +} + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Structure definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@struct [StructDefinition] { + node @struct.lexical_scope + node @struct.defs +} + +@struct [StructDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @struct.lexical_scope -> def + edge @struct.defs -> def +} + +;; Connect the struct to the contract/interface/library they belong to +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @struct variant: [StructDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @struct.defs + edge @unit_member.defs -> @struct.defs +} + +@member [StructMember] { + node @member.lexical_scope + node @member.defs +} + +@member item: [StructMember ... @name name: [Identifier] ...] { + node member + attr (member) pop_symbol = "." + edge @member.defs -> member + + node def + attr (def) node_definition = @name + edge member -> def +} + +;; TODO: missing connection between the struct field declarations and the struct + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Expressions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@expr [Expression] { + node @expr.lexical_scope +} + +@expr [Expression ... @name variant: [Identifier]] { + node ref + attr (ref) node_reference = @name + + edge ref -> @expr.lexical_scope +} + +@expr [Expression ... variant: [_ ... @child [Expression] ...] ...] { + edge @child.lexical_scope -> @expr.lexical_scope +} + +@stmt [Statement ... variant: [_ ... @expr [Expression] ...] ...] { + edge @expr.lexical_scope -> @stmt.lexical_scope +} + +@member [MemberAccess] { + node @member.lexical_scope +} + +@member [MemberAccess @name [Identifier]] { + node ref + attr (ref) node_reference = @name + + edge ref -> @member.lexical_scope +} + +[MemberAccessExpression + ... + @expr operand: [Expression] + ... + @member member: [MemberAccess] + ... +] { + node member + attr (member) push_symbol = "." + + edge @member.lexical_scope -> member + edge member -> @expr.lexical_scope +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml index 98e1f55650..a11d7e0ea4 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml +++ b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml @@ -37,7 +37,7 @@ cli = ["dep:clap", "dep:serde_json", "__private_ariadne"] # This is meant to be used by the CLI or internally only. __private_ariadne = ["dep:ariadne"] # For internal development only -__experimental_bindings_api = ["dep:metaslang_graph_builder"] +__experimental_bindings_api = ["dep:metaslang_bindings"] [build-dependencies] # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ anyhow = { workspace = true } # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ @@ -48,8 +48,8 @@ solidity_language = { workspace = true } # __REMOVE_THIS_LINE_DURING_CAR [dependencies] ariadne = { workspace = true, optional = true } clap = { workspace = true, optional = true } +metaslang_bindings = { workspace = true, optional = true } metaslang_cst = { workspace = true } -metaslang_graph_builder = { workspace = true, optional = true } semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, optional = true } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/generated/binding_rules.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/generated/binding_rules.rs index 49adbd1481..7715b748c1 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/generated/binding_rules.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/generated/binding_rules.rs @@ -3,6 +3,368 @@ #[allow(clippy::needless_raw_string_hashes)] #[allow(dead_code)] // TODO(#982): use to create the graph pub const BINDING_RULES_SOURCE: &str = r#####" - // TODO(#982): add rules for Solidity here... + attribute node_definition = node => type = "pop_symbol", node_symbol = node, is_definition +attribute node_reference = node => type = "push_symbol", node_symbol = node, is_reference +attribute node_symbol = node => symbol = (source-text node), source_node = node +attribute pop_symbol = symbol => type = "pop_symbol", symbol = symbol +attribute push_symbol = symbol => type = "push_symbol", symbol = symbol +attribute symbol_definition = symbol => type = "pop_symbol", symbol = symbol, is_definition +attribute symbol_reference = symbol => type = "push_symbol", symbol = symbol, is_reference + +;; Generalities +;; - we will define two nodes for all meaningful CST nodes +;; - a lexical_scope node which will connect "upwards" towards the root of the CST +;; - a defs node to access the definitions reachable from each node (usually connecting "downwards") +;; - the pair will not be created for every CST node, as there is a lot of redundancy in the tree +;; - identifier nodes that are part of the definition of an artifact +;; will create graph nodes with the node_definition attributes +;; - identifier nodes that are references will create graph nodes with the node_reference attributes + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Source unit (aka .sol file) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@source_unit [SourceUnit] { + node @source_unit.lexical_scope + node @source_unit.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Contract definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@contract [ContractDefinition] { + node @contract.lexical_scope + node @contract.defs +} + +@contract [ContractDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @contract.lexical_scope -> def + edge @contract.defs -> def +} + +;; Connect the contract to its containing source unit +@source_unit [SourceUnit ... [SourceUnitMembers + ... + [SourceUnitMember @contract [ContractDefinition]] + ... +] ...] { + edge @source_unit.defs -> @contract.defs + edge @contract.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Interface definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@interface [InterfaceDefinition] { + node @interface.lexical_scope + node @interface.defs +} + +@interface [InterfaceDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @interface.lexical_scope -> def + edge @interface.defs -> def +} + +;; Connect the interface to its containing source unit +@source_unit [SourceUnit [SourceUnitMembers + ... + [SourceUnitMember @interface [InterfaceDefinition]] + ... +]] { + edge @source_unit.defs -> @interface.defs + edge @interface.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Library definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@library [LibraryDefinition] { + node @library.lexical_scope + node @library.defs +} + +@library [LibraryDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @library.lexical_scope -> def + edge @library.defs -> def +} + +;; Connect the library to its containing source unit +@source_unit [SourceUnit [SourceUnitMembers + ... + [SourceUnitMember @library [LibraryDefinition]] + ... +]] { + edge @source_unit.defs -> @library.defs + edge @library.lexical_scope -> @source_unit.lexical_scope +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Function definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@function [FunctionDefinition] { + node @function.lexical_scope + node @function.defs +} + +@function [FunctionDefinition ... name: [FunctionName ... @name [Identifier] ...] ...] { + node def + attr (def) node_definition = @name + + edge @function.lexical_scope -> def + edge @function.defs -> def +} + +@param [Parameter] { + node @param.lexical_scope + node @param.defs +} + +@param [Parameter ... @name [Identifier]] { + node def + attr (def) node_definition = @name + + edge @param.lexical_scope -> def + edge @param.defs -> def +} + +;; Connect the parameters to the functions they belong to +@function [FunctionDefinition + ... + parameters: [_ ... parameters: [Parameters ... @param item: [Parameter] ...] ...] + ... +] { + edge @function.lexical_scope -> @param.defs + edge @function.defs -> @param.defs +} + +;; Connect the function to the contract/interface/library they belong to +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @function variant: [FunctionDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @function.defs + edge @unit_member.defs -> @function.defs + edge @function.lexical_scope -> @unit_member.lexical_scope +} + +@body [FunctionBody] { + node @body.lexical_scope + node @body.defs +} + +;; Connect the function body to the function definition +@function [FunctionDefinition ... @body body: [FunctionBody] ...] { + edge @body.lexical_scope -> @function.lexical_scope + edge @function.defs -> @body.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Blocks +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@block [Block] { + node @block.lexical_scope + node @block.defs +} + +@stmt [Statement] { + node @stmt.lexical_scope + node @stmt.defs +} + +@block [Block ... statements: [_ ... @stmt [Statement]...] ...] { + edge @stmt.lexical_scope -> @block.lexical_scope + edge @block.defs -> @stmt.defs +} + +@body [FunctionBody @block variant: [Block]] { + edge @block.lexical_scope -> @body.lexical_scope + edge @body.defs -> @block.defs +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Declaration Statements +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@stmt [Statement [VariableDeclarationStatement ... @name name: [Identifier] ...]] { + node def + attr (def) node_definition = @name + + edge @stmt.lexical_scope -> def + edge @stmt.defs -> def +} + +@stmt [Statement [TupleDeconstructionStatement + ... + [TupleDeconstructionElements + ... + item: [_ member: [_ variant: [_ ... @name name: [Identifier]]]] + ... + ] + ... +]] { + node def + attr (def) node_definition = @name + + edge @stmt.lexical_scope -> def + edge @stmt.defs -> def +} + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; State Variables +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@state_var [StateVariableDefinition] { + node @state_var.lexical_scope + node @state_var.defs +} + +@state_var [StateVariableDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @state_var.lexical_scope -> def + edge @state_var.defs -> def +} + +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @state_var variant: [StateVariableDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @state_var.defs + edge @unit_member.defs -> @state_var.defs + edge @state_var.lexical_scope -> @unit_member.lexical_scope +} + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Structure definitions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@struct [StructDefinition] { + node @struct.lexical_scope + node @struct.defs +} + +@struct [StructDefinition ... @name name: [Identifier] ...] { + node def + attr (def) node_definition = @name + + edge @struct.lexical_scope -> def + edge @struct.defs -> def +} + +;; Connect the struct to the contract/interface/library they belong to +[SourceUnitMember @unit_member variant: [_ + ... + members: [_ + ... + item: [_ @struct variant: [StructDefinition]] + ... + ] + ... + ] +] { + edge @unit_member.lexical_scope -> @struct.defs + edge @unit_member.defs -> @struct.defs +} + +@member [StructMember] { + node @member.lexical_scope + node @member.defs +} + +@member item: [StructMember ... @name name: [Identifier] ...] { + node member + attr (member) pop_symbol = "." + edge @member.defs -> member + + node def + attr (def) node_definition = @name + edge member -> def +} + +;; TODO: missing connection between the struct field declarations and the struct + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Expressions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +@expr [Expression] { + node @expr.lexical_scope +} + +@expr [Expression ... @name variant: [Identifier]] { + node ref + attr (ref) node_reference = @name + + edge ref -> @expr.lexical_scope +} + +@expr [Expression ... variant: [_ ... @child [Expression] ...] ...] { + edge @child.lexical_scope -> @expr.lexical_scope +} + +@stmt [Statement ... variant: [_ ... @expr [Expression] ...] ...] { + edge @expr.lexical_scope -> @stmt.lexical_scope +} + +@member [MemberAccess] { + node @member.lexical_scope +} + +@member [MemberAccess @name [Identifier]] { + node ref + attr (ref) node_reference = @name + + edge ref -> @member.lexical_scope +} + +[MemberAccessExpression + ... + @expr operand: [Expression] + ... + @member member: [MemberAccess] + ... +] { + node member + attr (member) push_symbol = "." + + edge @member.lexical_scope -> member + edge member -> @expr.lexical_scope +} "#####; diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/mod.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/mod.rs index 47ccc571ee..78c2b74f82 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/mod.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/bindings/mod.rs @@ -3,10 +3,14 @@ #[path = "generated/binding_rules.rs"] mod binding_rules; -use metaslang_graph_builder::ast; -pub use metaslang_graph_builder::functions::Functions; -pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; +use metaslang_bindings; +use semver::Version; use crate::cst::KindTypes; -pub type File = ast::File; +pub type Bindings = metaslang_bindings::Bindings; +pub type Handle<'a> = metaslang_bindings::Handle<'a, KindTypes>; + +pub fn create(version: Version) -> Bindings { + Bindings::create(version, binding_rules::BINDING_RULES_SOURCE) +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/bindings.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/bindings.rs new file mode 100644 index 0000000000..56279e2b34 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/bindings.rs @@ -0,0 +1,88 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use core::fmt; + +use semver::Version; + +use super::CommandError; +use crate::bindings::{self, Bindings, Handle}; +use crate::cursor::Cursor; + +pub fn execute(file_path_string: &str, version: Version) -> Result<(), CommandError> { + let mut bindings = bindings::create(version.clone()); + let parse_output = super::parse::parse_source_file(file_path_string, version, |_| ())?; + let tree_cursor = parse_output.create_tree_cursor(); + + bindings.add_file(file_path_string, tree_cursor); + + print_definitions(&bindings); + print_references(&bindings); + + Ok(()) +} + +fn print_definitions(bindings: &Bindings) { + println!("\nAll definitions found:"); + for definition in bindings.all_definitions() { + println!("{}", DisplayDefinition(&definition)); + } +} + +fn print_references(bindings: &Bindings) { + println!("\nAll references found:"); + for reference in bindings.all_references() { + println!("{}", DisplayReference(&reference)); + if let Some(def) = reference.jump_to_definition() { + println!(" -> {}", DisplayDefinition(&def)); + } else { + println!(" -> No definition found"); + } + } +} + +struct DisplayRange<'a>(&'a Cursor); + +impl<'a> fmt::Display for DisplayRange<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let range = self.0.text_range(); + write!(f, "{}..{}", range.start, range.end) + } +} + +struct DisplayDefinition<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayDefinition<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` defined at {location} in {file}") + } else { + write!( + f, + "Definition without available cursor: {definition:?} in {file}", + definition = self.0 + ) + } + } +} + +struct DisplayReference<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayReference<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` referenced at {location} in {file}") + } else { + write!( + f, + "Reference without available cursor: {reference:?} in {file}", + reference = self.0 + ) + } + } +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs deleted file mode 100644 index 45799e4816..0000000000 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs +++ /dev/null @@ -1,59 +0,0 @@ -// This file is generated automatically by infrastructure scripts. Please don't edit by hand. - -use std::fs; -use std::path::PathBuf; - -use semver::Version; - -use super::parse::parse_source_file; -use super::CommandError; -use crate::bindings::{ - ExecutionConfig, File as GraphBuilderFile, Functions, NoCancellation, Variables, -}; - -pub fn execute( - file_path_string: &str, - version: Version, - msgb_path_string: &str, - output_json: bool, - debug: bool, -) -> Result<(), CommandError> { - let parse_output = parse_source_file(file_path_string, version, |_| ())?; - let msgb = parse_graph_builder(msgb_path_string)?; - - let functions = Functions::stdlib(); - let variables = Variables::new(); - let mut execution_config = ExecutionConfig::new(&functions, &variables); - if debug { - execution_config = execution_config.debug_attributes( - "_location".into(), - "_variable".into(), - "_match".into(), - ); - } - - let tree = parse_output.create_tree_cursor(); - let graph = msgb.execute(&tree, &execution_config, &NoCancellation)?; - - if output_json { - graph.display_json(None)?; - } else { - print!("{}", graph.pretty_print()); - } - - Ok(()) -} - -fn parse_graph_builder(msgb_path_string: &str) -> Result { - let msgb_path = PathBuf::from(&msgb_path_string) - .canonicalize() - .map_err(|_| CommandError::FileNotFound(msgb_path_string.to_string()))?; - - let msgb_source = fs::read_to_string(&msgb_path)?; - GraphBuilderFile::from_str(&msgb_source).map_err(|parser_error| { - let error_message = parser_error - .display_pretty(&msgb_path, &msgb_source) - .to_string(); - CommandError::ParseFailed(error_message) - }) -} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs index 19611de6cc..9427cfd313 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs @@ -3,7 +3,7 @@ use thiserror::Error; #[cfg(feature = "__experimental_bindings_api")] -pub mod build_graph; +pub mod bindings; pub mod parse; #[derive(Debug, Error)] @@ -19,8 +19,4 @@ pub enum CommandError { #[error("Parsing failed: {0}")] ParseFailed(String), - - #[cfg(feature = "__experimental_bindings_api")] - #[error(transparent)] - ExecutionFailed(#[from] crate::bindings::ExecutionError), } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs index a7a8f3e084..78852ab54d 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs @@ -23,27 +23,15 @@ pub enum Commands { json: bool, }, - // This is only intended for internal development + /// This is only intended for internal development #[cfg(feature = "__experimental_bindings_api")] - /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) - BuildGraph { + Bindings { /// File path to the source file to parse file_path: String, /// The language version to use for parsing #[arg(short, long)] version: Version, - - /// The graph buider (.msgb) file to use - msgb_path: String, - - /// Print the graph as JSON - #[clap(long)] - json: bool, - - /// Include debug info (location, variable and match) in the built graph - #[clap(long)] - debug: bool, }, } @@ -56,13 +44,9 @@ impl Commands { json, } => commands::parse::execute(&file_path, version, json), #[cfg(feature = "__experimental_bindings_api")] - Commands::BuildGraph { - file_path, - version, - msgb_path, - json, - debug, - } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + Commands::Bindings { file_path, version } => { + commands::bindings::execute(&file_path, version) + } }; match command_result { Ok(()) => ExitCode::SUCCESS, diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs index efb4c26f6a..8677536706 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs @@ -9,7 +9,7 @@ use slang_solidity::cli::Commands; // https://github.com/rust-lang/cargo/issues/1982 mod supress_api_dependencies { #[cfg(feature = "__experimental_bindings_api")] - use metaslang_graph_builder as _; + use metaslang_bindings as _; use { ariadne as _, metaslang_cst as _, semver as _, serde as _, serde_json as _, strum as _, strum_macros as _, thiserror as _, diff --git a/crates/solidity/outputs/cargo/tests/Cargo.toml b/crates/solidity/outputs/cargo/tests/Cargo.toml index d411646ff9..a00b4faa13 100644 --- a/crates/solidity/outputs/cargo/tests/Cargo.toml +++ b/crates/solidity/outputs/cargo/tests/Cargo.toml @@ -19,7 +19,7 @@ infra_utils = { workspace = true } once_cell = { workspace = true } regex = { workspace = true } semver = { workspace = true } -slang_solidity = { workspace = true, features = ["__private_ariadne"] } +slang_solidity = { workspace = true, features = ["__private_ariadne", "__experimental_bindings_api"] } solidity_language = { workspace = true } strum_macros = { workspace = true } diff --git a/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml b/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml index d2bb2f5cb5..639f3428fc 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml +++ b/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml @@ -13,8 +13,8 @@ infra_utils = { workspace = true } testlang_language = { workspace = true } [dependencies] +metaslang_bindings = { workspace = true, optional = true } metaslang_cst = { workspace = true } -metaslang_graph_builder = { workspace = true, optional = true } semver = { workspace = true } serde = { workspace = true } strum = { workspace = true } @@ -25,4 +25,4 @@ thiserror = { workspace = true } workspace = true [features] -__experimental_bindings_api = ["dep:metaslang_graph_builder"] +__experimental_bindings_api = ["dep:metaslang_bindings"] diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/bindings/mod.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/bindings/mod.rs index 47ccc571ee..78c2b74f82 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/bindings/mod.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/bindings/mod.rs @@ -3,10 +3,14 @@ #[path = "generated/binding_rules.rs"] mod binding_rules; -use metaslang_graph_builder::ast; -pub use metaslang_graph_builder::functions::Functions; -pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; +use metaslang_bindings; +use semver::Version; use crate::cst::KindTypes; -pub type File = ast::File; +pub type Bindings = metaslang_bindings::Bindings; +pub type Handle<'a> = metaslang_bindings::Handle<'a, KindTypes>; + +pub fn create(version: Version) -> Bindings { + Bindings::create(version, binding_rules::BINDING_RULES_SOURCE) +} diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/bindings.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/bindings.rs new file mode 100644 index 0000000000..56279e2b34 --- /dev/null +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/bindings.rs @@ -0,0 +1,88 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use core::fmt; + +use semver::Version; + +use super::CommandError; +use crate::bindings::{self, Bindings, Handle}; +use crate::cursor::Cursor; + +pub fn execute(file_path_string: &str, version: Version) -> Result<(), CommandError> { + let mut bindings = bindings::create(version.clone()); + let parse_output = super::parse::parse_source_file(file_path_string, version, |_| ())?; + let tree_cursor = parse_output.create_tree_cursor(); + + bindings.add_file(file_path_string, tree_cursor); + + print_definitions(&bindings); + print_references(&bindings); + + Ok(()) +} + +fn print_definitions(bindings: &Bindings) { + println!("\nAll definitions found:"); + for definition in bindings.all_definitions() { + println!("{}", DisplayDefinition(&definition)); + } +} + +fn print_references(bindings: &Bindings) { + println!("\nAll references found:"); + for reference in bindings.all_references() { + println!("{}", DisplayReference(&reference)); + if let Some(def) = reference.jump_to_definition() { + println!(" -> {}", DisplayDefinition(&def)); + } else { + println!(" -> No definition found"); + } + } +} + +struct DisplayRange<'a>(&'a Cursor); + +impl<'a> fmt::Display for DisplayRange<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let range = self.0.text_range(); + write!(f, "{}..{}", range.start, range.end) + } +} + +struct DisplayDefinition<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayDefinition<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` defined at {location} in {file}") + } else { + write!( + f, + "Definition without available cursor: {definition:?} in {file}", + definition = self.0 + ) + } + } +} + +struct DisplayReference<'a>(&'a Handle<'a>); + +impl<'a> fmt::Display for DisplayReference<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let file = self.0.get_file().unwrap_or(""); + if let Some(cursor) = self.0.get_cursor() { + let location = DisplayRange(&cursor); + let identifier = cursor.node().unparse(); + write!(f, "`{identifier}` referenced at {location} in {file}") + } else { + write!( + f, + "Reference without available cursor: {reference:?} in {file}", + reference = self.0 + ) + } + } +} diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs deleted file mode 100644 index 45799e4816..0000000000 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs +++ /dev/null @@ -1,59 +0,0 @@ -// This file is generated automatically by infrastructure scripts. Please don't edit by hand. - -use std::fs; -use std::path::PathBuf; - -use semver::Version; - -use super::parse::parse_source_file; -use super::CommandError; -use crate::bindings::{ - ExecutionConfig, File as GraphBuilderFile, Functions, NoCancellation, Variables, -}; - -pub fn execute( - file_path_string: &str, - version: Version, - msgb_path_string: &str, - output_json: bool, - debug: bool, -) -> Result<(), CommandError> { - let parse_output = parse_source_file(file_path_string, version, |_| ())?; - let msgb = parse_graph_builder(msgb_path_string)?; - - let functions = Functions::stdlib(); - let variables = Variables::new(); - let mut execution_config = ExecutionConfig::new(&functions, &variables); - if debug { - execution_config = execution_config.debug_attributes( - "_location".into(), - "_variable".into(), - "_match".into(), - ); - } - - let tree = parse_output.create_tree_cursor(); - let graph = msgb.execute(&tree, &execution_config, &NoCancellation)?; - - if output_json { - graph.display_json(None)?; - } else { - print!("{}", graph.pretty_print()); - } - - Ok(()) -} - -fn parse_graph_builder(msgb_path_string: &str) -> Result { - let msgb_path = PathBuf::from(&msgb_path_string) - .canonicalize() - .map_err(|_| CommandError::FileNotFound(msgb_path_string.to_string()))?; - - let msgb_source = fs::read_to_string(&msgb_path)?; - GraphBuilderFile::from_str(&msgb_source).map_err(|parser_error| { - let error_message = parser_error - .display_pretty(&msgb_path, &msgb_source) - .to_string(); - CommandError::ParseFailed(error_message) - }) -} diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs index 19611de6cc..9427cfd313 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs @@ -3,7 +3,7 @@ use thiserror::Error; #[cfg(feature = "__experimental_bindings_api")] -pub mod build_graph; +pub mod bindings; pub mod parse; #[derive(Debug, Error)] @@ -19,8 +19,4 @@ pub enum CommandError { #[error("Parsing failed: {0}")] ParseFailed(String), - - #[cfg(feature = "__experimental_bindings_api")] - #[error(transparent)] - ExecutionFailed(#[from] crate::bindings::ExecutionError), } diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs index a7a8f3e084..78852ab54d 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs @@ -23,27 +23,15 @@ pub enum Commands { json: bool, }, - // This is only intended for internal development + /// This is only intended for internal development #[cfg(feature = "__experimental_bindings_api")] - /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) - BuildGraph { + Bindings { /// File path to the source file to parse file_path: String, /// The language version to use for parsing #[arg(short, long)] version: Version, - - /// The graph buider (.msgb) file to use - msgb_path: String, - - /// Print the graph as JSON - #[clap(long)] - json: bool, - - /// Include debug info (location, variable and match) in the built graph - #[clap(long)] - debug: bool, }, } @@ -56,13 +44,9 @@ impl Commands { json, } => commands::parse::execute(&file_path, version, json), #[cfg(feature = "__experimental_bindings_api")] - Commands::BuildGraph { - file_path, - version, - msgb_path, - json, - debug, - } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + Commands::Bindings { file_path, version } => { + commands::bindings::execute(&file_path, version) + } }; match command_result { Ok(()) => ExitCode::SUCCESS, diff --git a/crates/testlang/outputs/cargo/tests/Cargo.toml b/crates/testlang/outputs/cargo/tests/Cargo.toml index 637d232b03..e014cc4c4d 100644 --- a/crates/testlang/outputs/cargo/tests/Cargo.toml +++ b/crates/testlang/outputs/cargo/tests/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true publish = false [dev-dependencies] +metaslang_graph_builder = { workspace = true } semver = { workspace = true } slang_testlang = { workspace = true, features = [ "__experimental_bindings_api", diff --git a/crates/testlang/outputs/cargo/tests/src/graph/mod.rs b/crates/testlang/outputs/cargo/tests/src/graph/mod.rs index 2e2b82f076..a8eaa45605 100644 --- a/crates/testlang/outputs/cargo/tests/src/graph/mod.rs +++ b/crates/testlang/outputs/cargo/tests/src/graph/mod.rs @@ -1,5 +1,7 @@ +use metaslang_graph_builder::ast::File; +use metaslang_graph_builder::functions::Functions; +use metaslang_graph_builder::{ExecutionConfig, NoCancellation, Variables}; use semver::Version; -use slang_testlang::bindings::{ExecutionConfig, File, Functions, NoCancellation, Variables}; use slang_testlang::kinds::NonterminalKind; use slang_testlang::language::Language;