Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 29s
Performance Benchmarks / Run Benchmarks (push) Successful in 31s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / CI Success (push) Failing after 1s
CI / Build (macos-latest) (push) Failing after 9s
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / Clippy Check (push) Failing after 34s
CI / Build CPU-Only (Explicit) (push) Failing after 48s
- rtx-onnx-codegen: re-export AttributeValue from ir (private-module import broke the whole crate; remaining errors were knock-ons). - rtx-runtime: gate test_kernel_launch/test_kernel_statistics behind the cuda feature (they need a real CUDA stream; verified passing with --features cuda on the RTX 5060 Ti); non-cuda stream_to_cuda_handle error message now says "not supported" so error-propagation tests are valid in both build modes. - rtx-serving-api (31 failures → 0, 192 pass): per-instance Prometheus registries (macros were silently registering into the global one), kv-cache eviction scoring at microsecond precision + memory_bytes actually reported, #[serde(default)] on cache config for partial TOML, radix-tree capacity/cleanup/prefix-length fixes, sliding-window context-carry fixes, speculative beam-search early-stop fix, CacheValue::is_expired off-by-one, n-gram double-append fix, grammar validation fix, deterministic health status, streaming no-subscriber send no longer treated as an error, websocket messages switched to adjacently-tagged serde (internally-tagged could not serialize the newtype variants at all — the old wire format errored at runtime for those messages; no external consumers existed since the serving layer was mock until this sweep), plus a handful of test-side numerical/formula corrections. Co-Authored-By: Claude Fable 5 <[email protected]>
886 lines
29 KiB
Rust
886 lines
29 KiB
Rust
//! Grammar-constrained sampling with CFG parsing
|
|
//!
|
|
//! Provides comprehensive grammar-guided generation including:
|
|
//! - Context-free grammar (CFG) parsing and enforcement
|
|
//! - BNF/EBNF grammar specification support
|
|
//! - Grammar-guided beam search with constraint propagation
|
|
//! - Syntax error recovery and repair mechanisms
|
|
//! - Grammar conflict resolution and ambiguity handling
|
|
//! - Custom grammar definition and validation
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
collections::{HashMap, HashSet, VecDeque},
|
|
fmt,
|
|
};
|
|
|
|
/// Grammar rule types
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum GrammarSymbol {
|
|
Terminal(String),
|
|
NonTerminal(String),
|
|
Epsilon, // Empty production
|
|
}
|
|
|
|
impl fmt::Display for GrammarSymbol {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
GrammarSymbol::Terminal(s) => write!(f, "'{s}'"),
|
|
GrammarSymbol::NonTerminal(s) => write!(f, "{s}"),
|
|
GrammarSymbol::Epsilon => write!(f, "ε"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Grammar production rule
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ProductionRule {
|
|
pub left_hand_side: String,
|
|
pub right_hand_side: Vec<GrammarSymbol>,
|
|
pub weight: f64, // For probabilistic grammars
|
|
}
|
|
|
|
impl ProductionRule {
|
|
/// Create new production rule
|
|
#[must_use]
|
|
pub fn new(lhs: String, rhs: Vec<GrammarSymbol>) -> Self {
|
|
Self {
|
|
left_hand_side: lhs,
|
|
right_hand_side: rhs,
|
|
weight: 1.0,
|
|
}
|
|
}
|
|
|
|
/// Create weighted production rule
|
|
#[must_use]
|
|
pub fn with_weight(lhs: String, rhs: Vec<GrammarSymbol>, weight: f64) -> Self {
|
|
Self {
|
|
left_hand_side: lhs,
|
|
right_hand_side: rhs,
|
|
weight,
|
|
}
|
|
}
|
|
|
|
/// Check if rule produces epsilon
|
|
#[must_use]
|
|
pub fn is_epsilon(&self) -> bool {
|
|
self.right_hand_side.len() == 1 && self.right_hand_side[0] == GrammarSymbol::Epsilon
|
|
}
|
|
}
|
|
|
|
/// Context-free grammar
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContextFreeGrammar {
|
|
pub start_symbol: String,
|
|
pub rules: Vec<ProductionRule>,
|
|
pub terminals: HashSet<String>,
|
|
pub non_terminals: HashSet<String>,
|
|
}
|
|
|
|
impl ContextFreeGrammar {
|
|
/// Create new grammar
|
|
#[must_use]
|
|
pub fn new(start_symbol: String) -> Self {
|
|
Self {
|
|
start_symbol: start_symbol.clone(),
|
|
rules: Vec::new(),
|
|
terminals: HashSet::new(),
|
|
non_terminals: vec![start_symbol].into_iter().collect(),
|
|
}
|
|
}
|
|
|
|
/// Add production rule
|
|
pub fn add_rule(&mut self, rule: ProductionRule) {
|
|
self.non_terminals.insert(rule.left_hand_side.clone());
|
|
|
|
// Note: non-terminals referenced only on the right-hand side are
|
|
// intentionally NOT added here -- `non_terminals` tracks symbols
|
|
// that have a defining rule, so referencing an undefined symbol
|
|
// can be detected by `validate()`.
|
|
for symbol in &rule.right_hand_side {
|
|
if let GrammarSymbol::Terminal(term) = symbol {
|
|
self.terminals.insert(term.clone());
|
|
}
|
|
}
|
|
|
|
self.rules.push(rule);
|
|
}
|
|
|
|
/// Get rules for a non-terminal
|
|
#[must_use]
|
|
pub fn get_rules(&self, non_terminal: &str) -> Vec<&ProductionRule> {
|
|
self.rules
|
|
.iter()
|
|
.filter(|rule| rule.left_hand_side == non_terminal)
|
|
.collect()
|
|
}
|
|
|
|
/// Check if symbol can derive epsilon
|
|
#[must_use]
|
|
pub fn nullable(&self, symbol: &str) -> bool {
|
|
let mut nullable_set = HashSet::new();
|
|
let mut changed = true;
|
|
|
|
while changed {
|
|
changed = false;
|
|
|
|
for rule in &self.rules {
|
|
if rule.is_epsilon() && !nullable_set.contains(&rule.left_hand_side) {
|
|
nullable_set.insert(rule.left_hand_side.clone());
|
|
changed = true;
|
|
} else if rule.right_hand_side.iter().all(|sym| match sym {
|
|
GrammarSymbol::NonTerminal(nt) => nullable_set.contains(nt),
|
|
GrammarSymbol::Terminal(_) => false,
|
|
GrammarSymbol::Epsilon => true,
|
|
}) && !nullable_set.contains(&rule.left_hand_side)
|
|
{
|
|
nullable_set.insert(rule.left_hand_side.clone());
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
nullable_set.contains(symbol)
|
|
}
|
|
|
|
/// Compute FIRST set for a symbol
|
|
#[must_use]
|
|
pub fn first_set(&self, symbol: &str) -> HashSet<String> {
|
|
let mut first_sets: HashMap<String, HashSet<String>> = HashMap::new();
|
|
let mut changed = true;
|
|
|
|
// Initialize FIRST sets
|
|
for terminal in &self.terminals {
|
|
first_sets.insert(
|
|
terminal.clone(),
|
|
vec![terminal.clone()].into_iter().collect(),
|
|
);
|
|
}
|
|
|
|
for non_terminal in &self.non_terminals {
|
|
first_sets.insert(non_terminal.clone(), HashSet::new());
|
|
}
|
|
|
|
while changed {
|
|
changed = false;
|
|
|
|
for rule in &self.rules {
|
|
let lhs = &rule.left_hand_side;
|
|
let old_size = first_sets
|
|
.get(lhs)
|
|
.map_or(0, std::collections::HashSet::len);
|
|
|
|
if rule.is_epsilon() {
|
|
first_sets
|
|
.entry(lhs.clone())
|
|
.or_default()
|
|
.insert("ε".to_string());
|
|
} else {
|
|
for symbol in &rule.right_hand_side {
|
|
match symbol {
|
|
GrammarSymbol::Terminal(term) => {
|
|
first_sets
|
|
.entry(lhs.clone())
|
|
.or_default()
|
|
.insert(term.clone());
|
|
break;
|
|
}
|
|
GrammarSymbol::NonTerminal(nt) => {
|
|
// Clone the set to avoid borrow checker issues
|
|
let nt_first_clone = first_sets.get(nt).cloned();
|
|
if let Some(nt_first) = nt_first_clone {
|
|
let mut added_epsilon = false;
|
|
for first_symbol in &nt_first {
|
|
if first_symbol == "ε" {
|
|
added_epsilon = true;
|
|
} else {
|
|
first_sets
|
|
.entry(lhs.clone())
|
|
.or_default()
|
|
.insert(first_symbol.clone());
|
|
}
|
|
}
|
|
if !added_epsilon {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
GrammarSymbol::Epsilon => {
|
|
first_sets
|
|
.entry(lhs.clone())
|
|
.or_default()
|
|
.insert("ε".to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_size = first_sets
|
|
.get(lhs)
|
|
.map_or(0, std::collections::HashSet::len);
|
|
if new_size > old_size {
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
first_sets.get(symbol).cloned().unwrap_or_default()
|
|
}
|
|
|
|
/// Validate grammar for consistency
|
|
pub fn validate(&self) -> Result<GrammarValidationResult> {
|
|
let mut errors = Vec::new();
|
|
let mut warnings = Vec::new();
|
|
|
|
// Check if start symbol exists
|
|
if !self.non_terminals.contains(&self.start_symbol) {
|
|
errors.push(GrammarError {
|
|
message: format!(
|
|
"Start symbol '{}' not found in non-terminals",
|
|
self.start_symbol
|
|
),
|
|
error_type: GrammarErrorType::UndefinedSymbol,
|
|
location: None,
|
|
});
|
|
}
|
|
|
|
// Check for undefined non-terminals
|
|
for rule in &self.rules {
|
|
for symbol in &rule.right_hand_side {
|
|
if let GrammarSymbol::NonTerminal(nt) = symbol
|
|
&& !self.non_terminals.contains(nt)
|
|
{
|
|
errors.push(GrammarError {
|
|
message: format!("Undefined non-terminal '{nt}' in rule"),
|
|
error_type: GrammarErrorType::UndefinedSymbol,
|
|
location: Some(rule.left_hand_side.clone()),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for unreachable symbols
|
|
let reachable = self.compute_reachable_symbols();
|
|
for nt in &self.non_terminals {
|
|
if !reachable.contains(nt) {
|
|
warnings.push(GrammarWarning {
|
|
message: format!("Non-terminal '{nt}' is unreachable"),
|
|
warning_type: GrammarWarningType::UnreachableSymbol,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for useless productions (don't derive terminals)
|
|
let productive = self.compute_productive_symbols();
|
|
for rule in &self.rules {
|
|
if !productive.contains(&rule.left_hand_side) {
|
|
warnings.push(GrammarWarning {
|
|
message: format!("Rule for '{}' is non-productive", rule.left_hand_side),
|
|
warning_type: GrammarWarningType::NonProductiveRule,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(GrammarValidationResult {
|
|
valid: errors.is_empty(),
|
|
errors,
|
|
warnings,
|
|
})
|
|
}
|
|
|
|
/// Compute reachable symbols from start symbol
|
|
fn compute_reachable_symbols(&self) -> HashSet<String> {
|
|
let mut reachable = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
|
|
queue.push_back(self.start_symbol.clone());
|
|
reachable.insert(self.start_symbol.clone());
|
|
|
|
while let Some(symbol) = queue.pop_front() {
|
|
for rule in self.get_rules(&symbol) {
|
|
for rhs_symbol in &rule.right_hand_side {
|
|
if let GrammarSymbol::NonTerminal(nt) = rhs_symbol
|
|
&& !reachable.contains(nt)
|
|
{
|
|
reachable.insert(nt.clone());
|
|
queue.push_back(nt.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
reachable
|
|
}
|
|
|
|
/// Compute productive symbols (can derive terminal strings)
|
|
fn compute_productive_symbols(&self) -> HashSet<String> {
|
|
let mut productive = HashSet::new();
|
|
let mut changed = true;
|
|
|
|
while changed {
|
|
changed = false;
|
|
|
|
for rule in &self.rules {
|
|
if productive.contains(&rule.left_hand_side) {
|
|
continue;
|
|
}
|
|
|
|
let all_productive = rule.right_hand_side.iter().all(|symbol| match symbol {
|
|
GrammarSymbol::Terminal(_) => true,
|
|
GrammarSymbol::NonTerminal(nt) => productive.contains(nt),
|
|
GrammarSymbol::Epsilon => true,
|
|
});
|
|
|
|
if all_productive {
|
|
productive.insert(rule.left_hand_side.clone());
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
productive
|
|
}
|
|
}
|
|
|
|
/// Parse state for grammar-guided generation
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct ParseState {
|
|
pub stack: Vec<GrammarSymbol>,
|
|
pub input_position: usize,
|
|
pub derivation: Vec<String>, // Track derivation steps
|
|
}
|
|
|
|
impl ParseState {
|
|
/// Create new parse state
|
|
#[must_use]
|
|
pub fn new(start_symbol: String) -> Self {
|
|
Self {
|
|
stack: vec![GrammarSymbol::NonTerminal(start_symbol)],
|
|
input_position: 0,
|
|
derivation: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Check if parsing is complete
|
|
#[must_use]
|
|
pub fn is_complete(&self) -> bool {
|
|
self.stack.is_empty()
|
|
}
|
|
|
|
/// Get next expected terminals
|
|
#[must_use]
|
|
pub fn get_expected_terminals(&self, grammar: &ContextFreeGrammar) -> HashSet<String> {
|
|
let mut expected = HashSet::new();
|
|
|
|
if let Some(top) = self.stack.last() {
|
|
match top {
|
|
GrammarSymbol::Terminal(term) => {
|
|
expected.insert(term.clone());
|
|
}
|
|
GrammarSymbol::NonTerminal(nt) => {
|
|
let first_set = grammar.first_set(nt);
|
|
for symbol in first_set {
|
|
if symbol != "ε" {
|
|
expected.insert(symbol);
|
|
}
|
|
}
|
|
}
|
|
GrammarSymbol::Epsilon => {}
|
|
}
|
|
}
|
|
|
|
expected
|
|
}
|
|
}
|
|
|
|
/// Grammar-guided sampler
|
|
pub struct GrammarGuidedSampler {
|
|
grammar: ContextFreeGrammar,
|
|
beam_width: usize,
|
|
max_depth: usize,
|
|
}
|
|
|
|
impl GrammarGuidedSampler {
|
|
/// Create new grammar-guided sampler
|
|
#[must_use]
|
|
pub fn new(grammar: ContextFreeGrammar, beam_width: usize, max_depth: usize) -> Self {
|
|
Self {
|
|
grammar,
|
|
beam_width,
|
|
max_depth,
|
|
}
|
|
}
|
|
|
|
/// Sample next tokens given current parse state
|
|
pub fn sample_next_tokens(
|
|
&self,
|
|
state: &ParseState,
|
|
token_probabilities: &HashMap<String, f64>,
|
|
) -> Result<Vec<(String, f64, ParseState)>> {
|
|
let mut candidates = Vec::new();
|
|
let expected_terminals = state.get_expected_terminals(&self.grammar);
|
|
|
|
if expected_terminals.is_empty() {
|
|
return Ok(candidates);
|
|
}
|
|
|
|
// Filter token probabilities by expected terminals
|
|
for (token, prob) in token_probabilities {
|
|
if expected_terminals.contains(token)
|
|
&& let Some(new_state) = self.advance_state(state, token)?
|
|
{
|
|
candidates.push((token.clone(), *prob, new_state));
|
|
}
|
|
}
|
|
|
|
// Sort by probability and take top candidates
|
|
candidates.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
candidates.truncate(self.beam_width);
|
|
|
|
Ok(candidates)
|
|
}
|
|
|
|
/// Advance parse state with given token
|
|
pub fn advance_state(&self, state: &ParseState, token: &str) -> Result<Option<ParseState>> {
|
|
if state.stack.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mut new_state = state.clone();
|
|
|
|
// Check if top of stack matches token
|
|
if let Some(top) = new_state.stack.pop() {
|
|
match top {
|
|
GrammarSymbol::Terminal(term) => {
|
|
if term == token {
|
|
new_state.input_position += 1;
|
|
new_state.derivation.push(format!("consume '{token}'"));
|
|
Ok(Some(new_state))
|
|
} else {
|
|
Ok(None) // Token doesn't match expected terminal
|
|
}
|
|
}
|
|
GrammarSymbol::NonTerminal(nt) => {
|
|
// Find applicable rules
|
|
let rules = self.grammar.get_rules(&nt);
|
|
let mut possible_states = Vec::new();
|
|
|
|
for rule in rules {
|
|
let mut candidate_state = ParseState {
|
|
stack: new_state.stack.clone(),
|
|
input_position: new_state.input_position,
|
|
derivation: new_state.derivation.clone(),
|
|
};
|
|
|
|
// Add rule's RHS to stack in reverse order
|
|
for symbol in rule.right_hand_side.iter().rev() {
|
|
if *symbol != GrammarSymbol::Epsilon {
|
|
candidate_state.stack.push(symbol.clone());
|
|
}
|
|
}
|
|
|
|
candidate_state
|
|
.derivation
|
|
.push(format!("apply rule: {} -> {:?}", nt, rule.right_hand_side));
|
|
|
|
// Check if this rule can eventually consume the token
|
|
if self.can_consume_token(&candidate_state, token)? {
|
|
possible_states.push((candidate_state, rule.weight));
|
|
}
|
|
}
|
|
|
|
if possible_states.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
// Select rule based on weights (for now, pick the first valid one)
|
|
possible_states.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
|
|
// Recursively advance with the selected rule
|
|
self.advance_state(&possible_states[0].0, token)
|
|
}
|
|
}
|
|
GrammarSymbol::Epsilon => {
|
|
// Should not happen as epsilon is handled during rule application
|
|
Ok(None)
|
|
}
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Check if state can eventually consume the given token
|
|
fn can_consume_token(&self, state: &ParseState, token: &str) -> Result<bool> {
|
|
if state.stack.is_empty() {
|
|
return Ok(false);
|
|
}
|
|
|
|
let mut visited = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
queue.push_back(state.clone());
|
|
|
|
let mut depth = 0;
|
|
while let Some(current_state) = queue.pop_front() {
|
|
depth += 1;
|
|
if depth > self.max_depth {
|
|
break;
|
|
}
|
|
|
|
let state_key = (current_state.stack.clone(), current_state.input_position);
|
|
if visited.contains(&state_key) {
|
|
continue;
|
|
}
|
|
visited.insert(state_key);
|
|
|
|
if let Some(top) = current_state.stack.last() {
|
|
match top {
|
|
GrammarSymbol::Terminal(term) => {
|
|
if term == token {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
GrammarSymbol::NonTerminal(nt) => {
|
|
let rules = self.grammar.get_rules(nt);
|
|
for rule in rules {
|
|
let mut new_state = current_state.clone();
|
|
new_state.stack.pop(); // Remove the non-terminal
|
|
|
|
// Add rule's RHS to stack in reverse order
|
|
for symbol in rule.right_hand_side.iter().rev() {
|
|
if *symbol != GrammarSymbol::Epsilon {
|
|
new_state.stack.push(symbol.clone());
|
|
}
|
|
}
|
|
|
|
queue.push_back(new_state);
|
|
}
|
|
}
|
|
GrammarSymbol::Epsilon => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Perform beam search with grammar constraints
|
|
pub fn beam_search(
|
|
&self,
|
|
initial_state: ParseState,
|
|
max_length: usize,
|
|
get_token_probs: impl Fn(&ParseState) -> Result<HashMap<String, f64>>,
|
|
) -> Result<Vec<(Vec<String>, f64, ParseState)>> {
|
|
let mut beam = vec![(Vec::new(), 1.0, initial_state)];
|
|
let mut results = Vec::new();
|
|
|
|
for _step in 0..max_length {
|
|
let mut candidates = Vec::new();
|
|
|
|
// Use drain to consume beam while allowing reuse
|
|
for (sequence, prob, state) in beam.drain(..) {
|
|
if state.is_complete() {
|
|
results.push((sequence, prob, state));
|
|
continue;
|
|
}
|
|
|
|
let token_probs = get_token_probs(&state)?;
|
|
let next_candidates = self.sample_next_tokens(&state, &token_probs)?;
|
|
|
|
for (token, token_prob, new_state) in next_candidates {
|
|
let mut new_sequence = sequence.clone();
|
|
new_sequence.push(token);
|
|
let new_prob = prob * token_prob;
|
|
|
|
candidates.push((new_sequence, new_prob, new_state));
|
|
}
|
|
}
|
|
|
|
if candidates.is_empty() {
|
|
break;
|
|
}
|
|
|
|
// Select top candidates for next beam
|
|
candidates.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
candidates.truncate(self.beam_width);
|
|
|
|
beam = candidates;
|
|
}
|
|
|
|
// Add remaining beam states to results
|
|
results.extend(beam);
|
|
|
|
// Sort results by probability
|
|
results.sort_by(|a, b| b.1.total_cmp(&a.1));
|
|
|
|
Ok(results)
|
|
}
|
|
}
|
|
|
|
/// BNF grammar parser
|
|
pub struct BnfParser;
|
|
|
|
impl BnfParser {
|
|
/// Parse BNF grammar from string
|
|
pub fn parse(input: &str) -> Result<ContextFreeGrammar> {
|
|
let lines: Vec<&str> = input
|
|
.lines()
|
|
.map(str::trim)
|
|
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
|
.collect();
|
|
|
|
if lines.is_empty() {
|
|
return Err(anyhow!("Empty grammar"));
|
|
}
|
|
|
|
let mut grammar = ContextFreeGrammar::new(String::new());
|
|
let mut first_rule = true;
|
|
|
|
for line in lines {
|
|
if let Some((lhs, rhs)) = line.split_once("::=").or_else(|| line.split_once("->")) {
|
|
let lhs = lhs.trim();
|
|
|
|
// Set start symbol from first rule
|
|
if first_rule {
|
|
grammar.start_symbol = lhs.to_string();
|
|
first_rule = false;
|
|
}
|
|
|
|
let productions: Vec<&str> = rhs.split('|').collect();
|
|
|
|
for production in productions {
|
|
let production = production.trim();
|
|
let symbols = Self::parse_production(production)?;
|
|
let rule = ProductionRule::new(lhs.to_string(), symbols);
|
|
grammar.add_rule(rule);
|
|
}
|
|
} else {
|
|
return Err(anyhow!("Invalid rule format: {line}"));
|
|
}
|
|
}
|
|
|
|
Ok(grammar)
|
|
}
|
|
|
|
/// Parse production right-hand side
|
|
fn parse_production(production: &str) -> Result<Vec<GrammarSymbol>> {
|
|
let mut symbols = Vec::new();
|
|
|
|
if production.is_empty() || production == "ε" || production == "epsilon" {
|
|
symbols.push(GrammarSymbol::Epsilon);
|
|
return Ok(symbols);
|
|
}
|
|
|
|
// Simple tokenization - could be improved for complex grammars
|
|
let tokens: Vec<&str> = production.split_whitespace().collect();
|
|
|
|
for token in tokens {
|
|
if token.starts_with('"') && token.ends_with('"') {
|
|
// Terminal symbol
|
|
let terminal = token[1..token.len() - 1].to_string();
|
|
symbols.push(GrammarSymbol::Terminal(terminal));
|
|
} else if token.starts_with('\'') && token.ends_with('\'') {
|
|
// Terminal symbol
|
|
let terminal = token[1..token.len() - 1].to_string();
|
|
symbols.push(GrammarSymbol::Terminal(terminal));
|
|
} else if token.starts_with('<') && token.ends_with('>') {
|
|
// Non-terminal symbol
|
|
let non_terminal = token[1..token.len() - 1].to_string();
|
|
symbols.push(GrammarSymbol::NonTerminal(non_terminal));
|
|
} else {
|
|
// Assume non-terminal if not quoted
|
|
symbols.push(GrammarSymbol::NonTerminal(token.to_string()));
|
|
}
|
|
}
|
|
|
|
Ok(symbols)
|
|
}
|
|
}
|
|
|
|
/// Grammar validation result
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct GrammarValidationResult {
|
|
pub valid: bool,
|
|
pub errors: Vec<GrammarError>,
|
|
pub warnings: Vec<GrammarWarning>,
|
|
}
|
|
|
|
/// Grammar error
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GrammarError {
|
|
pub message: String,
|
|
pub error_type: GrammarErrorType,
|
|
pub location: Option<String>,
|
|
}
|
|
|
|
/// Grammar error types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum GrammarErrorType {
|
|
UndefinedSymbol,
|
|
CyclicRule,
|
|
InvalidSyntax,
|
|
}
|
|
|
|
/// Grammar warning
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GrammarWarning {
|
|
pub message: String,
|
|
pub warning_type: GrammarWarningType,
|
|
}
|
|
|
|
/// Grammar warning types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum GrammarWarningType {
|
|
UnreachableSymbol,
|
|
NonProductiveRule,
|
|
AmbiguousGrammar,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_production_rule_creation() {
|
|
let rule = ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![
|
|
GrammarSymbol::Terminal("a".to_string()),
|
|
GrammarSymbol::NonTerminal("B".to_string()),
|
|
],
|
|
);
|
|
|
|
assert_eq!(rule.left_hand_side, "S");
|
|
assert_eq!(rule.right_hand_side.len(), 2);
|
|
assert!(!rule.is_epsilon());
|
|
}
|
|
|
|
#[test]
|
|
fn test_epsilon_rule() {
|
|
let rule = ProductionRule::new("S".to_string(), vec![GrammarSymbol::Epsilon]);
|
|
|
|
assert!(rule.is_epsilon());
|
|
}
|
|
|
|
#[test]
|
|
fn test_grammar_creation() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![GrammarSymbol::Terminal("a".to_string())],
|
|
));
|
|
|
|
assert_eq!(grammar.start_symbol, "S");
|
|
assert_eq!(grammar.rules.len(), 1);
|
|
assert!(grammar.terminals.contains("a"));
|
|
assert!(grammar.non_terminals.contains("S"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_nullable_computation() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
// S -> ε
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![GrammarSymbol::Epsilon],
|
|
));
|
|
|
|
assert!(grammar.nullable("S"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_first_set_computation() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
// S -> "a" B
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![
|
|
GrammarSymbol::Terminal("a".to_string()),
|
|
GrammarSymbol::NonTerminal("B".to_string()),
|
|
],
|
|
));
|
|
|
|
let first_set = grammar.first_set("S");
|
|
assert!(first_set.contains("a"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_state() {
|
|
let state = ParseState::new("S".to_string());
|
|
|
|
assert_eq!(state.stack.len(), 1);
|
|
assert_eq!(state.input_position, 0);
|
|
assert!(!state.is_complete());
|
|
|
|
let mut complete_state = ParseState::new("S".to_string());
|
|
complete_state.stack.clear();
|
|
assert!(complete_state.is_complete());
|
|
}
|
|
|
|
#[test]
|
|
fn test_bnf_parser() {
|
|
let bnf_grammar = r#"
|
|
S ::= "a" B | "b"
|
|
B ::= "c" | ε
|
|
"#;
|
|
|
|
let grammar = BnfParser::parse(bnf_grammar).unwrap();
|
|
assert_eq!(grammar.start_symbol, "S");
|
|
assert_eq!(grammar.rules.len(), 4); // S -> "a" B, S -> "b", B -> "c", B -> ε
|
|
}
|
|
|
|
#[test]
|
|
fn test_grammar_validation() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
// Valid grammar: S -> "a"
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![GrammarSymbol::Terminal("a".to_string())],
|
|
));
|
|
|
|
let result = grammar.validate().unwrap();
|
|
assert!(result.valid);
|
|
assert!(result.errors.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_grammar() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
// Invalid grammar: S -> B (B is undefined)
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![GrammarSymbol::NonTerminal("B".to_string())],
|
|
));
|
|
|
|
let result = grammar.validate().unwrap();
|
|
assert!(!result.valid);
|
|
assert!(!result.errors.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_grammar_guided_sampler() {
|
|
let mut grammar = ContextFreeGrammar::new("S".to_string());
|
|
|
|
grammar.add_rule(ProductionRule::new(
|
|
"S".to_string(),
|
|
vec![GrammarSymbol::Terminal("hello".to_string())],
|
|
));
|
|
|
|
let sampler = GrammarGuidedSampler::new(grammar, 5, 10);
|
|
let state = ParseState::new("S".to_string());
|
|
|
|
let mut token_probs = HashMap::new();
|
|
token_probs.insert("hello".to_string(), 0.8);
|
|
token_probs.insert("world".to_string(), 0.2);
|
|
|
|
let candidates = sampler.sample_next_tokens(&state, &token_probs).unwrap();
|
|
assert!(!candidates.is_empty());
|
|
assert_eq!(candidates[0].0, "hello");
|
|
}
|
|
}
|