Skip to content

Commit

Permalink
Add to support \use\ load partial for Pest grammars.
Browse files Browse the repository at this point in the history
Example:

base.pest

\`\`\`
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
\`\`\`

json.pest

\`\`\`
use "base.pest"

json = { ... }
\`\`\`
  • Loading branch information
huacnlee committed Jan 4, 2023
1 parent 157f159 commit bac664c
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 22 deletions.
115 changes: 98 additions & 17 deletions generator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extern crate quote;
use std::env;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use std::path::{Path, PathBuf};

use proc_macro2::TokenStream;
use syn::{Attribute, DeriveInput, Generics, Ident, Lit, Meta};
Expand All @@ -36,6 +36,42 @@ mod generator;
use pest_meta::parser::{self, rename_meta_rule, Rule};
use pest_meta::{optimizer, unwrap_or_report, validator};

fn join_path(path: &str) -> PathBuf {
let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());

// Check whether we can find a file at the path relative to the CARGO_MANIFEST_DIR
// first.
//
// If we cannot find the expected file over there, fallback to the
// `CARGO_MANIFEST_DIR/src`, which is the old default and kept for convenience
// reasons.
// TODO: This could be refactored once `std::path::absolute()` get's stabilized.
// https://doc.rust-lang.org/std/path/fn.absolute.html
let path = if Path::new(&root).join(path).exists() {
Path::new(&root).join(path)
} else {
Path::new(&root).join("src/").join(path)
};

path
}

/// Get path relative to `path` dir, or relative to root path
fn partial_path(path: Option<&PathBuf>, filename: &str) -> PathBuf {
let root = match path {
Some(path) => path.parent().unwrap().to_path_buf(),
None => join_path("./"),
};

// Add .pest suffix if not exist
let mut filename = filename.to_string();
if !filename.to_lowercase().ends_with(".pest") {
filename.push_str(".pest");
}

root.join(filename)
}

/// Processes the derive/proc macro input and generates the corresponding parser based
/// on the parsed grammar. If `include_grammar` is set to true, it'll generate an explicit
/// "include_str" statement (done in pest_derive, but turned off in the local bootstrap).
Expand All @@ -44,26 +80,13 @@ pub fn derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream {
let (name, generics, contents) = parse_derive(ast);

let mut data = String::new();
let mut has_use = false;
let mut path = None;

for content in contents {
let (_data, _path) = match content {
GrammarSource::File(ref path) => {
let root = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());

// Check whether we can find a file at the path relative to the CARGO_MANIFEST_DIR
// first.
//
// If we cannot find the expected file over there, fallback to the
// `CARGO_MANIFEST_DIR/src`, which is the old default and kept for convenience
// reasons.
// TODO: This could be refactored once `std::path::absolute()` get's stabilized.
// https://doc.rust-lang.org/std/path/fn.absolute.html
let path = if Path::new(&root).join(path).exists() {
Path::new(&root).join(path)
} else {
Path::new(&root).join("src/").join(path)
};
let path = join_path(path);

let file_name = match path.file_name() {
Some(file_name) => file_name,
Expand All @@ -85,13 +108,44 @@ pub fn derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream {
}
}

let pairs = match parser::parse(Rule::grammar_rules, &data) {
// parse `use filename.pest` and replace data
let raw_data = data.clone();
let mut pairs = match parser::parse(Rule::grammar_rules, &raw_data) {
Ok(pairs) => pairs,
Err(error) => panic!("error parsing \n{}", error.renamed_rules(rename_meta_rule)),
};

// parse `use filename.pest` and replace data
let mut partial_pairs = pairs.clone().flatten().peekable();
while let Some(pair) = partial_pairs.next() {
if pair.as_rule() == Rule::_use {
if let Some(filename) = partial_pairs.peek() {
let partial_data = match read_file(partial_path(path.as_ref(), filename.as_str())) {
Ok(data) => data,
Err(error) => panic!("error opening {:?}: {}", filename, error),
};

let (start, end) = (pair.as_span().start(), pair.as_span().end());

data.replace_range(start..end, &partial_data);
has_use = true;
} else {
panic!("use must next with filename")
}
}
}

if has_use {
// Re-parse the data after replacing the `use` statement
pairs = match parser::parse(Rule::grammar_rules, &data) {
Ok(pairs) => pairs,
Err(error) => panic!("error parsing \n{}", error.renamed_rules(rename_meta_rule)),
};
}

let defaults = unwrap_or_report(validator::validate_pairs(pairs.clone()));
let ast = unwrap_or_report(parser::consume_rules(pairs));

let optimized = optimizer::optimize(ast);

generator::generate(name, &generics, path, optimized, defaults, include_grammar)
Expand Down Expand Up @@ -155,6 +209,10 @@ fn get_attribute(attr: &Attribute) -> GrammarSource {

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use crate::partial_path;

use super::parse_derive;
use super::GrammarSource;

Expand Down Expand Up @@ -225,4 +283,27 @@ mod tests {
let ast = syn::parse_str(definition).unwrap();
parse_derive(ast);
}

#[test]
fn test_partial_path() {
assert_eq!(
"tests/grammars/base.pest",
partial_path(Some(&PathBuf::from("tests/grammars/foo.pest")), "base")
.to_str()
.unwrap()
);

assert_eq!(
"tests/grammars/base.pest",
partial_path(Some(&PathBuf::from("tests/grammars/foo.pest")), "base.pest")
.to_str()
.unwrap()
);

let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
assert_eq!(
std::path::Path::new(&root).join("base.pest"),
partial_path(None, "base.pest")
);
}
}
1 change: 1 addition & 0 deletions grammars/src/grammars/base.pest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
3 changes: 1 addition & 2 deletions grammars/src/grammars/json.pest
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.
use "base.pest"

json = { SOI ~ (object | array) ~ EOI }

Expand All @@ -28,5 +29,3 @@ exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ }
bool = { "true" | "false" }

null = { "null" }

WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
2 changes: 1 addition & 1 deletion grammars/src/grammars/toml.pest
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.
use "./base"

toml = { SOI ~ (table | array_table | pair)* ~ EOI }

Expand Down Expand Up @@ -70,5 +71,4 @@ exp = @{ ("E" | "e") ~ ("+" | "-")? ~ int }

boolean = { "true" | "false" }

WHITESPACE = _{ " " | "\t" | NEWLINE }
COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }
6 changes: 5 additions & 1 deletion meta/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ grammar_rules = _{ SOI ~ grammar_rule+ ~ EOI }

grammar_rule = {
identifier ~ assignment_operator ~ modifier? ~
opening_brace ~ expression ~ closing_brace
opening_brace ~ expression ~ closing_brace |
_use
}

assignment_operator = { "=" }
Expand Down Expand Up @@ -96,3 +97,6 @@ newline = _{ "\n" | "\r\n" }
WHITESPACE = _{ " " | "\t" | newline }
block_comment = _{ "/*" ~ (block_comment | !"*/" ~ ANY)* ~ "*/" }
COMMENT = _{ block_comment | ("//" ~ (!newline ~ ANY)*) }

_use = ${ "use" ~ " "+ ~ "\"" ~ path ~ "\"" }
path = @{ (!(newline | "\"") ~ ANY)* ~ ".pest"? }
28 changes: 27 additions & 1 deletion meta/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub fn rename_meta_rule(rule: &Rule) -> String {
Rule::insensitive_string => "`^`".to_owned(),
Rule::range_operator => "`..`".to_owned(),
Rule::single_quote => "`'`".to_owned(),
Rule::_use => "use".to_owned(),
other_rule => format!("{:?}", other_rule),
}
}
Expand Down Expand Up @@ -1093,13 +1094,38 @@ mod tests {
};
}

#[test]
fn test_use() {
parses_to! {
parser: PestParser,
input: "use \"foo\"",
rule: Rule::_use,
tokens: [
_use(0, 9, [
path(5, 8),
])
]
};

parses_to! {
parser: PestParser,
input: "use \"foo.bar.pest\"",
rule: Rule::_use,
tokens: [
_use(0, 19, [
path(6, 18),
])
]
};
}

#[test]
fn wrong_identifier() {
fails_with! {
parser: PestParser,
input: "0",
rule: Rule::grammar_rules,
positives: vec![Rule::identifier],
positives: vec![Rule::grammar_rule],
negatives: vec![],
pos: 0
};
Expand Down

0 comments on commit bac664c

Please sign in to comment.