From ea3141741ef3e491b2125f0d24d5db58c2f5d600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= Date: Mon, 10 Jun 2024 13:28:58 -0400 Subject: [PATCH] Add command to build a graph from a CST and extract CLI commands into the runtime library (#983) Closes #981 1. Exposes the `metaslang_graph_builder` functionality in the generated languages runtime, parameterized with the language `KindTypes`. 2. Exercise the graph builder via a test in `testlang`. 3. Extracts the CLI command from `slang_solidity` into the runtime crate to be able to easily reuse it from other generated languages. 4. Adds a new `build-graph` command to the common CLI commands to run a source file through an arbitrary `.msgb` file and build a graph. 5. Expose the language's root non-terminal kind in the public API. Items 1, 2 and 4 are only available when building with the private feature flag `__graph_builder`. --- .changeset/forty-deers-arrive.md | 5 ++ Cargo.lock | 4 + crates/codegen/runtime/cargo/Cargo.toml | 5 ++ .../src/runtime/cli/commands/build_graph.rs | 57 ++++++++++++++ .../cargo/src/runtime/cli/commands/mod.rs | 24 ++++++ .../cargo/src/runtime/cli/commands/parse.rs | 52 +++++++++++++ .../runtime/cargo/src/runtime/cli/mod.rs | 73 ++++++++++++++++++ .../cargo/src/runtime/generated/language.rs | 11 +++ .../cargo/src/runtime/language.rs.jinja2 | 7 ++ .../codegen/runtime/cargo/src/runtime/mod.rs | 14 ++++ crates/codegen/runtime/generator/src/kinds.rs | 8 +- .../napi-bindings/generated/index.d.ts | 1 + .../cli/src/commands/publish/cargo/mod.rs | 1 + .../outputs/cargo/slang_solidity/Cargo.toml | 6 +- .../src/generated/cli/commands/build_graph.rs | 59 +++++++++++++++ .../src/generated/cli/commands/mod.rs | 26 +++++++ .../src/generated/cli/commands/parse.rs | 54 +++++++++++++ .../slang_solidity/src/generated/cli/mod.rs | 75 +++++++++++++++++++ .../src/generated/generated/language.rs | 11 +++ .../cargo/slang_solidity/src/generated/mod.rs | 14 ++++ .../outputs/cargo/slang_solidity/src/lib.rs | 2 +- .../outputs/cargo/slang_solidity/src/main.rs | 71 ++---------------- .../napi-bindings/generated/index.d.ts | 1 + .../outputs/cargo/slang_testlang/Cargo.toml | 4 + .../src/generated/cli/commands/build_graph.rs | 59 +++++++++++++++ .../src/generated/cli/commands/mod.rs | 26 +++++++ .../src/generated/cli/commands/parse.rs | 54 +++++++++++++ .../slang_testlang/src/generated/cli/mod.rs | 75 +++++++++++++++++++ .../src/generated/generated/language.rs | 11 +++ .../cargo/slang_testlang/src/generated/mod.rs | 14 ++++ .../testlang/outputs/cargo/tests/Cargo.toml | 2 +- .../outputs/cargo/tests/src/graph/mod.rs | 52 +++++++++++++ .../testlang/outputs/cargo/tests/src/lib.rs | 1 + .../napi-bindings/generated/index.d.ts | 1 + .../outputs/npm/tests/src/tests/public-api.ts | 5 ++ 35 files changed, 817 insertions(+), 68 deletions(-) create mode 100644 .changeset/forty-deers-arrive.md create mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs create mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs create mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs create mode 100644 crates/codegen/runtime/cargo/src/runtime/cli/mod.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs create mode 100644 crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs create mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs create mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs create mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs create mode 100644 crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs create mode 100644 crates/testlang/outputs/cargo/tests/src/graph/mod.rs diff --git a/.changeset/forty-deers-arrive.md b/.changeset/forty-deers-arrive.md new file mode 100644 index 0000000000..b7c5bb824c --- /dev/null +++ b/.changeset/forty-deers-arrive.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/slang": patch +--- + +Expose the language root non-terminal kind at `Language.rootKind()`. diff --git a/Cargo.lock b/Cargo.lock index 5ecf8e325a..11f50a7246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,8 +386,10 @@ version = "0.14.2" dependencies = [ "anyhow", "ariadne", + "clap", "codegen_runtime_generator", "metaslang_cst", + "metaslang_graph_builder", "napi", "napi-derive", "semver", @@ -1935,6 +1937,7 @@ dependencies = [ "codegen_runtime_generator", "infra_utils", "metaslang_cst", + "metaslang_graph_builder", "semver", "serde", "serde_json", @@ -1968,6 +1971,7 @@ dependencies = [ "codegen_runtime_generator", "infra_utils", "metaslang_cst", + "metaslang_graph_builder", "semver", "serde", "strum", diff --git a/crates/codegen/runtime/cargo/Cargo.toml b/crates/codegen/runtime/cargo/Cargo.toml index 9b3caf8551..e5c70d069c 100644 --- a/crates/codegen/runtime/cargo/Cargo.toml +++ b/crates/codegen/runtime/cargo/Cargo.toml @@ -13,7 +13,9 @@ codegen_runtime_generator = { workspace = true } [dependencies] ariadne = { workspace = true, optional = true } +clap = { 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 } @@ -28,8 +30,11 @@ thiserror = { workspace = true } [features] default = ["slang_napi_interfaces"] slang_napi_interfaces = ["dep:napi", "dep:napi-derive", "dep:serde_json"] +cli = ["dep:clap", "dep:serde_json", "__private_ariadne"] # Only used by the `slang_solidity` CLI __private_ariadne = ["dep:ariadne"] +# For internal development only +__graph_builder = ["dep:metaslang_graph_builder"] [lints] workspace = true 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 new file mode 100644 index 0000000000..72e5eebdc9 --- /dev/null +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/build_graph.rs @@ -0,0 +1,57 @@ +use std::fs; +use std::path::PathBuf; + +use semver::Version; + +use super::parse::parse_source_file; +use super::CommandError; +use crate::graph_builder::{ + 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 new file mode 100644 index 0000000000..b193c50225 --- /dev/null +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/mod.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +#[cfg(feature = "__graph_builder")] +pub mod build_graph; +pub mod parse; + +#[derive(Debug, Error)] +pub enum CommandError { + #[error("File not found: {0:?}")] + FileNotFound(String), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + LanguageError(#[from] crate::language::Error), + + #[error("Parsing failed: {0}")] + ParseFailed(String), + + #[cfg(feature = "__graph_builder")] + #[error(transparent)] + ExecutionFailed(#[from] crate::graph_builder::ExecutionError), +} diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs b/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs new file mode 100644 index 0000000000..496d3dbe55 --- /dev/null +++ b/crates/codegen/runtime/cargo/src/runtime/cli/commands/parse.rs @@ -0,0 +1,52 @@ +use std::fs; +use std::path::PathBuf; + +use semver::Version; + +use super::CommandError; +use crate::diagnostic; +use crate::language::Language; +use crate::parse_output::ParseOutput; + +pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<(), CommandError> { + parse_source_file(file_path_string, version, |output| { + if json { + let root_node = output.tree(); + let json = serde_json::to_string_pretty(&root_node).expect("JSON serialization failed"); + println!("{json}"); + } + }) + .map(|_| ()) +} + +pub(crate) fn parse_source_file( + file_path_string: &str, + version: Version, + run_before_checking: F, +) -> Result +where + F: FnOnce(&ParseOutput), +{ + let file_path = PathBuf::from(&file_path_string) + .canonicalize() + .map_err(|_| CommandError::FileNotFound(file_path_string.to_string()))?; + + let input = fs::read_to_string(file_path)?; + let language = Language::new(version)?; + let parse_output = language.parse(Language::ROOT_KIND, &input); + + run_before_checking(&parse_output); + + if parse_output.is_valid() { + Ok(parse_output) + } else { + const COLOR: bool = true; + let report = parse_output + .errors() + .iter() + .map(|error| diagnostic::render(error, file_path_string, &input, COLOR)) + .collect::>() + .join("\n"); + Err(CommandError::ParseFailed(report)) + } +} diff --git a/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs b/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs new file mode 100644 index 0000000000..b4455eb73b --- /dev/null +++ b/crates/codegen/runtime/cargo/src/runtime/cli/mod.rs @@ -0,0 +1,73 @@ +use std::process::ExitCode; + +use clap::Subcommand; +use semver::Version; + +pub mod commands; + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Parses a source file, and outputs any syntax errors, or a JSON concrete syntax tree + Parse { + /// File path to the source file to parse + file_path: String, + + /// The language version to use for parsing + #[arg(short, long)] + version: Version, + + /// Print the concrete syntax tree as JSON + #[clap(long)] + json: bool, + }, + + // This is only intended for internal development + #[cfg(feature = "__graph_builder")] + /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) + BuildGraph { + /// 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, + }, +} + +impl Commands { + pub fn execute(self) -> ExitCode { + let command_result = match self { + Commands::Parse { + file_path, + version, + json, + } => commands::parse::execute(&file_path, version, json), + #[cfg(feature = "__graph_builder")] + Commands::BuildGraph { + file_path, + version, + msgb_path, + json, + debug, + } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + }; + match command_result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } + } +} diff --git a/crates/codegen/runtime/cargo/src/runtime/generated/language.rs b/crates/codegen/runtime/cargo/src/runtime/generated/language.rs index 4a30b84464..82eec83340 100644 --- a/crates/codegen/runtime/cargo/src/runtime/generated/language.rs +++ b/crates/codegen/runtime/cargo/src/runtime/generated/language.rs @@ -54,6 +54,8 @@ impl From for napi::Error { impl Language { pub const SUPPORTED_VERSIONS: &'static [Version] = &[]; + pub const ROOT_KIND: NonterminalKind = NonterminalKind::Stub1; + pub fn new(version: Version) -> std::result::Result { if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { Ok(Self { version }) @@ -117,6 +119,15 @@ impl Language { .collect(); } + #[napi( + js_name = "rootKind", + ts_return_type = "kinds.NonterminalKind", + catch_unwind + )] + pub fn root_kind_napi() -> NonterminalKind { + Self::ROOT_KIND + } + #[napi( js_name = "parse", ts_return_type = "parse_output.ParseOutput", diff --git a/crates/codegen/runtime/cargo/src/runtime/language.rs.jinja2 b/crates/codegen/runtime/cargo/src/runtime/language.rs.jinja2 index 34d5c64cbf..8cac2cbad7 100644 --- a/crates/codegen/runtime/cargo/src/runtime/language.rs.jinja2 +++ b/crates/codegen/runtime/cargo/src/runtime/language.rs.jinja2 @@ -67,6 +67,8 @@ impl Language { {%- endif -%} ]; + pub const ROOT_KIND: NonterminalKind = NonterminalKind::{{ model.kinds.root_kind }}; + pub fn new(version: Version) -> std::result::Result { if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { Ok(Self { @@ -295,6 +297,11 @@ impl Language { return Self::SUPPORTED_VERSIONS.iter().map(|v| v.to_string()).collect(); } + #[napi(js_name = "rootKind", ts_return_type = "kinds.NonterminalKind", catch_unwind)] + pub fn root_kind_napi() -> NonterminalKind { + Self::ROOT_KIND + } + #[napi(js_name = "parse", ts_return_type = "parse_output.ParseOutput", catch_unwind)] pub fn parse_napi( &self, diff --git a/crates/codegen/runtime/cargo/src/runtime/mod.rs b/crates/codegen/runtime/cargo/src/runtime/mod.rs index 10c77e6ca3..020618ad6e 100644 --- a/crates/codegen/runtime/cargo/src/runtime/mod.rs +++ b/crates/codegen/runtime/cargo/src/runtime/mod.rs @@ -13,6 +13,9 @@ pub mod parse_output; #[cfg(feature = "slang_napi_interfaces")] pub mod napi_interface; +#[cfg(feature = "cli")] +pub mod cli; + mod metaslang_cst { #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] // These derives are because default #[derive(...)] on a generic type implements only the trait @@ -69,3 +72,14 @@ pub mod text_index { use metaslang_cst::text_index; pub use text_index::{TextIndex, TextRange, TextRangeExtensions}; } + +#[cfg(feature = "__graph_builder")] +pub mod graph_builder { + use metaslang_graph_builder::ast; + pub use metaslang_graph_builder::functions::Functions; + pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; + + use super::metaslang_cst::KindTypes; + + pub type File = ast::File; +} diff --git a/crates/codegen/runtime/generator/src/kinds.rs b/crates/codegen/runtime/generator/src/kinds.rs index 8b53dd95e4..4d17bbcb90 100644 --- a/crates/codegen/runtime/generator/src/kinds.rs +++ b/crates/codegen/runtime/generator/src/kinds.rs @@ -14,10 +14,12 @@ pub struct KindsModel { trivia_scanner_names: BTreeSet, /// Defines `EdgeLabel` enum variants. labels: BTreeSet, - /// Built-in labels for edges + /// Built-in labels for edges. built_in_labels: &'static [&'static str], // Defines the `LexicalContext(Type)` enum and type-level variants. lexical_contexts: BTreeSet, + /// Defines the root `NonterminalKind` for a source file of the language. + root_kind: Identifier, } impl Default for KindsModel { @@ -29,6 +31,7 @@ impl Default for KindsModel { labels: BTreeSet::default(), built_in_labels: BuiltInLabel::VARIANTS, lexical_contexts: BTreeSet::default(), + root_kind: Identifier::from("Stub1"), } } } @@ -103,12 +106,15 @@ impl KindsModel { .chain(std::iter::once(Identifier::from("Default"))) .collect(); + let root_kind = language.root_item.clone(); + KindsModel { nonterminal_kinds, terminal_kinds, trivia_scanner_names, labels, lexical_contexts, + root_kind, ..Self::default() } } diff --git a/crates/codegen/runtime/npm/src/runtime/napi-bindings/generated/index.d.ts b/crates/codegen/runtime/npm/src/runtime/napi-bindings/generated/index.d.ts index 7555aa036e..9975939386 100644 --- a/crates/codegen/runtime/npm/src/runtime/napi-bindings/generated/index.d.ts +++ b/crates/codegen/runtime/npm/src/runtime/napi-bindings/generated/index.d.ts @@ -39,6 +39,7 @@ export namespace language { constructor(version: string); get version(): string; static supportedVersions(): Array; + static rootKind(): kinds.NonterminalKind; parse(kind: kinds.NonterminalKind, input: string): parse_output.ParseOutput; } } diff --git a/crates/infra/cli/src/commands/publish/cargo/mod.rs b/crates/infra/cli/src/commands/publish/cargo/mod.rs index ff10d57c46..803225f186 100644 --- a/crates/infra/cli/src/commands/publish/cargo/mod.rs +++ b/crates/infra/cli/src/commands/publish/cargo/mod.rs @@ -14,6 +14,7 @@ use crate::commands::publish::DryRun; const USER_FACING_CRATES: &[&str] = &[ // Sorted by dependency order (from dependencies to dependents): "metaslang_cst", + "metaslang_graph_builder", "slang_solidity", ]; diff --git a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml index e85f2182b6..5ff31c184b 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml +++ b/crates/solidity/outputs/cargo/slang_solidity/Cargo.toml @@ -33,9 +33,11 @@ required-features = ["cli"] [features] default = ["cli"] -cli = ["dep:anyhow", "dep:clap", "dep:serde_json", "__private_ariadne"] +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 +__graph_builder = ["dep:metaslang_graph_builder"] [build-dependencies] # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ anyhow = { workspace = true } # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ @@ -44,10 +46,10 @@ infra_utils = { workspace = true } # __REMOVE_THIS_LINE_DURING_CAR solidity_language = { workspace = true } # __REMOVE_THIS_LINE_DURING_CARGO_PUBLISH__ [dependencies] -anyhow = { workspace = true, optional = true } ariadne = { workspace = true, optional = true } clap = { 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/cli/commands/build_graph.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs new file mode 100644 index 0000000000..4dd1a29667 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/build_graph.rs @@ -0,0 +1,59 @@ +// 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::graph_builder::{ + 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 new file mode 100644 index 0000000000..82afb2ffe2 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/mod.rs @@ -0,0 +1,26 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use thiserror::Error; + +#[cfg(feature = "__graph_builder")] +pub mod build_graph; +pub mod parse; + +#[derive(Debug, Error)] +pub enum CommandError { + #[error("File not found: {0:?}")] + FileNotFound(String), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + LanguageError(#[from] crate::language::Error), + + #[error("Parsing failed: {0}")] + ParseFailed(String), + + #[cfg(feature = "__graph_builder")] + #[error(transparent)] + ExecutionFailed(#[from] crate::graph_builder::ExecutionError), +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs new file mode 100644 index 0000000000..c2ed11a906 --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/commands/parse.rs @@ -0,0 +1,54 @@ +// 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::CommandError; +use crate::diagnostic; +use crate::language::Language; +use crate::parse_output::ParseOutput; + +pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<(), CommandError> { + parse_source_file(file_path_string, version, |output| { + if json { + let root_node = output.tree(); + let json = serde_json::to_string_pretty(&root_node).expect("JSON serialization failed"); + println!("{json}"); + } + }) + .map(|_| ()) +} + +pub(crate) fn parse_source_file( + file_path_string: &str, + version: Version, + run_before_checking: F, +) -> Result +where + F: FnOnce(&ParseOutput), +{ + let file_path = PathBuf::from(&file_path_string) + .canonicalize() + .map_err(|_| CommandError::FileNotFound(file_path_string.to_string()))?; + + let input = fs::read_to_string(file_path)?; + let language = Language::new(version)?; + let parse_output = language.parse(Language::ROOT_KIND, &input); + + run_before_checking(&parse_output); + + if parse_output.is_valid() { + Ok(parse_output) + } else { + const COLOR: bool = true; + let report = parse_output + .errors() + .iter() + .map(|error| diagnostic::render(error, file_path_string, &input, COLOR)) + .collect::>() + .join("\n"); + Err(CommandError::ParseFailed(report)) + } +} 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 new file mode 100644 index 0000000000..1a81d87bbd --- /dev/null +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/cli/mod.rs @@ -0,0 +1,75 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::process::ExitCode; + +use clap::Subcommand; +use semver::Version; + +pub mod commands; + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Parses a source file, and outputs any syntax errors, or a JSON concrete syntax tree + Parse { + /// File path to the source file to parse + file_path: String, + + /// The language version to use for parsing + #[arg(short, long)] + version: Version, + + /// Print the concrete syntax tree as JSON + #[clap(long)] + json: bool, + }, + + // This is only intended for internal development + #[cfg(feature = "__graph_builder")] + /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) + BuildGraph { + /// 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, + }, +} + +impl Commands { + pub fn execute(self) -> ExitCode { + let command_result = match self { + Commands::Parse { + file_path, + version, + json, + } => commands::parse::execute(&file_path, version, json), + #[cfg(feature = "__graph_builder")] + Commands::BuildGraph { + file_path, + version, + msgb_path, + json, + debug, + } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + }; + match command_result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } + } +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/generated/language.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/generated/language.rs index c30451599b..32bda6e9a9 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/generated/language.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/generated/language.rs @@ -170,6 +170,8 @@ impl Language { Version::new(0, 8, 26), ]; + pub const ROOT_KIND: NonterminalKind = NonterminalKind::SourceUnit; + pub fn new(version: Version) -> std::result::Result { if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { Ok(Self { @@ -13527,6 +13529,15 @@ impl Language { .collect(); } + #[napi( + js_name = "rootKind", + ts_return_type = "kinds.NonterminalKind", + catch_unwind + )] + pub fn root_kind_napi() -> NonterminalKind { + Self::ROOT_KIND + } + #[napi( js_name = "parse", ts_return_type = "parse_output.ParseOutput", diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/generated/mod.rs b/crates/solidity/outputs/cargo/slang_solidity/src/generated/mod.rs index c444d0844e..fa1b927f10 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/generated/mod.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/generated/mod.rs @@ -15,6 +15,9 @@ pub mod parse_output; #[cfg(feature = "slang_napi_interfaces")] pub mod napi_interface; +#[cfg(feature = "cli")] +pub mod cli; + mod metaslang_cst { #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] // These derives are because default #[derive(...)] on a generic type implements only the trait @@ -71,3 +74,14 @@ pub mod text_index { use metaslang_cst::text_index; pub use text_index::{TextIndex, TextRange, TextRangeExtensions}; } + +#[cfg(feature = "__graph_builder")] +pub mod graph_builder { + use metaslang_graph_builder::ast; + pub use metaslang_graph_builder::functions::Functions; + pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; + + use super::metaslang_cst::KindTypes; + + pub type File = ast::File; +} diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs b/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs index 30b501784b..ff73e70f3d 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/lib.rs @@ -8,5 +8,5 @@ pub use generated::*; // https://github.com/rust-lang/cargo/issues/1982 #[cfg(feature = "cli")] mod supress_cli_dependencies { - use {anyhow as _, ariadne as _, clap as _, serde_json as _}; + use {ariadne as _, clap as _, serde_json as _}; } diff --git a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs index 17d9986a37..aab1370ce3 100644 --- a/crates/solidity/outputs/cargo/slang_solidity/src/main.rs +++ b/crates/solidity/outputs/cargo/slang_solidity/src/main.rs @@ -1,20 +1,18 @@ -use std::fs; -use std::path::PathBuf; use std::process::ExitCode; -use anyhow::{Context, Result}; -use clap::{Parser as ClapParser, Subcommand}; -use semver::Version; -use slang_solidity::kinds::NonterminalKind; -use slang_solidity::language::Language; +use clap::Parser as ClapParser; +use slang_solidity::cli::Commands; // Below are dependencies used by the API `lib.rs`, but not the CLI "main.rs". // However, we need to add a fake usage to suppress Cargo warnings about unused dependencies. // This is a known issue, and we should remove this hack once there is a better solution from Cargo. // https://github.com/rust-lang/cargo/issues/1982 mod supress_api_dependencies { + #[cfg(feature = "__graph_builder")] + use metaslang_graph_builder as _; use { - ariadne as _, metaslang_cst as _, serde as _, strum as _, strum_macros as _, thiserror as _, + ariadne as _, metaslang_cst as _, semver as _, serde as _, serde_json as _, strum as _, + strum_macros as _, thiserror as _, }; } @@ -26,61 +24,8 @@ struct Cli { command: Commands, } -#[derive(Subcommand, Debug)] -enum Commands { - /// Parses a Solidity (*.sol) source file, and outputs any syntax errors, or a JSON concrete syntax tree - Parse { - /// File path to the Solidity (*.sol) source file to parse - file_path: String, - - /// The Solidity language version to use for parsing - #[arg(short, long)] - version: Version, - - /// Print the concrete syntax tree as JSON - #[clap(long)] - json: bool, - }, -} - -fn main() -> Result { - match Cli::parse().command { - Commands::Parse { - file_path, - version, - json, - } => execute_parse_command(&file_path, version, json), - } -} - -fn execute_parse_command(file_path_string: &str, version: Version, json: bool) -> Result { - let file_path = PathBuf::from(&file_path_string) - .canonicalize() - .with_context(|| format!("Failed to find file path: {file_path_string:?}"))?; - - let input = fs::read_to_string(file_path)?; - let language = Language::new(version)?; - let output = language.parse(NonterminalKind::SourceUnit, &input); - - let errors = output.errors(); - for error in errors { - const COLOR: bool = true; - let report = slang_solidity::diagnostic::render(error, file_path_string, &input, COLOR); - eprintln!("{report}"); - } - - if json { - let root_node = output.tree(); - let json = serde_json::to_string_pretty(&root_node)?; - println!("{json}"); - } - - if errors.is_empty() { - Ok(ExitCode::SUCCESS) - } else { - eprintln!("Couldn't parse the Solidity source file."); - Ok(ExitCode::FAILURE) - } +fn main() -> ExitCode { + Cli::parse().command.execute() } #[test] diff --git a/crates/solidity/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts b/crates/solidity/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts index 8be5db76c3..da36b11a3b 100644 --- a/crates/solidity/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts +++ b/crates/solidity/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts @@ -735,6 +735,7 @@ export namespace language { constructor(version: string); get version(): string; static supportedVersions(): Array; + static rootKind(): kinds.NonterminalKind; parse(kind: kinds.NonterminalKind, input: string): parse_output.ParseOutput; } } diff --git a/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml b/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml index 49c89cf9eb..3fa620ccda 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml +++ b/crates/testlang/outputs/cargo/slang_testlang/Cargo.toml @@ -14,6 +14,7 @@ testlang_language = { workspace = true } [dependencies] metaslang_cst = { workspace = true } +metaslang_graph_builder = { workspace = true, optional = true } semver = { workspace = true } serde = { workspace = true } strum = { workspace = true } @@ -22,3 +23,6 @@ thiserror = { workspace = true } [lints] workspace = true + +[features] +__graph_builder = ["dep:metaslang_graph_builder"] 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 new file mode 100644 index 0000000000..4dd1a29667 --- /dev/null +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/build_graph.rs @@ -0,0 +1,59 @@ +// 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::graph_builder::{ + 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 new file mode 100644 index 0000000000..82afb2ffe2 --- /dev/null +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/mod.rs @@ -0,0 +1,26 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use thiserror::Error; + +#[cfg(feature = "__graph_builder")] +pub mod build_graph; +pub mod parse; + +#[derive(Debug, Error)] +pub enum CommandError { + #[error("File not found: {0:?}")] + FileNotFound(String), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + LanguageError(#[from] crate::language::Error), + + #[error("Parsing failed: {0}")] + ParseFailed(String), + + #[cfg(feature = "__graph_builder")] + #[error(transparent)] + ExecutionFailed(#[from] crate::graph_builder::ExecutionError), +} diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs new file mode 100644 index 0000000000..c2ed11a906 --- /dev/null +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/commands/parse.rs @@ -0,0 +1,54 @@ +// 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::CommandError; +use crate::diagnostic; +use crate::language::Language; +use crate::parse_output::ParseOutput; + +pub fn execute(file_path_string: &str, version: Version, json: bool) -> Result<(), CommandError> { + parse_source_file(file_path_string, version, |output| { + if json { + let root_node = output.tree(); + let json = serde_json::to_string_pretty(&root_node).expect("JSON serialization failed"); + println!("{json}"); + } + }) + .map(|_| ()) +} + +pub(crate) fn parse_source_file( + file_path_string: &str, + version: Version, + run_before_checking: F, +) -> Result +where + F: FnOnce(&ParseOutput), +{ + let file_path = PathBuf::from(&file_path_string) + .canonicalize() + .map_err(|_| CommandError::FileNotFound(file_path_string.to_string()))?; + + let input = fs::read_to_string(file_path)?; + let language = Language::new(version)?; + let parse_output = language.parse(Language::ROOT_KIND, &input); + + run_before_checking(&parse_output); + + if parse_output.is_valid() { + Ok(parse_output) + } else { + const COLOR: bool = true; + let report = parse_output + .errors() + .iter() + .map(|error| diagnostic::render(error, file_path_string, &input, COLOR)) + .collect::>() + .join("\n"); + Err(CommandError::ParseFailed(report)) + } +} 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 new file mode 100644 index 0000000000..1a81d87bbd --- /dev/null +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/cli/mod.rs @@ -0,0 +1,75 @@ +// This file is generated automatically by infrastructure scripts. Please don't edit by hand. + +use std::process::ExitCode; + +use clap::Subcommand; +use semver::Version; + +pub mod commands; + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Parses a source file, and outputs any syntax errors, or a JSON concrete syntax tree + Parse { + /// File path to the source file to parse + file_path: String, + + /// The language version to use for parsing + #[arg(short, long)] + version: Version, + + /// Print the concrete syntax tree as JSON + #[clap(long)] + json: bool, + }, + + // This is only intended for internal development + #[cfg(feature = "__graph_builder")] + /// Parses a source file and builds a graph executing the instructions from the builder file (*.msgb) + BuildGraph { + /// 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, + }, +} + +impl Commands { + pub fn execute(self) -> ExitCode { + let command_result = match self { + Commands::Parse { + file_path, + version, + json, + } => commands::parse::execute(&file_path, version, json), + #[cfg(feature = "__graph_builder")] + Commands::BuildGraph { + file_path, + version, + msgb_path, + json, + debug, + } => commands::build_graph::execute(&file_path, version, &msgb_path, json, debug), + }; + match command_result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } + } +} diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/generated/language.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/generated/language.rs index 1a2da1f0ef..7d72cffff8 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/generated/language.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/generated/language.rs @@ -61,6 +61,8 @@ impl Language { Version::new(1, 1, 1), ]; + pub const ROOT_KIND: NonterminalKind = NonterminalKind::SourceUnit; + pub fn new(version: Version) -> std::result::Result { if Self::SUPPORTED_VERSIONS.binary_search(&version).is_ok() { Ok(Self { @@ -846,6 +848,15 @@ impl Language { .collect(); } + #[napi( + js_name = "rootKind", + ts_return_type = "kinds.NonterminalKind", + catch_unwind + )] + pub fn root_kind_napi() -> NonterminalKind { + Self::ROOT_KIND + } + #[napi( js_name = "parse", ts_return_type = "parse_output.ParseOutput", diff --git a/crates/testlang/outputs/cargo/slang_testlang/src/generated/mod.rs b/crates/testlang/outputs/cargo/slang_testlang/src/generated/mod.rs index c444d0844e..fa1b927f10 100644 --- a/crates/testlang/outputs/cargo/slang_testlang/src/generated/mod.rs +++ b/crates/testlang/outputs/cargo/slang_testlang/src/generated/mod.rs @@ -15,6 +15,9 @@ pub mod parse_output; #[cfg(feature = "slang_napi_interfaces")] pub mod napi_interface; +#[cfg(feature = "cli")] +pub mod cli; + mod metaslang_cst { #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] // These derives are because default #[derive(...)] on a generic type implements only the trait @@ -71,3 +74,14 @@ pub mod text_index { use metaslang_cst::text_index; pub use text_index::{TextIndex, TextRange, TextRangeExtensions}; } + +#[cfg(feature = "__graph_builder")] +pub mod graph_builder { + use metaslang_graph_builder::ast; + pub use metaslang_graph_builder::functions::Functions; + pub use metaslang_graph_builder::{ExecutionConfig, ExecutionError, NoCancellation, Variables}; + + use super::metaslang_cst::KindTypes; + + pub type File = ast::File; +} diff --git a/crates/testlang/outputs/cargo/tests/Cargo.toml b/crates/testlang/outputs/cargo/tests/Cargo.toml index 5f56849b99..c09c26b8fc 100644 --- a/crates/testlang/outputs/cargo/tests/Cargo.toml +++ b/crates/testlang/outputs/cargo/tests/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dev-dependencies] semver = { workspace = true } -slang_testlang = { workspace = true } +slang_testlang = { workspace = true, features = ["__graph_builder"] } [lints] workspace = true diff --git a/crates/testlang/outputs/cargo/tests/src/graph/mod.rs b/crates/testlang/outputs/cargo/tests/src/graph/mod.rs new file mode 100644 index 0000000000..a1de41a3c3 --- /dev/null +++ b/crates/testlang/outputs/cargo/tests/src/graph/mod.rs @@ -0,0 +1,52 @@ +use semver::Version; +use slang_testlang::graph_builder::{ExecutionConfig, File, Functions, NoCancellation, Variables}; +use slang_testlang::kinds::NonterminalKind; +use slang_testlang::language::Language; + +#[test] +fn builds_a_graph() { + let version = Version::parse("1.0.0").unwrap(); + let language = Language::new(version).unwrap(); + + let msgb_source = r" + @tree [Tree] { + node @tree.def + attr (@tree.def) is_tree + } + + @tree_node [TreeNode] { + node @tree_node.def + } + + @tree [Tree ... @root node: [TreeNode] ...] { + edge @root.def -> @tree.def + } + + @parent [TreeNode ... members: [_ ... [_ @child variant: [TreeNode]] ...] ...] { + edge @child.def -> @parent.def + } + "; + + let msgb = File::from_str(msgb_source); + assert!(msgb.is_ok()); + + let mut msgb = msgb.unwrap(); + assert!(msgb.check().is_ok()); + + let source = "tree $t1 [A [B C]];"; + let parse_output = language.parse(NonterminalKind::SourceUnit, source); + + assert!(parse_output.is_valid()); + let tree = parse_output.create_tree_cursor(); + + let functions = Functions::stdlib(); + let variables = Variables::new(); + let config = ExecutionConfig::new(&functions, &variables); + + let graph = msgb.execute(&tree, &config, &NoCancellation).unwrap(); + + assert_eq!( + graph.pretty_print().to_string(), + "node 0\n is_tree: #true\nnode 1\nedge 1 -> 0\nnode 2\nedge 2 -> 1\n" + ); +} diff --git a/crates/testlang/outputs/cargo/tests/src/lib.rs b/crates/testlang/outputs/cargo/tests/src/lib.rs index 89c4951c81..81e01938e3 100644 --- a/crates/testlang/outputs/cargo/tests/src/lib.rs +++ b/crates/testlang/outputs/cargo/tests/src/lib.rs @@ -1,5 +1,6 @@ #![cfg(test)] mod errors; +mod graph; mod query; mod versions; diff --git a/crates/testlang/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts b/crates/testlang/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts index db83a9720f..5b0375ec8d 100644 --- a/crates/testlang/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts +++ b/crates/testlang/outputs/npm/package/src/generated/napi-bindings/generated/index.d.ts @@ -67,6 +67,7 @@ export namespace language { constructor(version: string); get version(): string; static supportedVersions(): Array; + static rootKind(): kinds.NonterminalKind; parse(kind: kinds.NonterminalKind, input: string): parse_output.ParseOutput; } } diff --git a/crates/testlang/outputs/npm/tests/src/tests/public-api.ts b/crates/testlang/outputs/npm/tests/src/tests/public-api.ts index aa1abab8f7..c05788059a 100644 --- a/crates/testlang/outputs/npm/tests/src/tests/public-api.ts +++ b/crates/testlang/outputs/npm/tests/src/tests/public-api.ts @@ -1,5 +1,6 @@ import * as slang from "@slang-private/slang-testlang"; import { NonterminalKind, TerminalKind } from "@slang-private/slang-testlang/kinds"; +import { Language } from "@slang-private/slang-testlang/language"; test("use namespace imports of the API", () => { expect(slang.kinds.NonterminalKind.SourceUnit).toEqual("SourceUnit"); @@ -12,3 +13,7 @@ test("use nested imports of the API", () => { expect(NonterminalKind.TreeNode).toEqual("TreeNode"); expect(TerminalKind.Identifier).toEqual("Identifier"); }); + +test("language exposes a root kind", () => { + expect(Language.rootKind()).toEqual(NonterminalKind.SourceUnit); +});