Skip to content

Commit e1764fe

Browse files
committed
Interactive parser development example writing.
1 parent cc783b2 commit e1764fe

19 files changed

+641
-0
lines changed

src/4_example/a_1_Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "pie"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
pie_graph = "0.0.1"
8+
9+
[dev-dependencies]
10+
dev_shared = { path = "../dev_shared" }
11+
assert_matches = "1"
12+
pest = "2"
13+
pest_meta = "2"
14+
pest_vm = "2"

src/4_example/a_2_main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
3+
}

src/4_example/a_3_main_parse_mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod parse;
2+
3+
fn main() {
4+
5+
}

src/4_example/a_4_grammar.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::collections::HashSet;
2+
use std::fmt::Write;
3+
4+
/// Parse programs with a compiled pest grammar.
5+
#[derive(Clone, Eq, PartialEq, Debug)]
6+
pub struct CompiledGrammar {
7+
rules: Vec<pest_meta::optimizer::OptimizedRule>,
8+
rule_names: HashSet<String>,
9+
}
10+
11+
impl CompiledGrammar {
12+
/// Compile the pest grammar from `grammar_text`, using `path` to annotate errors. Returns a [`Self`] instance.
13+
///
14+
/// # Errors
15+
///
16+
/// Returns `Err(error_string)` when compiling the grammar fails.
17+
pub fn new(grammar_text: &str, path: Option<&str>) -> Result<Self, String> {
18+
match pest_meta::parse_and_optimize(grammar_text) {
19+
Ok((builtin_rules, rules)) => {
20+
let mut rule_names = HashSet::with_capacity(builtin_rules.len() + rules.len());
21+
rule_names.extend(builtin_rules.iter().map(|s| s.to_string()));
22+
rule_names.extend(rules.iter().map(|s| s.name.clone()));
23+
Ok(Self { rules, rule_names })
24+
},
25+
Err(errors) => {
26+
let mut error_string = String::new();
27+
for mut error in errors {
28+
if let Some(path) = path.as_ref() {
29+
error = error.with_path(path);
30+
}
31+
error = error.renamed_rules(pest_meta::parser::rename_meta_rule);
32+
let _ = writeln!(error_string, "{}", error); // Ignore error: writing to String cannot fail.
33+
}
34+
Err(error_string)
35+
}
36+
}
37+
}
38+
}

src/4_example/a_5_parse.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::collections::HashSet;
2+
use std::fmt::Write;
3+
4+
/// Parse programs with a compiled pest grammar.
5+
#[derive(Clone, Eq, PartialEq, Debug)]
6+
pub struct CompiledGrammar {
7+
rules: Vec<pest_meta::optimizer::OptimizedRule>,
8+
rule_names: HashSet<String>,
9+
}
10+
11+
impl CompiledGrammar {
12+
/// Compile the pest grammar from `grammar_text`, using `path` to annotate errors. Returns a [`Self`] instance.
13+
///
14+
/// # Errors
15+
///
16+
/// Returns `Err(error_string)` when compiling the grammar fails.
17+
pub fn new(grammar_text: &str, path: Option<&str>) -> Result<Self, String> {
18+
match pest_meta::parse_and_optimize(grammar_text) {
19+
Ok((builtin_rules, rules)) => {
20+
let mut rule_names = HashSet::with_capacity(builtin_rules.len() + rules.len());
21+
rule_names.extend(builtin_rules.iter().map(|s| s.to_string()));
22+
rule_names.extend(rules.iter().map(|s| s.name.clone()));
23+
Ok(Self { rules, rule_names })
24+
},
25+
Err(errors) => {
26+
let mut error_string = String::new();
27+
for mut error in errors {
28+
if let Some(path) = path.as_ref() {
29+
error = error.with_path(path);
30+
}
31+
error = error.renamed_rules(pest_meta::parser::rename_meta_rule);
32+
let _ = writeln!(error_string, "{}", error); // Ignore error: writing to String cannot fail.
33+
}
34+
Err(error_string)
35+
}
36+
}
37+
}
38+
39+
/// Parse `program_text` with rule `rule_name` using this compiled grammar, using `path` to annotate errors. Returns
40+
/// parsed pairs formatted as a string.
41+
///
42+
/// # Errors
43+
///
44+
/// Returns `Err(error_string)` when parsing fails.
45+
pub fn parse(&self, program_text: &str, rule_name: &str, path: Option<&str>) -> Result<String, String> {
46+
if !self.rule_names.contains(rule_name) {
47+
let message = format!("rule '{}' was not found", rule_name);
48+
return Err(message);
49+
}
50+
// Note: can't store `Vm` in `CompiledGrammar` because `Vm` is not `Clone` nor `Eq`.
51+
let vm = pest_vm::Vm::new(self.rules.clone());
52+
match vm.parse(rule_name, program_text) {
53+
Ok(pairs) => Ok(format!("{}", pairs)),
54+
Err(mut error) => {
55+
if let Some(path) = path {
56+
error = error.with_path(path);
57+
}
58+
error = error.renamed_rules(|r| r.to_string());
59+
let error_string = format!("{}", error);
60+
Err(error_string)
61+
}
62+
}
63+
}
64+
}

src/4_example/a_6_test.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
#[cfg(test)]
3+
mod tests {
4+
use super::*;
5+
6+
#[test]
7+
fn test_compile_parse() -> Result<(), String> {
8+
// Grammar compilation failure.
9+
let result = CompiledGrammar::new("asd = { fgh } qwe = { rty }", None);
10+
assert!(result.is_err());
11+
println!("{}", result.unwrap_err());
12+
13+
// Grammar that parses numbers.
14+
let compiled_grammar = CompiledGrammar::new("num = { ASCII_DIGIT+ }", None)?;
15+
println!("{:?}", compiled_grammar);
16+
17+
// Parse failure
18+
let result = compiled_grammar.parse("a", "num", None);
19+
assert!(result.is_err());
20+
println!("{}", result.unwrap_err());
21+
// Parse failure due to non-existent rule.
22+
let result = compiled_grammar.parse("1", "asd", None);
23+
assert!(result.is_err());
24+
println!("{}", result.unwrap_err());
25+
// Parse success
26+
let result = compiled_grammar.parse("1", "num", None);
27+
assert!(result.is_ok());
28+
println!("{}", result.unwrap());
29+
30+
Ok(())
31+
}
32+
}

src/4_example/b_1_main_task_mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod parse;
2+
pub mod task;
3+
4+
fn main() {
5+
6+
}

src/4_example/b_2_tasks_outputs.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::io::Read;
2+
use std::path::{Path, PathBuf};
3+
4+
use pie::{Context, Task};
5+
6+
use crate::parse::CompiledGrammar;
7+
8+
/// Tasks for compiling a grammar and parsing files with it.
9+
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
10+
pub enum Tasks {
11+
CompileGrammar { grammar_file_path: PathBuf },
12+
Parse { compiled_grammar_task: Box<Tasks>, program_file_path: PathBuf, rule_name: String }
13+
}
14+
15+
impl Tasks {
16+
/// Create a [`Self::CompileGrammar`] task that compiles the grammar in file `grammar_file_path`.
17+
pub fn compile_grammar(grammar_file_path: impl Into<PathBuf>) -> Self {
18+
Self::CompileGrammar { grammar_file_path: grammar_file_path.into() }
19+
}
20+
21+
/// Create a [`Self::Parse`] task that uses the compiled grammar returned by requiring `compiled_grammar_task` to
22+
/// parse the program in file `program_file_path`, starting parsing with `rule_name`.
23+
pub fn parse(
24+
compiled_grammar_task: &Tasks,
25+
program_file_path: impl Into<PathBuf>,
26+
rule_name: impl Into<String>
27+
) -> Self {
28+
Self::Parse {
29+
compiled_grammar_task: Box::new(compiled_grammar_task.clone()),
30+
program_file_path: program_file_path.into(),
31+
rule_name: rule_name.into()
32+
}
33+
}
34+
}
35+
36+
/// Outputs for [`Tasks`].
37+
#[derive(Clone, Eq, PartialEq, Debug)]
38+
pub enum Outputs {
39+
CompiledGrammar(CompiledGrammar),
40+
Parsed(Option<String>)
41+
}

src/4_example/b_3_require_file.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
fn require_file_to_string<C: Context<Tasks>>(context: &mut C, path: impl AsRef<Path>) -> Result<String, String> {
3+
let path = path.as_ref();
4+
let mut file = context.require_file(path)
5+
.map_err(|e| format!("Opening file '{}' for reading failed: {}", path.display(), e))?
6+
.ok_or_else(|| format!("File '{}' does not exist", path.display()))?;
7+
let mut text = String::new();
8+
file.read_to_string(&mut text)
9+
.map_err(|e| format!("Reading file '{}' failed: {}", path.display(), e))?;
10+
Ok(text)
11+
}

src/4_example/b_4_task.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
impl Task for Tasks {
3+
type Output = Result<Outputs, String>;
4+
5+
fn execute<C: Context<Self>>(&self, context: &mut C) -> Self::Output {
6+
match self {
7+
Tasks::CompileGrammar { grammar_file_path } => {
8+
let grammar_text = require_file_to_string(context, grammar_file_path)?;
9+
let compiled_grammar = CompiledGrammar::new(&grammar_text, Some(grammar_file_path.to_string_lossy().as_ref()))?;
10+
Ok(Outputs::CompiledGrammar(compiled_grammar))
11+
}
12+
Tasks::Parse { compiled_grammar_task, program_file_path, rule_name } => {
13+
let Ok(Outputs::CompiledGrammar(compiled_grammar)) = context.require_task(compiled_grammar_task.as_ref()) else {
14+
// Return `None` if compiling grammar failed. Don't propagate the error, otherwise the error would be
15+
// duplicated for all `Parse` tasks.
16+
return Ok(Outputs::Parsed(None));
17+
};
18+
let program_text = require_file_to_string(context, program_file_path)?;
19+
let output = compiled_grammar.parse(&program_text, rule_name, Some(program_file_path.to_string_lossy().as_ref()))?;
20+
Ok(Outputs::Parsed(Some(output)))
21+
}
22+
}
23+
}
24+
}

src/4_example/c_1_Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "pie"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
pie_graph = "0.0.1"
8+
9+
[dev-dependencies]
10+
dev_shared = { path = "../dev_shared" }
11+
assert_matches = "1"
12+
pest = "2"
13+
pest_meta = "2"
14+
pest_vm = "2"
15+
clap = { version = "4", features = ["derive"] }

src/4_example/c_2_cli.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use std::path::PathBuf;
2+
3+
use clap::Parser;
4+
5+
pub mod parse;
6+
pub mod task;
7+
8+
#[derive(Parser)]
9+
pub struct Args {
10+
/// Path to the pest grammar file.
11+
grammar_file_path: PathBuf,
12+
/// Rule name (from the pest grammar file) used to parse program files.
13+
rule_name: String,
14+
/// Paths to program files to parse with the pest grammar.
15+
program_file_paths: Vec<PathBuf>
16+
}
17+
18+
fn main() {
19+
let args = Args::parse();
20+
}

src/4_example/c_3_compile_parse.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::fmt::Write;
2+
use std::path::PathBuf;
3+
4+
use clap::Parser;
5+
use pie::Pie;
6+
use pie::tracker::writing::WritingTracker;
7+
8+
use crate::task::{Outputs, Tasks};
9+
10+
pub mod parse;
11+
pub mod task;
12+
13+
#[derive(Parser)]
14+
pub struct Args {
15+
/// Path to the pest grammar file.
16+
grammar_file_path: PathBuf,
17+
/// Rule name (from the pest grammar file) used to parse program files.
18+
rule_name: String,
19+
/// Paths to program files to parse with the pest grammar.
20+
program_file_paths: Vec<PathBuf>
21+
}
22+
23+
fn main() {
24+
let args = Args::parse();
25+
compile_grammar_and_parse(args);
26+
}
27+
28+
fn compile_grammar_and_parse(args: Args) {
29+
let mut pie = Pie::with_tracker(WritingTracker::with_stderr());
30+
31+
let mut session = pie.new_session();
32+
let mut errors = String::new();
33+
34+
let compile_grammar_task = Tasks::compile_grammar(&args.grammar_file_path);
35+
if let Err(error) = session.require(&compile_grammar_task) {
36+
let _ = writeln!(errors, "{}", error); // Ignore error: writing to String cannot fail.
37+
}
38+
39+
for path in args.program_file_paths {
40+
let task = Tasks::parse(&compile_grammar_task, &path, &args.rule_name);
41+
match session.require(&task) {
42+
Err(error) => { let _ = writeln!(errors, "{}", error); }
43+
Ok(Outputs::Parsed(Some(output))) => println!("Parsing '{}' succeeded: {}", path.display(), output),
44+
_ => {}
45+
}
46+
}
47+
48+
if !errors.is_empty() {
49+
println!("Errors:\n{}", errors);
50+
}
51+
}

src/4_example/c_4_grammar.pest

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
num = { ASCII_DIGIT+ }
2+
3+
main = { SOI ~ num ~ EOI }
4+
5+
WHITESPACE = _{ " " | "\t" | "\n" | "\r" }

src/4_example/c_4_test_1.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
42

src/4_example/c_4_test_2.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

0 commit comments

Comments
 (0)