Skip to content

Commit 033ad63

Browse files
authored
Add visualization of ASTs to CLI & REPL (#158)
* Add visualization of ASTs to CLI & REPL - Add actual arg parsing to CLI - Make REPL a sub-mode initiated by a CLI command - Add partial (lots of `todo!()`s) translation of AST into graphviz - Add FFI to graphviz to layout & render - Add `display` mode which prints image to console - Add docs for above to README
1 parent 62a2159 commit 033ad63

File tree

12 files changed

+1021
-122
lines changed

12 files changed

+1021
-122
lines changed

partiql-cli/Cargo.toml

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,53 @@ partiql-source-map = { path = "../partiql-source-map" }
3333
partiql-ast = { path = "../partiql-ast" }
3434

3535

36-
rustyline = "9.1.2"
37-
syntect = "5.0"
38-
owo-colors = "3.4.0"
39-
supports-color = "1.3.0"
40-
supports-unicode = "1.0.2"
41-
supports-hyperlinks = "1.2.0"
42-
termbg = "0.4.1"
43-
shellexpand = "2.1.0"
44-
partiql-parser = { path = "../partiql-parser" }
45-
partiql-source-map = { path = "../partiql-source-map" }
46-
partiql-ast = { path = "../partiql-ast" }
36+
rustyline = "10.*"
37+
syntect = "5.*"
38+
owo-colors = "3.*"
39+
supports-color = "1.*"
40+
supports-unicode = "1.*"
41+
supports-hyperlinks = "1.*"
42+
termbg = "0.4.*"
43+
shellexpand = "2.*"
44+
4745

46+
thiserror = "1.*"
47+
miette = { version ="5.*", features = ["fancy"] }
48+
clap = { version = "3.*", features = ["derive"] }
4849

49-
thiserror = "1.0.31"
50-
miette = { version ="4.7.1", features = ["fancy"] }
50+
# serde
51+
serde = { version ="1.*", features = ["derive"], optional = true }
52+
serde_json = { version ="1.*", optional = true }
5153

54+
### Dependencies for the `render` feature
55+
viuer = { version ="0.6.*", features = ["sixel"], optional = true }
56+
image = { version ="0.24.*", optional = true }
57+
graphviz-sys = { version ="0.1.3", optional = true }
58+
resvg = { version ="0.23.*", optional = true }
59+
usvg = { version ="0.23.*", optional = true }
60+
tiny-skia = { version ="0.6.*", optional = true }
61+
strum = { version ="0.24.*", features = ["derive"], optional = true }
62+
dot-writer = { version = "0.1.*", optional = true }
5263

53-
tui = "0.18.0"
54-
crossterm = "0.23.2"
64+
65+
66+
[features]
67+
default = []
68+
serde = [
69+
"dep:serde",
70+
"dep:serde_json",
71+
"partiql-parser/serde",
72+
"partiql-source-map/serde",
73+
"partiql-ast/serde",
74+
]
75+
visualize = [
76+
"serde",
77+
"dep:viuer",
78+
"dep:image",
79+
"dep:graphviz-sys",
80+
"dep:resvg",
81+
"dep:usvg",
82+
"dep:tiny-skia",
83+
"dep:strum",
84+
"dep:dot-writer",
85+
]

partiql-cli/README.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
1-
# PartiQL Rust REPL
1+
# PartiQL Rust CLI
2+
PoC for a CLI & REPL. It should be considered experimental, subject to change, etc.
23

3-
PoC for a REPL. It should be considered experimental, subject to change, etc.
4-
5-
In its current state, it largely exists to test parser interface & types from the perspective of an external application.
4+
In its current state, it largely exists to test parser interface & types from the perspective of an external application.
65
Probably the the mietter::Diagnostic stuff should be refactored and moved to the main parser crate.
76

8-
The REPL currently accepts no commands, assuming any/all input is a PartiQL query, which it will attempt to parse. Parse errors are pretty printed to the output.
7+
## CLI Commands
8+
9+
- **`help`** : print the CLI's help message and supported commands
10+
- **`repl`** : launches the [REPL](##REPL)
11+
- **`ast -T<format> "<query>"`**: outputs a rendered version of the parsed AST ([see Visualization](##Visualizations)):
12+
- **`<format>`**:
13+
- **`json`** : pretty-print to stdout in a json dump
14+
- **`dot`** : pretty-print to stdout in [Graphviz][Graphviz] [dot][GvDot] format
15+
- **`svg`** : print to stdout a [Graphviz][Graphviz] rendered svg xml document
16+
- **`png`** : print to stdout a [Graphviz][Graphviz] rendered png bitmap
17+
- **`display`** : display a [Graphviz][Graphviz] rendered png bitmap directly in supported terminals
18+
- **`query`** : the PartiQL query text
19+
20+
## REPL
21+
22+
The REPL currently assumes most of the input line is a PartiQL query, which it will attempt to parse.
23+
- For an invalid query, errors are pretty printed to the output.
24+
- For a valid query,
25+
- with no prefix, `Parse OK!` is printed to the output
26+
- if prefixed by `\ast`, a rendered AST tree image is printed to the output ([see Visualization](##Visualizations))
927

1028
Features:
1129
- Syntax highlighting of query input
1230
- User-friendly error reporting
1331
- Readling/editing
1432
- `CTRL-D`/`CTRL-C` to quit.
1533

34+
# Visualizations
35+
36+
In order to use any of the [Graphviz][Graphviz]-based visualizations, you will need the graphviz libraries
37+
installed on your machine (e.g. `brew install graphviz` or similar).
38+
1639
# TODO
1740

41+
See [REPL-tagged issues](https://github.com/partiql/partiql-lang-rust/issues?q=is%3Aissue+is%3Aopen+%5BREPL%5D)
42+
1843
- Use central location for syntax files rather than embedded in this crate
19-
- Add github issue link for Internal Compiler Errors
2044
- Better interaction model
2145
- commands
2246
- more robust editing
23-
- etc.
47+
- etc.
48+
49+
50+
[Graphviz]: https://graphviz.org/
51+
[GvDot]: https://graphviz.org/doc/info/lang.html

partiql-cli/src/args.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use clap::{ArgEnum, Parser, Subcommand};
2+
3+
#[derive(Parser)]
4+
#[clap(author, version, about, long_about = None)]
5+
pub struct Args {
6+
#[clap(subcommand)]
7+
pub command: Commands,
8+
}
9+
10+
#[derive(Subcommand)]
11+
pub enum Commands {
12+
#[cfg(feature = "visualize")]
13+
/// Dump the AST for a query
14+
Ast {
15+
#[clap(short = 'T', long = "format", value_enum)]
16+
format: Format,
17+
18+
/// Query to parse
19+
#[clap(value_parser)]
20+
query: String,
21+
},
22+
/// interactive REPL (Read Eval Print Loop) shell
23+
Repl,
24+
}
25+
26+
#[derive(ArgEnum, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
27+
pub enum Format {
28+
/// JSON
29+
Json,
30+
/// Graphviz dot
31+
Dot,
32+
/// Graphviz svg output
33+
Svg,
34+
/// Graphviz svg rendered to png
35+
Png,
36+
/// Display rendered output
37+
Display,
38+
}

partiql-cli/src/error.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use miette::{Diagnostic, LabeledSpan, SourceCode};
2+
use partiql_parser::{ParseError, ParserError};
3+
use partiql_source_map::location::{BytePosition, Location};
4+
5+
use thiserror::Error;
6+
7+
#[derive(Debug, Error, Diagnostic)]
8+
#[error("Error for query `{query}`")]
9+
pub struct CLIErrors {
10+
query: String,
11+
#[related]
12+
related: Vec<CLIError>,
13+
}
14+
15+
impl CLIErrors {
16+
pub fn from_parser_error(err: ParserError) -> Self {
17+
let query = err.text.to_string();
18+
19+
let related = err
20+
.errors
21+
.into_iter()
22+
.map(|e| CLIError::from_parse_error(e, &query))
23+
.collect();
24+
CLIErrors { query, related }
25+
}
26+
}
27+
28+
#[derive(Debug, Error)]
29+
pub enum CLIError {
30+
#[error("PartiQL syntax error:")]
31+
SyntaxError {
32+
src: String,
33+
msg: String,
34+
loc: Location<BytePosition>,
35+
},
36+
#[error("Internal Compiler Error - please report this (https://github.com/partiql/partiql-lang-rust/issues).")]
37+
InternalCompilerError { src: String },
38+
}
39+
40+
impl Diagnostic for CLIError {
41+
fn source_code(&self) -> Option<&dyn SourceCode> {
42+
match self {
43+
CLIError::SyntaxError { src, .. } => Some(src),
44+
CLIError::InternalCompilerError { src, .. } => Some(src),
45+
}
46+
}
47+
48+
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
49+
match self {
50+
CLIError::SyntaxError { msg, loc, .. } => {
51+
Some(Box::new(std::iter::once(LabeledSpan::new(
52+
Some(msg.to_string()),
53+
loc.start.0 .0 as usize,
54+
loc.end.0 .0 as usize - loc.start.0 .0 as usize,
55+
))))
56+
}
57+
CLIError::InternalCompilerError { .. } => None,
58+
}
59+
}
60+
}
61+
62+
impl CLIError {
63+
pub fn from_parse_error(err: ParseError, source: &str) -> Self {
64+
match err {
65+
ParseError::SyntaxError(partiql_source_map::location::Located { inner, location }) => {
66+
CLIError::SyntaxError {
67+
src: source.to_string(),
68+
msg: format!("Syntax error `{}`", inner),
69+
loc: location,
70+
}
71+
}
72+
ParseError::UnexpectedToken(partiql_source_map::location::Located {
73+
inner,
74+
location,
75+
}) => CLIError::SyntaxError {
76+
src: source.to_string(),
77+
msg: format!("Unexpected token `{}`", inner.token),
78+
loc: location,
79+
},
80+
ParseError::LexicalError(partiql_source_map::location::Located { inner, location }) => {
81+
CLIError::SyntaxError {
82+
src: source.to_string(),
83+
msg: format!("Lexical error `{}`", inner),
84+
loc: location,
85+
}
86+
}
87+
ParseError::Unknown(location) => CLIError::SyntaxError {
88+
src: source.to_string(),
89+
msg: "Unknown parser error".to_string(),
90+
loc: Location {
91+
start: location,
92+
end: location,
93+
},
94+
},
95+
ParseError::IllegalState(_location) => CLIError::InternalCompilerError {
96+
src: source.to_string(),
97+
},
98+
_ => {
99+
todo!("Not yet handled {:?}", err);
100+
}
101+
}
102+
}
103+
}

partiql-cli/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub mod args;
2+
3+
pub mod error;
4+
pub mod repl;
5+
6+
#[cfg(feature = "visualize")]
7+
pub mod visualize;

partiql-cli/src/main.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![deny(rustdoc::broken_intra_doc_links)]
2+
3+
use clap::Parser;
4+
use partiql_cli::error::CLIErrors;
5+
use partiql_cli::{args, repl};
6+
7+
use partiql_parser::Parsed;
8+
9+
#[allow(dead_code)]
10+
fn parse(query: &str) -> Result<Parsed, CLIErrors> {
11+
partiql_parser::Parser::default()
12+
.parse(query)
13+
.map_err(CLIErrors::from_parser_error)
14+
}
15+
16+
fn main() -> miette::Result<()> {
17+
let args = args::Args::parse();
18+
19+
match &args.command {
20+
args::Commands::Repl => repl::repl(),
21+
22+
#[cfg(feature = "visualize")]
23+
args::Commands::Ast { format, query } => {
24+
use partiql_cli::args::Format;
25+
use partiql_cli::visualize::render::{display, to_dot, to_json, to_png, to_svg};
26+
use std::io::Write;
27+
28+
let parsed = parse(&query)?;
29+
match format {
30+
Format::Json => println!("{}", to_json(&parsed.ast)),
31+
Format::Dot => println!("{}", to_dot(&parsed.ast)),
32+
Format::Svg => println!("{}", to_svg(&parsed.ast)),
33+
Format::Png => {
34+
std::io::stdout()
35+
.write(&to_png(&parsed.ast))
36+
.expect("png write");
37+
}
38+
Format::Display => display(&parsed.ast),
39+
}
40+
41+
Ok(())
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)