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
192 changes: 192 additions & 0 deletions src/follow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::collections::{BTreeMap, BTreeSet};

use crate::grammar::{EOF, NonTerminal, Production, Symbol, Terminal};

/// SLR(1) の reduce 先読みとして使う FOLLOW 集合を求める。
///
/// `productions[0]` は拡張生成規則 S' -> S でなければならない。`FOLLOW(S') = {$}` を
/// 起点に、変化がなくなるまで伝播させる。
pub(crate) fn follow_sets(
productions: &[Production],
augmented_start: NonTerminal,
) -> BTreeMap<NonTerminal, BTreeSet<Terminal>> {
let nullable = nullable_non_terminals(productions);
let first = first_sets(productions, &nullable);

let mut follow: BTreeMap<NonTerminal, BTreeSet<Terminal>> = BTreeMap::new();
for production in productions {
follow.entry(production.left).or_default();
}
follow.entry(augmented_start).or_default().insert(EOF);

loop {
let mut changed = false;

for production in productions {
for (index, symbol) in production.right.iter().enumerate() {
let Symbol::NonTerminal(target) = symbol else {
continue;
};

let rest = &production.right[index + 1..];
let mut addition = first_of_sequence(rest, &first, &nullable);
if is_nullable_sequence(rest, &nullable) {
let inherited = follow.get(&production.left).into_iter().flatten().copied();
addition.extend(inherited);
}

let entry = follow.entry(*target).or_default();
for terminal in addition {
changed |= entry.insert(terminal);
}
}
}

if !changed {
return follow;
}
}
}

/// 空列に還元できる非終端記号。右辺が空の生成規則、または全要素が nullable な生成規則を持つもの。
fn nullable_non_terminals(productions: &[Production]) -> BTreeSet<NonTerminal> {
let mut nullable = BTreeSet::new();

loop {
let mut changed = false;

for production in productions {
if nullable.contains(&production.left) {
continue;
}
if is_nullable_sequence(&production.right, &nullable) {
nullable.insert(production.left);
changed = true;
}
}

if !changed {
return nullable;
}
}
}

fn is_nullable_sequence(symbols: &[Symbol], nullable: &BTreeSet<NonTerminal>) -> bool {
symbols.iter().all(|symbol| match symbol {
Symbol::Terminal(_) => false,
Symbol::NonTerminal(non_terminal) => nullable.contains(non_terminal),
})
}

/// 各非終端記号の FIRST 集合。ε は含めない(nullable 集合と組で使う)。
fn first_sets(
productions: &[Production],
nullable: &BTreeSet<NonTerminal>,
) -> BTreeMap<NonTerminal, BTreeSet<Terminal>> {
let mut first: BTreeMap<NonTerminal, BTreeSet<Terminal>> = BTreeMap::new();

loop {
let mut changed = false;

for production in productions {
let addition = first_of_sequence(&production.right, &first, nullable);
let entry = first.entry(production.left).or_default();
for terminal in addition {
changed |= entry.insert(terminal);
}
}

if !changed {
return first;
}
}
}

/// 記号列の FIRST。nullable な非終端記号は読み飛ばして次の記号へ進む。
fn first_of_sequence(
symbols: &[Symbol],
first: &BTreeMap<NonTerminal, BTreeSet<Terminal>>,
nullable: &BTreeSet<NonTerminal>,
) -> BTreeSet<Terminal> {
let mut result = BTreeSet::new();

for symbol in symbols {
match symbol {
Symbol::Terminal(terminal) => {
result.insert(*terminal);
return result;
}
Symbol::NonTerminal(non_terminal) => {
if let Some(set) = first.get(non_terminal) {
result.extend(set.iter().copied());
}
if !nullable.contains(non_terminal) {
return result;
}
}
}
}

result
}

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

/// 拡張生成規則を先頭に足した生成規則列と、選んだ拡張開始記号を返す。
/// `lr::augment` と同じ規則(未使用の大文字を選ぶ)を使う。
fn augmented(text: &str) -> (Vec<Production>, NonTerminal) {
let grammar = parse_grammar_text(text).unwrap();
let used = grammar.non_terminals();
let start = ('A'..='Z')
.map(NonTerminal)
.find(|candidate| !used.contains(candidate))
.unwrap();

let mut productions = grammar.productions.clone();
productions.insert(
0,
Production {
left: start,
right: vec![Symbol::NonTerminal(grammar.start)],
},
);

(productions, start)
}

fn terminals(chars: &str) -> BTreeSet<Terminal> {
chars.chars().map(Terminal).collect()
}

#[test]
fn follow_of_arithmetic_grammar() {
let (productions, start) = augmented(include_str!("../reducer"));
let follow = follow_sets(&productions, start);

assert_eq!(follow[&NonTerminal('E')], terminals("*+$"));
assert_eq!(follow[&NonTerminal('B')], terminals("*+$"));
}

#[test]
fn follow_of_start_symbol_is_eof() {
let (productions, start) = augmented("S -> aA\nA -> b");
let follow = follow_sets(&productions, start);

assert_eq!(follow[&NonTerminal('S')], terminals("$"));
}

/// B が nullable なので、FOLLOW(A) は B を跨いで 'c' まで届く。
/// nullable を無視すると FIRST(B) が空のまま止まり FOLLOW(A) は空になる。
#[test]
fn nullable_non_terminal_is_traversed_when_computing_follow() {
let (productions, start) = augmented("S -> ABc\nA -> a\nB -> ");
let follow = follow_sets(&productions, start);

assert!(nullable_non_terminals(&productions).contains(&NonTerminal('B')));
assert_eq!(follow[&NonTerminal('A')], terminals("c"));
assert_eq!(follow[&NonTerminal('B')], terminals("c"));
}
}
4 changes: 2 additions & 2 deletions src/generator_engine.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use lr0_parser_rs::AstNode;
use lr0_parser_rs::grammar::{parse_grammar_text, parse_input_text};
use lr0_parser_rs::lr::compile;
use lr0_parser_rs::lr::compile_lr0;
use lr0_parser_rs::runtime::run;
use std::collections::HashMap;
use std::fs;
Expand Down Expand Up @@ -40,7 +40,7 @@ impl GeneratorEngine {
let grammar = parse_grammar_text(reducer_string)
.map_err(|err| format!("Failed to parse grammar: {err:?}"))?;
let machine =
compile(&grammar).map_err(|err| format!("Failed to compile parser: {err:?}"))?;
compile_lr0(&grammar).map_err(|err| format!("Failed to compile parser: {err:?}"))?;
let input = parse_input_text(input_string)
.map_err(|err| format!("Failed to parse input: {err:?}"))?;
let result =
Expand Down
5 changes: 4 additions & 1 deletion src/grammar.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::collections::BTreeSet;

/// 入力末尾を表す予約終端記号。`parse_input_text` が自動で付加する。
pub const EOF: Terminal = Terminal('$');

pub fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
Expand Down Expand Up @@ -138,7 +141,7 @@ pub fn parse_input_text(input: &str) -> Result<Vec<Symbol>, GrammarError> {
symbols.push(Symbol::Terminal(Terminal(value)));
}

symbols.push(Symbol::Terminal(Terminal('$')));
symbols.push(Symbol::Terminal(EOF));
Ok(symbols)
}

Expand Down
12 changes: 7 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ pub mod grammar;
pub mod lr;
pub mod runtime;

mod follow;

pub use ast::AstNode;
pub use lr::{LrItem, StateInfo};
pub use lr::{Conflict, LrItem, StateInfo};
pub use runtime::{ParseStep, StepAction, build_trace};

#[cfg(test)]
mod tests {
use crate::ast::AstNode;
use crate::grammar::{parse_grammar_text, parse_input_text};
use crate::lr::compile;
use crate::lr::compile_lr0;
use crate::runtime::run;

#[test]
fn grammar_compile_runtime_pipeline_parses_expression() {
let grammar = parse_grammar_text(include_str!("../reducer")).unwrap();
let machine = compile(&grammar).unwrap();
let machine = compile_lr0(&grammar).unwrap();
let input = parse_input_text("1+1*1").unwrap();

let result = run(&machine, &input).unwrap();
Expand Down Expand Up @@ -50,7 +52,7 @@ mod tests {
// S -> SP | P, P -> <> | <S>
// 8 states, all conflict-free — confirmed LR(0)
let grammar = parse_grammar_text("S -> SP\nS -> P\nP -> <>\nP -> <S>").unwrap();
let machine = compile(&grammar).unwrap();
let machine = compile_lr0(&grammar).unwrap();
let input = parse_input_text("<<>><>").unwrap();

let result = run(&machine, &input).unwrap();
Expand All @@ -66,6 +68,6 @@ mod tests {
// E -> EE creates a Shift/Reduce conflict: after reducing EE->E,
// the parser cannot decide between reducing again or shifting '<'.
let grammar = parse_grammar_text(include_str!("../paren_reducer")).unwrap();
assert!(matches!(compile(&grammar), Err(crate::lr::ParserError::ConflictReducer)));
assert!(matches!(compile_lr0(&grammar), Err(crate::lr::ParserError::Conflict(_))));
}
}
Loading