Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
[workspace]
members = ["./peg-macros", "./peg-runtime"]

[package]
name = "peg"
[workspace.package]
version = "0.8.5"
authors = [ "Kevin Mehall <[email protected]>" ]
license = "MIT"
repository = "https://github.com/kevinmehall/rust-peg"
edition = "2021"
rust-version = "1.68.0" # if changed, also update .github/workflows/rust.yml

[package]
name = "peg"
version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "A simple Parsing Expression Grammar (PEG) parser generator."
edition.workspace = true
rust-version.workspace = true
keywords = ["peg", "parser", "parsing", "grammar"]
categories = ["parsing"]
readme = "README.md"
edition = "2021"
rust-version = "1.68.0" # if changed, also update .github/workflows/rust.yml

[dependencies]
peg-macros = { path = "./peg-macros", version = "= 0.8.5" }
Expand All @@ -31,4 +39,4 @@ harness = false
default = ["std"]
trace = ["peg-macros/trace"]
std = ["peg-runtime/std"]
unstable = ["peg-runtime/unstable"]
unstable = ["peg-runtime/unstable"]
3 changes: 3 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ if cargo run -p peg-macros -- peg-macros/grammar.rustpeg > peg-macros/grammar_ne
then
diff -qs peg-macros/grammar.rs peg-macros/grammar_new.rs
rustfmt peg-macros/grammar.rs
# Idempotency bugs in rustfmt means this has to happen twice or the wrong
# output is emitted
rustfmt peg-macros/grammar.rs
else
echo "Failed"
fi
11 changes: 6 additions & 5 deletions peg-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
[package]
name = "peg-macros"
version = "0.8.5"
authors = [ "Kevin Mehall <[email protected]>" ]
license = "MIT"
repository = "https://github.com/kevinmehall/rust-peg"
version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Procedural macros for rust-peg. To use rust-peg, see the `peg` crate."
edition = "2021"
edition.workspace = true
rust-version.workspace = true

[dependencies]
quote = "1.0"
Expand Down
104 changes: 58 additions & 46 deletions peg-macros/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ impl LeftRecursionError {
}

impl<'a> LeftRecursionVisitor<'a> {
fn check(grammar: &'a Grammar, rules: &HashMap<String, &'a Rule>) -> (HashMap<String, bool>, Vec<LeftRecursionError>) {
fn check(
grammar: &'a Grammar,
rules: &HashMap<String, &'a Rule>,
) -> (HashMap<String, bool>, Vec<LeftRecursionError>) {
let mut visitor = LeftRecursionVisitor {
rules,
errors: Vec::new(),
Expand All @@ -64,7 +67,9 @@ impl<'a> LeftRecursionVisitor<'a> {
for rule in grammar.iter_rules() {
let nullable = visitor.walk_rule(rule);
debug_assert!(visitor.stack.is_empty());
rule_nullability.entry(rule.name.to_string()).or_insert(nullable);
rule_nullability
.entry(rule.name.to_string())
.or_insert(nullable);
}

(rule_nullability, visitor.errors)
Expand All @@ -84,9 +89,8 @@ impl<'a> LeftRecursionVisitor<'a> {
/// any input. This is a conservative heuristic, if unknown, we return false
/// to avoid reporting false-positives for left recursion.
fn walk_expr(&mut self, this_expr: &SpannedExpr) -> bool {
use self::Expr::*;
match this_expr.expr {
RuleExpr(ref rule_ident, _, _) => {
Expr::Rule(ref rule_ident, _, _) => {
let name = rule_ident.to_string();

if let Some(rule) = self.rules.get(&name) {
Expand All @@ -98,24 +102,22 @@ impl<'a> LeftRecursionVisitor<'a> {
let mut recursive_loop = self.stack[loop_start..].to_vec();
recursive_loop.push(name.clone());
match rule.cache {
None | Some(Cache::Simple) =>
self.errors.push(LeftRecursionError {
path: recursive_loop,
span: rule_ident.span(),
}),
_ => ()

None | Some(Cache::Simple) => self.errors.push(LeftRecursionError {
path: recursive_loop,
span: rule_ident.span(),
}),
_ => (),
}
return false;
}
self.walk_rule(rule)
} else {
// Missing rule would have already been reported
false
false
}
}

ActionExpr(ref elems, ..) => {
Expr::Action(ref elems, ..) => {
for elem in elems {
if !self.walk_expr(&elem.expr) {
return false;
Expand All @@ -125,27 +127,31 @@ impl<'a> LeftRecursionVisitor<'a> {
true
}

ChoiceExpr(ref choices) => {
Expr::Choice(ref choices) => {
let mut nullable = false;
for expr in choices {
nullable |= self.walk_expr(expr);
}
nullable
}

OptionalExpr(ref expr) | PosAssertExpr(ref expr) | NegAssertExpr(ref expr) => {
Expr::Optional(ref expr) | Expr::PosAssert(ref expr) | Expr::NegAssert(ref expr) => {
self.walk_expr(expr);
true
}

Repeat { ref inner, ref bound, .. } => {
Expr::Repeat {
ref inner,
ref bound,
..
} => {
let inner_nullable = self.walk_expr(inner);
inner_nullable | !bound.has_lower_bound()
}

MatchStrExpr(ref expr) | QuietExpr(ref expr) => self.walk_expr(expr),
Expr::MatchStr(ref expr) | Expr::Quiet(ref expr) => self.walk_expr(expr),

PrecedenceExpr { ref levels } => {
Expr::Precedence { ref levels } => {
let mut nullable = false;

for level in levels {
Expand All @@ -161,17 +167,17 @@ impl<'a> LeftRecursionVisitor<'a> {
}
}

nullable
nullable
}

| LiteralExpr(_)
| PatternExpr(_)
| MethodExpr(_, _)
| CustomExpr(_)
| FailExpr(_)
| MarkerExpr(_) => false,
Expr::Literal(_)
| Expr::Pattern(_)
| Expr::Method(_, _)
| Expr::Custom(_)
| Expr::Fail(_)
| Expr::Marker(_) => false,

PositionExpr => true,
Expr::Position => true,
}
}
}
Expand All @@ -189,13 +195,15 @@ pub struct LoopNullabilityError {

impl LoopNullabilityError {
pub fn msg(&self) -> String {
format!("loops infinitely because loop body can match without consuming input")
"loops infinitely because loop body can match without consuming input".to_string()
}
}


impl<'a> LoopNullabilityVisitor<'a> {
fn check(grammar: &'a Grammar, rule_nullability: &HashMap<String, bool>) -> Vec<LoopNullabilityError> {
fn check(
grammar: &'a Grammar,
rule_nullability: &HashMap<String, bool>,
) -> Vec<LoopNullabilityError> {
let mut visitor = LoopNullabilityVisitor {
rule_nullability,
errors: Vec::new(),
Expand All @@ -208,7 +216,6 @@ impl<'a> LoopNullabilityVisitor<'a> {
visitor.errors
}


/// Walk an expr and its children analyzing the nullability of loop bodies.
///
/// Returns true if the rule is known to match completely without consuming
Expand All @@ -219,49 +226,54 @@ impl<'a> LoopNullabilityVisitor<'a> {
/// entire expression tree rather than just the nullable prefix, and doesn't
/// recurse into calls.
fn walk_expr(&mut self, this_expr: &SpannedExpr) -> bool {
use self::Expr::*;
match this_expr.expr {
RuleExpr(ref rule_ident, _, _) => {
Expr::Rule(ref rule_ident, _, _) => {
let name = rule_ident.to_string();
*self.rule_nullability.get(&name).unwrap_or(&false)
}

ActionExpr(ref elems, ..) => {
Expr::Action(ref elems, ..) => {
let mut nullable = true;
for elem in elems {
nullable &= self.walk_expr(&elem.expr);
}
nullable
}

ChoiceExpr(ref choices) => {
Expr::Choice(ref choices) => {
let mut nullable = false;
for expr in choices {
nullable |= self.walk_expr(expr);
}
nullable
}

OptionalExpr(ref expr) | PosAssertExpr(ref expr) | NegAssertExpr(ref expr) => {
Expr::Optional(ref expr) | Expr::PosAssert(ref expr) | Expr::NegAssert(ref expr) => {
self.walk_expr(expr);
true
}

Repeat { ref inner, ref bound, ref sep } => {
Expr::Repeat {
ref inner,
ref bound,
ref sep,
} => {
let inner_nullable = self.walk_expr(inner);
let sep_nullable = sep.as_ref().map_or(true, |sep| self.walk_expr(sep));

// The entire purpose of this analysis: report errors if the loop body is nullable
if inner_nullable && sep_nullable && !bound.has_upper_bound() {
self.errors.push(LoopNullabilityError { span: this_expr.span });
self.errors.push(LoopNullabilityError {
span: this_expr.span,
});
}

inner_nullable | !bound.has_lower_bound()
}

MatchStrExpr(ref expr) | QuietExpr(ref expr) => self.walk_expr(expr),
Expr::MatchStr(ref expr) | Expr::Quiet(ref expr) => self.walk_expr(expr),

PrecedenceExpr { ref levels } => {
Expr::Precedence { ref levels } => {
let mut nullable = false;

for level in levels {
Expand All @@ -277,14 +289,14 @@ impl<'a> LoopNullabilityVisitor<'a> {
nullable
}

| LiteralExpr(_)
| PatternExpr(_)
| MethodExpr(_, _)
| CustomExpr(_)
| FailExpr(_)
| MarkerExpr(_) => false,
Expr::Literal(_)
| Expr::Pattern(_)
| Expr::Method(_, _)
| Expr::Custom(_)
| Expr::Fail(_)
| Expr::Marker(_) => false,

PositionExpr => true,
Expr::Position => true,
}
}
}
Loading