From cdd57bef2c9e690b150f62710e62b4652b731136 Mon Sep 17 00:00:00 2001 From: raiga0310 Date: Sat, 5 Sep 2026 14:08:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20SLR(1)=20=E8=A7=A3=E6=9E=90=E6=B3=95?= =?UTF-8?q?=E3=81=A8=E9=A0=85=E9=9B=86=E5=90=88=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lr::compile` を `compile_lr0` / `compile_slr` に分割し、内部を `Strategy` enum で切り替える `compile_with` に共通化した。LR(0) は 還元を全終端記号に対して行い、SLR は FOLLOW 集合を先読みに使う。 - `follow.rs` を新規追加。nullable / FIRST / FOLLOW を不動点反復で計算し、 SLR の還元先読みを供給する。ライブラリ内部専用 (`pub(crate)`)。 - `ParserError::ConflictReducer` を `ParserError::Conflict(Conflict)` に変更。 競合した状態・先読み終端・既存 action・投入 action を保持し、 UI で「どこで何が競合したか」を提示できるようにした。 - 拡張開始記号を確保できない場合の `NoAvailableStartSymbol` を追加。 - `pages/item_sets.rs` を新規追加。各状態の LR 項集合をドット位置つきで 表示し、SLR では還元項に先読みを併記する。 - `pages/parser.rs`: `ParserKind` から compile 関数を注入する形に変更し、 `Slr` を実装済みとして有効化。あわせて、run が失敗しても compile が 成功していれば状態機械と項集合を表示するよう順序を入れ替えた (入力エラー時こそ項集合を見たいため)。 - `grammar.rs`: EOF 終端 `$` を `grammar::EOF` 定数として公開。 Co-Authored-By: Claude --- src/follow.rs | 192 +++++++++++++++++ src/generator_engine.rs | 4 +- src/grammar.rs | 5 +- src/lib.rs | 12 +- src/lr.rs | 456 +++++++++++++++++++++++++++++++++------- src/pages/item_sets.rs | 122 +++++++++++ src/pages/mod.rs | 1 + src/pages/parser.rs | 140 +++++++++--- src/runtime.rs | 6 +- 9 files changed, 821 insertions(+), 117 deletions(-) create mode 100644 src/follow.rs create mode 100644 src/pages/item_sets.rs diff --git a/src/follow.rs b/src/follow.rs new file mode 100644 index 0000000..72ba33c --- /dev/null +++ b/src/follow.rs @@ -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> { + let nullable = nullable_non_terminals(productions); + let first = first_sets(productions, &nullable); + + let mut follow: BTreeMap> = 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 { + 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) -> 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, +) -> BTreeMap> { + let mut first: BTreeMap> = 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>, + nullable: &BTreeSet, +) -> BTreeSet { + 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, 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 { + 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")); + } +} diff --git a/src/generator_engine.rs b/src/generator_engine.rs index fc6851a..7678d50 100644 --- a/src/generator_engine.rs +++ b/src/generator_engine.rs @@ -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; @@ -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 = diff --git a/src/grammar.rs b/src/grammar.rs index 58a9b65..675020b 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -1,5 +1,8 @@ use std::collections::BTreeSet; +/// 入力末尾を表す予約終端記号。`parse_input_text` が自動で付加する。 +pub const EOF: Terminal = Terminal('$'); + pub fn read_file(path: &str) -> Result { std::fs::read_to_string(path) } @@ -138,7 +141,7 @@ pub fn parse_input_text(input: &str) -> Result, GrammarError> { symbols.push(Symbol::Terminal(Terminal(value))); } - symbols.push(Symbol::Terminal(Terminal('$'))); + symbols.push(Symbol::Terminal(EOF)); Ok(symbols) } diff --git a/src/lib.rs b/src/lib.rs index bae5564..b03d815 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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(); @@ -50,7 +52,7 @@ mod tests { // S -> SP | P, P -> <> | // 8 states, all conflict-free — confirmed LR(0) let grammar = parse_grammar_text("S -> SP\nS -> P\nP -> <>\nP -> ").unwrap(); - let machine = compile(&grammar).unwrap(); + let machine = compile_lr0(&grammar).unwrap(); let input = parse_input_text("<<>><>").unwrap(); let result = run(&machine, &input).unwrap(); @@ -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(_)))); } } diff --git a/src/lr.rs b/src/lr.rs index 9258199..a374051 100644 --- a/src/lr.rs +++ b/src/lr.rs @@ -1,68 +1,128 @@ use std::collections::{BTreeMap, BTreeSet}; -use crate::grammar::{Grammar, NonTerminal, Production, Symbol, Terminal}; +use crate::follow::follow_sets; +use crate::grammar::{EOF, Grammar, NonTerminal, Production, Symbol, Terminal}; + +/// 非終端記号 -> その左辺を持つ完成項が reduce を書き込む先読み集合。 +/// +/// LR(0) と SLR(1) の差はこのマップの中身だけであり、項集合の構築と表の走査は共通である。 +type ReduceLookaheads = BTreeMap>; + +type ActionTable = BTreeMap<(InternalState, Terminal), Action>; +type GotoTable = BTreeMap<(InternalState, NonTerminal), InternalState>; +type Edges = BTreeMap<(InternalState, Symbol), InternalState>; + +/// 解析法。LR(0) と SLR(1) の差は `ReduceLookaheads` の作り方だけである。 +#[derive(Clone, Copy)] +enum Strategy { + Lr0, + Slr, +} -pub fn compile(grammar: &Grammar) -> Result { - let mut productions = grammar.productions.clone(); - let start_symbol = grammar.start; - let augmented_start = NonTerminal(((start_symbol.0 as u8) + 1) as char); +pub fn compile_lr0(grammar: &Grammar) -> Result { + compile_with(grammar, Strategy::Lr0) +} - productions.insert( - 0, - Production { - left: augmented_start, - right: vec![Symbol::NonTerminal(start_symbol)], - }, - ); +pub fn compile_slr(grammar: &Grammar) -> Result { + compile_with(grammar, Strategy::Slr) +} + +fn compile_with(grammar: &Grammar, strategy: Strategy) -> Result { + let (productions, augmented_start) = augment(grammar)?; let non_terminals = grammar.non_terminals(); let terminals = grammar.terminals(); - let mut item_sets: Vec> = Vec::new(); - let mut edges: BTreeMap<(InternalState, Symbol), InternalState> = BTreeMap::new(); + let (item_sets, edges) = build_item_sets(&productions, &non_terminals, &terminals); + let reduce_lookaheads = match strategy { + Strategy::Lr0 => all_terminals_lookaheads(&productions, &terminals), + Strategy::Slr => follow_sets(&productions, augmented_start), + }; + let (action_table, goto_table) = + build_tables(&productions, &item_sets, &edges, &reduce_lookaheads)?; + let state_infos = + build_state_infos(&item_sets, &edges, &reduce_lookaheads, augmented_start); + + Ok(CompiledParser { + state_count: item_sets.len(), + productions, + action_table, + goto_table, + start_state: 0, + state_infos, + }) +} +/// LR(0) 項集合の正準集合と遷移辺を構築する。SLR(1) もこの結果をそのまま使う。 +fn build_item_sets( + productions: &[Production], + non_terminals: &BTreeSet, + terminals: &BTreeSet, +) -> (Vec>, Edges) { let initial_item = Item { production: productions[0].clone(), dot_pos: 0, }; - - let initial_closure = closure( + let mut item_sets = vec![closure( &vec![initial_item].into_iter().collect(), - &productions, - &non_terminals, - ); - item_sets.push(initial_closure); + productions, + non_terminals, + )]; + let mut edges = Edges::new(); + + let mut all_symbols: Vec = Vec::new(); + all_symbols.extend(terminals.iter().copied().map(Symbol::Terminal)); + all_symbols.extend(non_terminals.iter().copied().map(Symbol::NonTerminal)); let mut state_id = 0; while state_id < item_sets.len() { let current_set = item_sets[state_id].clone(); - let mut all_symbols = Vec::new(); - all_symbols.extend(terminals.iter().copied().map(Symbol::Terminal)); - all_symbols.extend(non_terminals.iter().copied().map(Symbol::NonTerminal)); - for symbol in all_symbols { - let goto_set = goto(¤t_set, symbol.clone(), &productions, &non_terminals); + for symbol in &all_symbols { + let goto_set = goto(¤t_set, symbol.clone(), productions, non_terminals); if goto_set.is_empty() { continue; } - let next_state = - if let Some(existing) = item_sets.iter().position(|set| *set == goto_set) { - existing - } else { - let new_id = item_sets.len(); + let next_state = match item_sets.iter().position(|set| *set == goto_set) { + Some(existing) => existing, + None => { item_sets.push(goto_set); - new_id - }; + item_sets.len() - 1 + } + }; - edges.insert((state_id, symbol), next_state); + edges.insert((state_id, symbol.clone()), next_state); } state_id += 1; } - let mut action_table = BTreeMap::new(); - let mut goto_table = BTreeMap::new(); + (item_sets, edges) +} + +/// LR(0) の reduce 先読み: どの完成項も全終端記号と `$` に対して reduce を書く。 +fn all_terminals_lookaheads( + productions: &[Production], + terminals: &BTreeSet, +) -> ReduceLookaheads { + let mut every = terminals.clone(); + every.insert(EOF); + + productions + .iter() + .map(|production| (production.left, every.clone())) + .collect() +} + +fn build_tables( + productions: &[Production], + item_sets: &[BTreeSet], + edges: &Edges, + reduce_lookaheads: &ReduceLookaheads, +) -> Result<(ActionTable, GotoTable), ParserError> { + let mut action_table = ActionTable::new(); + let mut goto_table = GotoTable::new(); for (state_id, item_set) in item_sets.iter().enumerate() { for item in item_set { @@ -92,53 +152,112 @@ pub fn compile(grammar: &Grammar) -> Result { .ok_or(ParserError::MissingProduction)?; if production_id == 0 { - insert_action(&mut action_table, state_id, Terminal('$'), Action::Accept)?; + insert_action(&mut action_table, state_id, EOF, Action::Accept)?; } else { - for terminal in &terminals { + let lookaheads = reduce_lookaheads + .get(&item.production.left) + .into_iter() + .flatten(); + for &terminal in lookaheads { insert_action( &mut action_table, state_id, - *terminal, + terminal, Action::Reduce(production_id), )?; } - - insert_action( - &mut action_table, - state_id, - Terminal('$'), - Action::Reduce(production_id), - )?; } } } } - let state_infos: Vec = item_sets.iter().enumerate().map(|(id, item_set)| { - let items = item_set.iter().map(|item| LrItem { - production: item.production.clone(), - dot_pos: item.dot_pos, - }).collect(); - let mut transitions: Vec<(Symbol, usize)> = edges.iter() - .filter(|((state, _), _)| *state == id) - .map(|((_, sym), &next)| (sym.clone(), next)) - .collect(); - transitions.sort_by(|a, b| a.0.cmp(&b.0)); - StateInfo { id, items, transitions } - }).collect(); + Ok((action_table, goto_table)) +} - Ok(CompiledParser { - productions, - action_table, - goto_table, - start_state: 0, - state_count: item_sets.len(), - state_infos, - }) +/// 完成項が reduce を書き込む先読み集合。未完成項では空。 +/// +/// 拡張生成規則 S' -> S の完成項だけは reduce ではなく Accept を書くので、 +/// `build_tables` と同じく `$` だけを返す。 +fn item_reduce_lookaheads( + item: &Item, + reduce_lookaheads: &ReduceLookaheads, + augmented_start: NonTerminal, +) -> BTreeSet { + if item.dot_pos < item.production.right.len() { + return BTreeSet::new(); + } + if item.production.left == augmented_start { + return [EOF].into_iter().collect(); + } + reduce_lookaheads + .get(&item.production.left) + .cloned() + .unwrap_or_default() +} + +fn build_state_infos( + item_sets: &[BTreeSet], + edges: &Edges, + reduce_lookaheads: &ReduceLookaheads, + augmented_start: NonTerminal, +) -> Vec { + item_sets + .iter() + .enumerate() + .map(|(id, item_set)| { + let items = item_set + .iter() + .map(|item| LrItem { + production: item.production.clone(), + dot_pos: item.dot_pos, + reduce_lookaheads: item_reduce_lookaheads( + item, + reduce_lookaheads, + augmented_start, + ), + }) + .collect(); + let mut transitions: Vec<(Symbol, usize)> = edges + .iter() + .filter(|((state, _), _)| *state == id) + .map(|((_, sym), &next)| (sym.clone(), next)) + .collect(); + transitions.sort_by(|a, b| a.0.cmp(&b.0)); + StateInfo { + id, + items, + transitions, + } + }) + .collect() +} + +/// 拡張生成規則 S' -> S を先頭に挿入する。 +/// +/// S' には文法中で未使用の大文字を選ぶ。既存の非終端記号と衝突すると拡張生成規則が +/// 実在の生成規則と値として等しくなり、production id 0 の判定が Accept を誤って +/// 書き込む。非終端記号は単一大文字に限られるため、26 文字すべて使用済みなら失敗する。 +fn augment(grammar: &Grammar) -> Result<(Vec, NonTerminal), ParserError> { + let used = grammar.non_terminals(); + let augmented_start = ('A'..='Z') + .map(NonTerminal) + .find(|candidate| !used.contains(candidate)) + .ok_or(ParserError::NoAvailableStartSymbol)?; + + let mut productions = grammar.productions.clone(); + productions.insert( + 0, + Production { + left: augmented_start, + right: vec![Symbol::NonTerminal(grammar.start)], + }, + ); + + Ok((productions, augmented_start)) } fn insert_action( - table: &mut BTreeMap<(InternalState, Terminal), Action>, + table: &mut ActionTable, state: InternalState, terminal: Terminal, action: Action, @@ -149,7 +268,12 @@ fn insert_action( } std::collections::btree_map::Entry::Occupied(entry) => { if *entry.get() != action { - return Err(ParserError::ConflictReducer); + return Err(ParserError::Conflict(Conflict { + state, + terminal, + existing: *entry.get(), + incoming: action, + })); } } } @@ -241,6 +365,15 @@ type ProductionId = usize; pub struct LrItem { pub production: Production, pub dot_pos: usize, + /// この**完成項**が reduce を書き込む先読み集合。未完成項では常に空。 + /// + /// LR(0) では全終端記号、SLR(1) では FOLLOW 集合になるので、同じ項集合を + /// 見比べれば競合の理由が読める。 + /// + /// これは LR(1) / LALR(1) の先読みとは**別概念**である。LR(1) の先読みは項の + /// 同一性を決める要素で未完成項にも付き、状態分裂の原因そのものになる。 + /// 両者を一つのフィールドに統合できるかは LR(1) を実装してから判断する。 + pub reduce_lookaheads: BTreeSet, } #[derive(Debug, Clone)] @@ -252,8 +385,8 @@ pub struct StateInfo { pub struct CompiledParser { productions: Vec, - action_table: BTreeMap<(InternalState, Terminal), Action>, - goto_table: BTreeMap<(InternalState, NonTerminal), InternalState>, + action_table: ActionTable, + goto_table: GotoTable, start_state: InternalState, state_count: usize, state_infos: Vec, @@ -298,20 +431,199 @@ pub enum Action { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParserError { - ConflictReducer, + Conflict(Conflict), MissingProduction, + /// 大文字 26 文字がすべて非終端記号として使われており、拡張開始記号を作れない。 + NoAvailableStartSymbol, +} + +/// 同じ (状態, 先読み) セルを 2 つの action が奪い合っている状態。 +/// +/// `existing` と `incoming` の順序は表の構築順に由来する実装詳細なので、 +/// 表示は順序に依存しない形にすること。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Conflict { + pub state: InternalState, + pub terminal: Terminal, + pub existing: Action, + pub incoming: Action, +} + +impl Conflict { + /// "Shift/Reduce" のような種別ラベル。2 つの action の順序には依存しない。 + pub fn kind(&self) -> &'static str { + match (self.existing, self.incoming) { + (Action::Shift(_), Action::Reduce(_)) | (Action::Reduce(_), Action::Shift(_)) => { + "Shift/Reduce" + } + (Action::Reduce(_), Action::Reduce(_)) => "Reduce/Reduce", + _ => "Conflict", + } + } } #[cfg(test)] mod tests { use super::*; - use crate::grammar::parse_grammar_text; + use crate::ast::AstNode; + use crate::grammar::{parse_grammar_text, parse_input_text}; + use crate::runtime::run; #[test] fn compile_builds_start_state() { let grammar = parse_grammar_text("E -> E+B\nE -> B\nB -> 0\nB -> 1").unwrap(); - let machine = compile(&grammar).unwrap(); + let machine = compile_lr0(&grammar).unwrap(); assert_eq!(machine.start_state(), 0); } + + /// 開始記号 E の隣の文字 F が実在する非終端記号のため、拡張生成規則 F -> E が + /// 文法内の F -> E と値として等しくなり、production id 0 = Accept と誤認される。 + #[test] + fn augmented_start_does_not_collide_with_existing_non_terminal() { + let grammar = parse_grammar_text("E -> aF\nF -> E\nE -> b").unwrap(); + let machine = compile_lr0(&grammar).unwrap(); + let input = parse_input_text("ab").unwrap(); + + let result = run(&machine, &input).unwrap(); + + assert_eq!( + result.ast, + AstNode::NonTerminal( + 'E', + vec![ + AstNode::Terminal('a'), + AstNode::NonTerminal( + 'F', + vec![AstNode::NonTerminal('E', vec![AstNode::Terminal('b')])], + ), + ], + ) + ); + } + + /// `a` を読んだ状態は { S -> a·A, S -> a·, A -> ·b }。 + /// LR(0) は完成項の reduce を全終端記号に書くため `b` で Shift と衝突するが、 + /// SLR は FOLLOW(S) = {$} に絞るので競合しない。 + const SLR_ONLY: &str = "S -> aA\nS -> a\nA -> b"; + + #[test] + fn lr0_conflicts_on_grammar_that_slr_accepts() { + let grammar = parse_grammar_text(SLR_ONLY).unwrap(); + + assert!(matches!( + compile_lr0(&grammar), + Err(ParserError::Conflict(_)) + )); + assert!(compile_slr(&grammar).is_ok()); + } + + #[test] + fn lr0_conflict_reports_terminal_and_competing_actions() { + let grammar = parse_grammar_text(SLR_ONLY).unwrap(); + let Err(ParserError::Conflict(conflict)) = compile_lr0(&grammar) else { + panic!("expected a Shift/Reduce conflict"); + }; + + // 状態番号は項集合の生成順に依存するのでアサートしない。 + assert_eq!(conflict.terminal, Terminal('b')); + assert_eq!(conflict.kind(), "Shift/Reduce"); + } + + #[test] + fn slr_parses_grammar_rejected_by_lr0() { + let grammar = parse_grammar_text(SLR_ONLY).unwrap(); + let machine = compile_slr(&grammar).unwrap(); + + let long = run(&machine, &parse_input_text("ab").unwrap()).unwrap(); + assert_eq!( + long.ast, + AstNode::NonTerminal( + 'S', + vec![ + AstNode::Terminal('a'), + AstNode::NonTerminal('A', vec![AstNode::Terminal('b')]), + ], + ) + ); + + let short = run(&machine, &parse_input_text("a").unwrap()).unwrap(); + assert_eq!( + short.ast, + AstNode::NonTerminal('S', vec![AstNode::Terminal('a')]) + ); + } + + /// FOLLOW(E) = {'<', '>', $} が終端記号全体と一致するため、SLR の reduce 集合は + /// LR(0) と同じになり競合が残る。SLR は万能ではない。 + #[test] + fn paren_grammar_conflicts_under_both_lr0_and_slr() { + let grammar = parse_grammar_text(include_str!("../paren_reducer")).unwrap(); + + assert!(matches!( + compile_lr0(&grammar), + Err(ParserError::Conflict(_)) + )); + assert!(matches!( + compile_slr(&grammar), + Err(ParserError::Conflict(_)) + )); + } + + fn completed_item_lookaheads( + machine: &CompiledParser, + left: char, + right_len: usize, + ) -> BTreeSet { + machine + .state_infos() + .iter() + .flat_map(|info| info.items.iter()) + .find(|item| { + item.production.left == NonTerminal(left) + && item.production.right.len() == right_len + && item.dot_pos == right_len + }) + .map(|item| item.reduce_lookaheads.clone()) + .expect("completed item not found") + } + + /// 同じ完成項の先読みが、LR(0) の全終端記号から SLR の FOLLOW へ絞られる。 + /// これが画面上で 2 つの解析法を見比べられる根拠になる。 + #[test] + fn reduce_lookaheads_narrow_from_all_terminals_to_follow() { + let grammar = parse_grammar_text(include_str!("../reducer")).unwrap(); + + let by_lr0 = completed_item_lookaheads(&compile_lr0(&grammar).unwrap(), 'B', 1); + let by_slr = completed_item_lookaheads(&compile_slr(&grammar).unwrap(), 'B', 1); + + assert_eq!(by_lr0, "*+01$".chars().map(Terminal).collect()); + assert_eq!(by_slr, "*+$".chars().map(Terminal).collect()); + } + + #[test] + fn unfinished_items_carry_no_reduce_lookaheads() { + let grammar = parse_grammar_text(include_str!("../reducer")).unwrap(); + let machine = compile_slr(&grammar).unwrap(); + + for item in machine.state_infos().iter().flat_map(|info| info.items.iter()) { + if item.dot_pos < item.production.right.len() { + assert!( + item.reduce_lookaheads.is_empty(), + "unfinished item carried lookaheads: {item:?}" + ); + } + } + } + + #[test] + fn lr0_and_slr_agree_on_arithmetic_grammar() { + let grammar = parse_grammar_text(include_str!("../reducer")).unwrap(); + let input = parse_input_text("1+1*1").unwrap(); + + let by_lr0 = run(&compile_lr0(&grammar).unwrap(), &input).unwrap(); + let by_slr = run(&compile_slr(&grammar).unwrap(), &input).unwrap(); + + assert_eq!(by_lr0.ast, by_slr.ast); + } } diff --git a/src/pages/item_sets.rs b/src/pages/item_sets.rs new file mode 100644 index 0000000..2faf800 --- /dev/null +++ b/src/pages/item_sets.rs @@ -0,0 +1,122 @@ +use eframe::egui; +use lr0_parser_rs::grammar::Symbol; +use lr0_parser_rs::{LrItem, StateInfo}; + +/// 項集合パネル。状態ごとに LR(0) 項を並べ、完成項には reduce 先読みを添える。 +/// +/// 先読みは LR(0) では全終端記号、SLR(1) では FOLLOW 集合になる。同じ文法で解析法を +/// 切り替えるとこの差がそのまま見えるので、競合が起きる理由を画面上で読める。 +pub(super) fn show_item_sets( + ui: &mut egui::Ui, + state_infos: &[StateInfo], + active_state: Option, +) { + if state_infos.is_empty() { + ui.label( + egui::RichText::new("No item sets yet. Parse a grammar first.") + .size(13.0) + .color(egui::Color32::GRAY), + ); + return; + } + + for info in state_infos { + let is_active = active_state == Some(info.id); + let mut title = egui::RichText::new(format!("State {}", info.id)).size(14.0); + if is_active { + title = title.strong().color(egui::Color32::from_rgb(255, 220, 50)); + } + + egui::CollapsingHeader::new(title) + .default_open(is_active || info.id == 0) + .show(ui, |ui| { + for item in &info.items { + ui.label(egui::RichText::new(format_item(item)).monospace().size(13.0)); + } + }); + } +} + +/// `E -> E · + B` 形式。完成項には reduce を書き込む先読み集合を添える。 +fn format_item(item: &LrItem) -> String { + let mut rhs = String::new(); + for (index, symbol) in item.production.right.iter().enumerate() { + if index == item.dot_pos { + rhs.push_str("· "); + } + rhs.push(symbol_char(symbol)); + rhs.push(' '); + } + if item.dot_pos >= item.production.right.len() { + rhs.push('·'); + } + + let text = format!("{} -> {}", item.production.left.0, rhs.trim_end()); + + if item.reduce_lookaheads.is_empty() { + return text; + } + + let lookaheads = item + .reduce_lookaheads + .iter() + .map(|terminal| terminal.0.to_string()) + .collect::>() + .join(" "); + + format!("{text:<24}{{ {lookaheads} }}") +} + +fn symbol_char(symbol: &Symbol) -> char { + match symbol { + Symbol::Terminal(terminal) => terminal.0, + Symbol::NonTerminal(non_terminal) => non_terminal.0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lr0_parser_rs::grammar::{NonTerminal, Production, Terminal}; + + fn item(left: char, right: &str, dot_pos: usize, lookaheads: &str) -> LrItem { + LrItem { + production: Production { + left: NonTerminal(left), + right: right + .chars() + .map(|c| { + if c.is_ascii_uppercase() { + Symbol::NonTerminal(NonTerminal(c)) + } else { + Symbol::Terminal(Terminal(c)) + } + }) + .collect(), + }, + dot_pos, + reduce_lookaheads: lookaheads.chars().map(Terminal).collect(), + } + } + + #[test] + fn unfinished_item_shows_dot_before_next_symbol_and_no_lookaheads() { + assert_eq!(format_item(&item('S', "aA", 1, "")), "S -> a · A"); + } + + #[test] + fn completed_item_shows_trailing_dot_and_lookaheads() { + let formatted = format_item(&item('S', "a", 1, "$")); + + assert!(formatted.starts_with("S -> a ·"), "{formatted}"); + assert!(formatted.ends_with("{ $ }"), "{formatted}"); + } + + /// 右辺が空の生成規則は常に完成項として扱われる。 + #[test] + fn epsilon_production_is_rendered_as_completed() { + let formatted = format_item(&item('A', "", 0, "c")); + + assert!(formatted.starts_with("A -> ·"), "{formatted}"); + } +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 53159f5..4f1caab 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,3 +1,4 @@ +mod item_sets; mod tree; pub mod generator; pub mod parser; diff --git a/src/pages/parser.rs b/src/pages/parser.rs index 64984b9..917f755 100644 --- a/src/pages/parser.rs +++ b/src/pages/parser.rs @@ -1,9 +1,10 @@ use eframe::egui; use lr0_parser_rs::grammar::{Grammar, GrammarError, Symbol, parse_grammar_text, parse_input_text}; -use lr0_parser_rs::lr::{CompiledParser, ParserError, compile}; +use lr0_parser_rs::lr::{Action, CompiledParser, ParserError, compile_lr0, compile_slr}; use lr0_parser_rs::runtime::{RuntimeError, run}; use lr0_parser_rs::{StateInfo, StepAction}; use std::fmt; +use super::item_sets::show_item_sets; use super::tree::{draw_tree, layout_ast, tree_pixel_height, H_GAP, NODE_R}; use crate::app::{ @@ -20,6 +21,15 @@ enum UiError { NotImplemented(String), } +/// パース表のセルに入る action の表示。実行トレース用の `render_action_label` とは別物。 +fn table_action_label(action: Action) -> String { + match action { + Action::Shift(state) => format!("shift({state})"), + Action::Reduce(production) => format!("reduce({production})"), + Action::Accept => "accept".to_string(), + } +} + impl fmt::Display for UiError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -38,12 +48,23 @@ impl fmt::Display for UiError { UiError::Grammar(GrammarError::InvalidSymbol(c)) => { write!(f, "Invalid symbol '{c}'. Non-terminals must be uppercase ASCII.") } - UiError::Compile(ParserError::ConflictReducer) => { - write!(f, "LR conflict: grammar is not LR(0). Check for ambiguous productions.") + UiError::Compile(ParserError::Conflict(conflict)) => { + write!( + f, + "LR conflict ({}): 状態 {} で先読み '{}' に対し {} と {} が競合しています。", + conflict.kind(), + conflict.state, + conflict.terminal.0, + table_action_label(conflict.existing), + table_action_label(conflict.incoming), + ) } UiError::Compile(ParserError::MissingProduction) => { write!(f, "Internal error: production not found during compile.") } + UiError::Compile(ParserError::NoAvailableStartSymbol) => { + write!(f, "All 26 uppercase letters are used as non-terminals; cannot create an augmented start symbol.") + } UiError::Runtime(RuntimeError::InvalidAction) => { write!(f, "Parse error: unexpected token in input. Check that the input matches the grammar.") } @@ -104,14 +125,20 @@ struct RunRequest { input_symbols: Vec, } +/// 解析法ごとの表構築関数。`ParserKind` から選ぶ。 +type CompileFn = fn(&Grammar) -> Result; + /// grammar text の parse → compile を依存的な逐次チェーンとして実行する。 /// parse が失敗すれば compile は行わない。 -fn validated_compile(grammar_text: &str) -> Validation { +fn validated_compile( + grammar_text: &str, + compile_fn: CompileFn, +) -> Validation { let grammar = match parse_grammar_text(grammar_text) { Ok(g) => g, Err(e) => return Validation::invalid(ParsePreparationError::Grammar(e)), }; - match compile(&grammar) { + match compile_fn(&grammar) { Ok(machine) => Validation::valid(CompiledGrammar { grammar, machine }), Err(e) => Validation::invalid(ParsePreparationError::Compile(e)), } @@ -151,6 +178,8 @@ impl ParserApp { } ui.add_space(12.0); self.show_state_machine_panel(ui, &view); + ui.add_space(12.0); + self.show_item_sets_panel(ui, &view); }); }); }); @@ -676,21 +705,41 @@ impl ParserApp { }); } + fn show_item_sets_panel(&self, ui: &mut egui::Ui, view: &TraceCursorView) { + ui.add_space(10.0); + ui.label(egui::RichText::new("Item Sets:").size(16.0)); + ui.add_space(8.0); + + let state_infos: &[StateInfo] = match &self.parser.status { + ParserStatus::Ready(artifacts) => &artifacts.state_infos, + ParserStatus::Empty => &[], + }; + let active_state = view.sm_highlight().source_state; + + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.set_min_width(ui.available_width()); + show_item_sets(ui, state_infos, active_state); + }); + } + fn handle_parse(&mut self) { - match self.parser.selected_kind { - ParserKind::Lr0 => self.handle_parse_lr0(), + let compile_fn: CompileFn = match self.parser.selected_kind { + ParserKind::Lr0 => compile_lr0, + ParserKind::Slr => compile_slr, other => { self.parser.result = UiError::NotImplemented(other.label().to_string()).to_string(); self.parser.parse_trace.clear(); self.parser.status = ParserStatus::Empty; + return; } - } + }; + self.handle_parse_with(compile_fn); } - fn handle_parse_lr0(&mut self) { + fn handle_parse_with(&mut self, compile_fn: CompileFn) { // ── フェーズ1: 独立な2チェーンを Applicative 的に合成 ────────────────── - let compiled = validated_compile(&self.workspace.reducer_string); + let compiled = validated_compile(&self.workspace.reducer_string, compile_fn); let input = validate_input(&self.workspace.input_string); let request = match compiled.map2(input, |cg, symbols| RunRequest { @@ -715,25 +764,8 @@ impl ParserApp { self.workspace.terminals = terminals_from_grammar(&request.grammar); self.apply_default_terminal_types(); - // ── フェーズ3: run(RunRequest への依存的な逐次処理) ────────────────── - match run(&request.machine, &request.input_symbols).map_err(UiError::Runtime) { - Ok(_) => { - self.parser.result.clear(); - } - Err(e) => { - self.parser.result = e.to_string(); - self.parser.parse_trace.clear(); - // parse table は表示するが SM は空で返す - self.parser.status = ParserStatus::Ready(ParseArtifacts { - symbols, - table, - state_infos: vec![], - accept_states: vec![], - }); - return; - } - } - + // 状態機械と項集合は compile が成功した時点で確定しており、run の成否とは独立。 + // 入力エラーのときこそ項集合を見たい場面なので、run より前に組み立てる。 let state_infos = request.machine.state_infos().to_vec(); let accept_states: Vec = table.iter().enumerate() .filter(|(_, row)| row.iter().any(|a| matches!(a, ParseTableAction::Accept))) @@ -746,6 +778,14 @@ impl ParserApp { accept_states, }); + // ── フェーズ3: run(RunRequest への依存的な逐次処理) ────────────────── + if let Err(e) = run(&request.machine, &request.input_symbols).map_err(UiError::Runtime) { + self.parser.result = e.to_string(); + self.parser.parse_trace.clear(); + return; + } + self.parser.result.clear(); + self.parser.parse_trace = build_animation_trace(&request.machine, &request.input_symbols).unwrap_or_default(); self.parser.trace_cursor = 0; self.parser.anim_playing = false; @@ -1072,18 +1112,33 @@ mod tests { const VALID_GRAMMAR: &str = "E -> E+B\nE -> B\nB -> 0\nB -> 1"; // E -> EE が Shift/Reduce 競合を引き起こし compile が失敗する文法 const CONFLICT_GRAMMAR: &str = "E -> <>\nE -> \nE -> EE"; + // LR(0) では 'b' で Shift/Reduce が競合するが SLR では通る文法 + const SLR_ONLY_GRAMMAR: &str = "S -> aA\nS -> a\nA -> b"; + + /// 解析法の選択が compile 関数として注入され、同じ文法でも結果が変わる。 + #[test] + fn parser_kind_selects_the_compile_function() { + assert!(matches!( + validated_compile(SLR_ONLY_GRAMMAR, compile_slr), + Validation::Valid(_) + )); + assert!(matches!( + validated_compile(SLR_ONLY_GRAMMAR, compile_lr0), + Validation::Invalid(ref errs) if matches!(errs[0], ParsePreparationError::Compile(_)) + )); + } // ── validated_compile ──────────────────────────────────────────── #[test] fn validated_compile_valid_grammar_returns_valid() { - assert!(matches!(validated_compile(VALID_GRAMMAR), Validation::Valid(_))); + assert!(matches!(validated_compile(VALID_GRAMMAR, compile_lr0), Validation::Valid(_))); } #[test] fn validated_compile_empty_grammar_returns_grammar_error() { assert!(matches!( - validated_compile(""), + validated_compile("", compile_lr0), Validation::Invalid(ref errs) if matches!(errs[0], ParsePreparationError::Grammar(_)) )); } @@ -1091,11 +1146,28 @@ mod tests { #[test] fn validated_compile_conflict_grammar_returns_compile_error() { assert!(matches!( - validated_compile(CONFLICT_GRAMMAR), + validated_compile(CONFLICT_GRAMMAR, compile_lr0), Validation::Invalid(ref errs) if matches!(errs[0], ParsePreparationError::Compile(_)) )); } + /// compile が成功していれば、run が失敗しても状態機械と項集合は表示できる。 + /// この性質は GUI を目視する以外に観測手段がないので、ここで固定しておく。 + #[test] + fn run_failure_still_exposes_state_infos() { + let mut app = ParserApp::default(); + app.workspace.reducer_string = VALID_GRAMMAR.to_string(); + app.workspace.input_string = "1++1".to_string(); + + app.handle_parse_with(compile_lr0); + + let ParserStatus::Ready(artifacts) = &app.parser.status else { + panic!("compile は成功しているので Ready のはず"); + }; + assert!(!artifacts.state_infos.is_empty()); + assert!(!app.parser.result.is_empty(), "run のエラーが表示されるはず"); + } + // ── validate_input ─────────────────────────────────────────────── #[test] @@ -1115,7 +1187,7 @@ mod tests { #[test] fn good_grammar_and_bad_input_yields_single_input_error() { - let result = validated_compile(VALID_GRAMMAR) + let result = validated_compile(VALID_GRAMMAR, compile_lr0) .map2(validate_input("X"), |_, _| unreachable!()); assert!(matches!( result, @@ -1127,7 +1199,7 @@ mod tests { #[test] fn compile_fail_and_bad_input_accumulates_both_errors() { // 設計価値の中心: compile 失敗と input 失敗が同時に蓄積される - let result = validated_compile(CONFLICT_GRAMMAR) + let result = validated_compile(CONFLICT_GRAMMAR, compile_lr0) .map2(validate_input("X"), |_, _| unreachable!()); assert!(matches!( result, diff --git a/src/runtime.rs b/src/runtime.rs index 9414d65..5e57f22 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -240,12 +240,12 @@ pub fn build_trace( mod tests { use super::*; use crate::grammar::{Symbol, Terminal, parse_grammar_text}; - use crate::lr::compile; + use crate::lr::compile_lr0; #[test] fn dump_trace_for_debug() { let grammar = parse_grammar_text("E -> E*B\nE -> E+B\nE -> B\nB -> 0\nB -> 1").unwrap(); - let machine = compile(&grammar).unwrap(); + let machine = compile_lr0(&grammar).unwrap(); let input = [ Symbol::Terminal(Terminal('1')), Symbol::Terminal(Terminal('+')), @@ -297,7 +297,7 @@ mod tests { #[test] fn run_returns_an_ast() { let grammar = parse_grammar_text("E -> E+B\nE -> B\nB -> 0\nB -> 1").unwrap(); - let machine = compile(&grammar).unwrap(); + let machine = compile_lr0(&grammar).unwrap(); let input = [ Symbol::Terminal(Terminal('1')), Symbol::Terminal(Terminal('+')),