Skip to content

Commit

Permalink
Interactive parser development example writing.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gohla committed Nov 20, 2023
1 parent cc783b2 commit e1764fe
Show file tree
Hide file tree
Showing 19 changed files with 641 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/4_example/a_1_Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "pie"
version = "0.1.0"
edition = "2021"

[dependencies]
pie_graph = "0.0.1"

[dev-dependencies]
dev_shared = { path = "../dev_shared" }
assert_matches = "1"
pest = "2"
pest_meta = "2"
pest_vm = "2"
3 changes: 3 additions & 0 deletions src/4_example/a_2_main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {

}
5 changes: 5 additions & 0 deletions src/4_example/a_3_main_parse_mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod parse;

fn main() {

}
38 changes: 38 additions & 0 deletions src/4_example/a_4_grammar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::collections::HashSet;
use std::fmt::Write;

/// Parse programs with a compiled pest grammar.
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct CompiledGrammar {
rules: Vec<pest_meta::optimizer::OptimizedRule>,
rule_names: HashSet<String>,
}

impl CompiledGrammar {
/// Compile the pest grammar from `grammar_text`, using `path` to annotate errors. Returns a [`Self`] instance.
///
/// # Errors
///
/// Returns `Err(error_string)` when compiling the grammar fails.
pub fn new(grammar_text: &str, path: Option<&str>) -> Result<Self, String> {
match pest_meta::parse_and_optimize(grammar_text) {
Ok((builtin_rules, rules)) => {
let mut rule_names = HashSet::with_capacity(builtin_rules.len() + rules.len());
rule_names.extend(builtin_rules.iter().map(|s| s.to_string()));
rule_names.extend(rules.iter().map(|s| s.name.clone()));
Ok(Self { rules, rule_names })
},
Err(errors) => {
let mut error_string = String::new();
for mut error in errors {
if let Some(path) = path.as_ref() {
error = error.with_path(path);
}
error = error.renamed_rules(pest_meta::parser::rename_meta_rule);
let _ = writeln!(error_string, "{}", error); // Ignore error: writing to String cannot fail.
}
Err(error_string)
}
}
}
}
64 changes: 64 additions & 0 deletions src/4_example/a_5_parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::collections::HashSet;
use std::fmt::Write;

/// Parse programs with a compiled pest grammar.
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct CompiledGrammar {
rules: Vec<pest_meta::optimizer::OptimizedRule>,
rule_names: HashSet<String>,
}

impl CompiledGrammar {
/// Compile the pest grammar from `grammar_text`, using `path` to annotate errors. Returns a [`Self`] instance.
///
/// # Errors
///
/// Returns `Err(error_string)` when compiling the grammar fails.
pub fn new(grammar_text: &str, path: Option<&str>) -> Result<Self, String> {
match pest_meta::parse_and_optimize(grammar_text) {
Ok((builtin_rules, rules)) => {
let mut rule_names = HashSet::with_capacity(builtin_rules.len() + rules.len());
rule_names.extend(builtin_rules.iter().map(|s| s.to_string()));
rule_names.extend(rules.iter().map(|s| s.name.clone()));
Ok(Self { rules, rule_names })
},
Err(errors) => {
let mut error_string = String::new();
for mut error in errors {
if let Some(path) = path.as_ref() {
error = error.with_path(path);
}
error = error.renamed_rules(pest_meta::parser::rename_meta_rule);
let _ = writeln!(error_string, "{}", error); // Ignore error: writing to String cannot fail.
}
Err(error_string)
}
}
}

/// Parse `program_text` with rule `rule_name` using this compiled grammar, using `path` to annotate errors. Returns
/// parsed pairs formatted as a string.
///
/// # Errors
///
/// Returns `Err(error_string)` when parsing fails.
pub fn parse(&self, program_text: &str, rule_name: &str, path: Option<&str>) -> Result<String, String> {
if !self.rule_names.contains(rule_name) {
let message = format!("rule '{}' was not found", rule_name);
return Err(message);
}
// Note: can't store `Vm` in `CompiledGrammar` because `Vm` is not `Clone` nor `Eq`.
let vm = pest_vm::Vm::new(self.rules.clone());
match vm.parse(rule_name, program_text) {
Ok(pairs) => Ok(format!("{}", pairs)),
Err(mut error) => {
if let Some(path) = path {
error = error.with_path(path);
}
error = error.renamed_rules(|r| r.to_string());
let error_string = format!("{}", error);
Err(error_string)
}
}
}
}
32 changes: 32 additions & 0 deletions src/4_example/a_6_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_compile_parse() -> Result<(), String> {
// Grammar compilation failure.
let result = CompiledGrammar::new("asd = { fgh } qwe = { rty }", None);
assert!(result.is_err());
println!("{}", result.unwrap_err());

// Grammar that parses numbers.
let compiled_grammar = CompiledGrammar::new("num = { ASCII_DIGIT+ }", None)?;
println!("{:?}", compiled_grammar);

// Parse failure
let result = compiled_grammar.parse("a", "num", None);
assert!(result.is_err());
println!("{}", result.unwrap_err());
// Parse failure due to non-existent rule.
let result = compiled_grammar.parse("1", "asd", None);
assert!(result.is_err());
println!("{}", result.unwrap_err());
// Parse success
let result = compiled_grammar.parse("1", "num", None);
assert!(result.is_ok());
println!("{}", result.unwrap());

Ok(())
}
}
6 changes: 6 additions & 0 deletions src/4_example/b_1_main_task_mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod parse;
pub mod task;

fn main() {

}
41 changes: 41 additions & 0 deletions src/4_example/b_2_tasks_outputs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::io::Read;
use std::path::{Path, PathBuf};

use pie::{Context, Task};

use crate::parse::CompiledGrammar;

/// Tasks for compiling a grammar and parsing files with it.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum Tasks {
CompileGrammar { grammar_file_path: PathBuf },
Parse { compiled_grammar_task: Box<Tasks>, program_file_path: PathBuf, rule_name: String }
}

impl Tasks {
/// Create a [`Self::CompileGrammar`] task that compiles the grammar in file `grammar_file_path`.
pub fn compile_grammar(grammar_file_path: impl Into<PathBuf>) -> Self {
Self::CompileGrammar { grammar_file_path: grammar_file_path.into() }
}

/// Create a [`Self::Parse`] task that uses the compiled grammar returned by requiring `compiled_grammar_task` to
/// parse the program in file `program_file_path`, starting parsing with `rule_name`.
pub fn parse(
compiled_grammar_task: &Tasks,
program_file_path: impl Into<PathBuf>,
rule_name: impl Into<String>
) -> Self {
Self::Parse {
compiled_grammar_task: Box::new(compiled_grammar_task.clone()),
program_file_path: program_file_path.into(),
rule_name: rule_name.into()
}
}
}

/// Outputs for [`Tasks`].
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Outputs {
CompiledGrammar(CompiledGrammar),
Parsed(Option<String>)
}
11 changes: 11 additions & 0 deletions src/4_example/b_3_require_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

fn require_file_to_string<C: Context<Tasks>>(context: &mut C, path: impl AsRef<Path>) -> Result<String, String> {
let path = path.as_ref();
let mut file = context.require_file(path)
.map_err(|e| format!("Opening file '{}' for reading failed: {}", path.display(), e))?
.ok_or_else(|| format!("File '{}' does not exist", path.display()))?;
let mut text = String::new();
file.read_to_string(&mut text)
.map_err(|e| format!("Reading file '{}' failed: {}", path.display(), e))?;
Ok(text)
}
24 changes: 24 additions & 0 deletions src/4_example/b_4_task.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

impl Task for Tasks {
type Output = Result<Outputs, String>;

fn execute<C: Context<Self>>(&self, context: &mut C) -> Self::Output {
match self {
Tasks::CompileGrammar { grammar_file_path } => {
let grammar_text = require_file_to_string(context, grammar_file_path)?;
let compiled_grammar = CompiledGrammar::new(&grammar_text, Some(grammar_file_path.to_string_lossy().as_ref()))?;
Ok(Outputs::CompiledGrammar(compiled_grammar))
}
Tasks::Parse { compiled_grammar_task, program_file_path, rule_name } => {
let Ok(Outputs::CompiledGrammar(compiled_grammar)) = context.require_task(compiled_grammar_task.as_ref()) else {
// Return `None` if compiling grammar failed. Don't propagate the error, otherwise the error would be
// duplicated for all `Parse` tasks.
return Ok(Outputs::Parsed(None));
};
let program_text = require_file_to_string(context, program_file_path)?;
let output = compiled_grammar.parse(&program_text, rule_name, Some(program_file_path.to_string_lossy().as_ref()))?;
Ok(Outputs::Parsed(Some(output)))
}
}
}
}
15 changes: 15 additions & 0 deletions src/4_example/c_1_Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "pie"
version = "0.1.0"
edition = "2021"

[dependencies]
pie_graph = "0.0.1"

[dev-dependencies]
dev_shared = { path = "../dev_shared" }
assert_matches = "1"
pest = "2"
pest_meta = "2"
pest_vm = "2"
clap = { version = "4", features = ["derive"] }
20 changes: 20 additions & 0 deletions src/4_example/c_2_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::path::PathBuf;

use clap::Parser;

pub mod parse;
pub mod task;

#[derive(Parser)]
pub struct Args {
/// Path to the pest grammar file.
grammar_file_path: PathBuf,
/// Rule name (from the pest grammar file) used to parse program files.
rule_name: String,
/// Paths to program files to parse with the pest grammar.
program_file_paths: Vec<PathBuf>
}

fn main() {
let args = Args::parse();
}
51 changes: 51 additions & 0 deletions src/4_example/c_3_compile_parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::fmt::Write;
use std::path::PathBuf;

use clap::Parser;
use pie::Pie;
use pie::tracker::writing::WritingTracker;

use crate::task::{Outputs, Tasks};

pub mod parse;
pub mod task;

#[derive(Parser)]
pub struct Args {
/// Path to the pest grammar file.
grammar_file_path: PathBuf,
/// Rule name (from the pest grammar file) used to parse program files.
rule_name: String,
/// Paths to program files to parse with the pest grammar.
program_file_paths: Vec<PathBuf>
}

fn main() {
let args = Args::parse();
compile_grammar_and_parse(args);
}

fn compile_grammar_and_parse(args: Args) {
let mut pie = Pie::with_tracker(WritingTracker::with_stderr());

let mut session = pie.new_session();
let mut errors = String::new();

let compile_grammar_task = Tasks::compile_grammar(&args.grammar_file_path);
if let Err(error) = session.require(&compile_grammar_task) {
let _ = writeln!(errors, "{}", error); // Ignore error: writing to String cannot fail.
}

for path in args.program_file_paths {
let task = Tasks::parse(&compile_grammar_task, &path, &args.rule_name);
match session.require(&task) {
Err(error) => { let _ = writeln!(errors, "{}", error); }
Ok(Outputs::Parsed(Some(output))) => println!("Parsing '{}' succeeded: {}", path.display(), output),
_ => {}
}
}

if !errors.is_empty() {
println!("Errors:\n{}", errors);
}
}
5 changes: 5 additions & 0 deletions src/4_example/c_4_grammar.pest
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
num = { ASCII_DIGIT+ }

main = { SOI ~ num ~ EOI }

WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
1 change: 1 addition & 0 deletions src/4_example/c_4_test_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
42
1 change: 1 addition & 0 deletions src/4_example/c_4_test_2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
Loading

0 comments on commit e1764fe

Please sign in to comment.