Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Started work on conflict API #55

Merged
merged 1 commit into from
Dec 12, 2018
Merged
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
20 changes: 18 additions & 2 deletions cfgrammar/src/lib/yacc/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,8 @@ where
}

/// Get the action return type as defined by the user
pub fn actiontype(&self) -> &Option<String> {
&self.actiontype
pub fn actiontype(&self) -> Option<&String> {
self.actiontype.as_ref()
}

/// Get the programs part of the grammar
Expand Down Expand Up @@ -557,6 +557,22 @@ where
}
}

/// Returns the string representation of a given production `pidx`.
pub fn pp_prod(&self, pidx: PIdx<StorageT>) -> String {
let mut sprod = String::new();
let ridx = self.prod_to_rule(pidx);
sprod.push_str(self.rule_name(ridx));
sprod.push_str(":");
for sym in self.prod(pidx) {
let s = match sym {
Symbol::Token(tidx) => self.token_name(*tidx).unwrap(),
Symbol::Rule(ridx) => self.rule_name(*ridx)
};
sprod.push_str(&format!(" \"{}\"", s));
}
sprod
}

/// Return a `SentenceGenerator` which can then generate minimal sentences for any rule
/// based on the user-defined `token_cost` function which gives the associated cost for
/// generating each token (where the cost must be greater than 0). Note that multiple
Expand Down
1 change: 1 addition & 0 deletions lrpar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ vob = "2.0"
regex = "1.0"

[dev-dependencies]
temp_testdir = "0.2"
189 changes: 176 additions & 13 deletions lrpar/src/lib/ctbuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use std::{
convert::AsRef,
env::{current_dir, var},
error::Error,
fmt::Debug,
fmt::{self, Debug},
fs::{self, read_to_string, File},
hash::Hash,
io::Write,
Expand All @@ -49,7 +49,7 @@ use cfgrammar::{
Symbol
};
use filetime::FileTime;
use lrtable::{from_yacc, Minimiser, StateGraph, StateTable};
use lrtable::{from_yacc, statetable::Conflicts, Minimiser, StateGraph, StateTable};
use num_traits::{AsPrimitive, PrimInt, Unsigned};
use regex::Regex;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -79,14 +79,72 @@ pub enum ActionKind {
GenericParseTree
}

struct CTConflictsError<StorageT: Eq + Hash> {
pub grm: YaccGrammar<StorageT>,
pub sgraph: StateGraph<StorageT>,
pub stable: StateTable<StorageT>
}

impl<StorageT> fmt::Display for CTConflictsError<StorageT>
where
StorageT: 'static + Debug + Hash + PrimInt + Serialize + TypeName + Unsigned,
usize: AsPrimitive<StorageT>,
u32: AsPrimitive<StorageT>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let conflicts = self.stable.conflicts().unwrap();
write!(
f,
"CTConflictsError{{{} Shift/Reduce, {} Reduce/Reduce}}",
conflicts.sr_len(),
conflicts.rr_len()
)
}
}

impl<StorageT> fmt::Debug for CTConflictsError<StorageT>
where
StorageT: 'static + Debug + Hash + PrimInt + Serialize + TypeName + Unsigned,
usize: AsPrimitive<StorageT>,
u32: AsPrimitive<StorageT>
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let conflicts = self.stable.conflicts().unwrap();
write!(
f,
"CTConflictsError{{{} Shift/Reduce, {} Reduce/Reduce}}",
conflicts.sr_len(),
conflicts.rr_len()
)
}
}

impl<StorageT> Error for CTConflictsError<StorageT>
where
StorageT: 'static + Debug + Hash + PrimInt + Serialize + TypeName + Unsigned,
usize: AsPrimitive<StorageT>,
u32: AsPrimitive<StorageT>
{
}

/// A `CTParserBuilder` allows one to specify the criteria for building a statically generated
/// parser.
pub struct CTParserBuilder<StorageT = u32> {
// Anything stored in here almost certainly needs to be included as part of the rebuild_cache
// function below so that, if it's changed, the grammar is rebuilt.
pub struct CTParserBuilder<StorageT = u32>
where
StorageT: Eq + Hash
{
// Anything stored in here (except `conflicts` and `error_on_conflict`) almost certainly needs
// to be included as part of the rebuild_cache function below so that, if it's changed, the
// grammar is rebuilt.
recoverer: RecoveryKind,
phantom: PhantomData<StorageT>,
actionkind: ActionKind
actionkind: ActionKind,
error_on_conflicts: bool,
conflicts: Option<(
YaccGrammar<StorageT>,
StateGraph<StorageT>,
StateTable<StorageT>
)>
}

impl CTParserBuilder<u32> {
Expand Down Expand Up @@ -132,7 +190,9 @@ where
CTParserBuilder {
recoverer: RecoveryKind::MF,
phantom: PhantomData,
actionkind: ActionKind::GenericParseTree
actionkind: ActionKind::GenericParseTree,
error_on_conflicts: true,
conflicts: None
}
}

Expand All @@ -156,9 +216,9 @@ where
/// If `StorageT` is not big enough to index the grammar's tokens, rules, or
/// productions.
pub fn process_file_in_src(
&self,
&mut self,
srcp: &str
) -> Result<(HashMap<String, StorageT>), Box<Error>> {
) -> Result<HashMap<String, StorageT>, Box<Error>> {
let mut inp = current_dir()?;
inp.push("src");
inp.push(srcp);
Expand All @@ -173,6 +233,30 @@ where
self
}

/// If set to true, `process_file_in_src` will return an error if the given grammar contains
/// any Shift/Reduce or Reduce/Reduce conflicts. Defaults to `true`.
pub fn error_on_conflicts(mut self, b: bool) -> Self {
self.error_on_conflicts = b;
self
}

/// If there are any conflicts in the grammar, return a tuple which allows users to inspect
/// and pretty print them; otherwise returns `None`. Note: The conflicts feature is currently
/// unstable and may change in the future.
pub fn conflicts(
&self
) -> Option<(
&YaccGrammar<StorageT>,
&StateGraph<StorageT>,
&StateTable<StorageT>,
&Conflicts<StorageT>
)> {
if let Some((grm, sgraph, stable)) = &self.conflicts {
return Some((grm, sgraph, stable, &stable.conflicts().unwrap()));
}
None
}

/// Statically compile the Yacc file `inp` into Rust, placing the output file(s) into
/// the directory `outd`. The latter defines a module with the following functions:
///
Expand All @@ -194,10 +278,10 @@ where
/// If `StorageT` is not big enough to index the grammar's tokens, rules, or
/// productions.
pub fn process_file<P, Q>(
&self,
&mut self,
inp: P,
outd: Q
) -> Result<(HashMap<String, StorageT>), Box<Error>>
) -> Result<HashMap<String, StorageT>, Box<Error>>
where
P: AsRef<Path>,
Q: AsRef<Path>
Expand Down Expand Up @@ -256,6 +340,15 @@ where
fs::remove_file(&outp_rs).ok();

let (sgraph, stable) = from_yacc(&grm, Minimiser::Pager)?;

if stable.conflicts().is_some() && self.error_on_conflicts {
return Err(Box::new(CTConflictsError {
grm,
sgraph,
stable
}));
}

// Because we're lazy, we don't write our own serializer. We use serde and bincode to
// create files $out_base.grm, $out_base.sgraph, and $out_base.out_stable which contain
// binary versions of the relevant structs, and then include those binary files into the
Expand All @@ -268,12 +361,12 @@ where
// Header
let mod_name = inp.as_ref().file_stem().unwrap().to_str().unwrap();
let actiontype = match grm.actiontype() {
Some(t) => t,
Some(t) => t.clone(), // Probably unneeded once NLL is in stable
None => {
match self.actionkind {
ActionKind::CustomAction => panic!("Action return type not defined!"),
ActionKind::GenericParseTree => {
"" // Dummy string that will never be used
String::new() // Dummy string that will never be used
}
}
}
Expand Down Expand Up @@ -475,6 +568,10 @@ where

let mut f = File::create(outp_rs)?;
f.write_all(outs.as_bytes())?;

if stable.conflicts().is_some() {
self.conflicts = Some((grm, sgraph, stable));
}
Ok(rule_ids)
}

Expand Down Expand Up @@ -565,3 +662,69 @@ pub fn _reconstitute<'a, StorageT: Deserialize<'a> + Hash + PrimInt + Unsigned>(
let stable = deserialize(stable_buf).unwrap();
(grm, sgraph, stable)
}

#[cfg(test)]
mod test {
extern crate temp_testdir;
use std::{fs::File, io::Write, path::PathBuf};

use self::temp_testdir::TempDir;
use super::{ActionKind, CTConflictsError, CTParserBuilder};

#[test]
fn test_conflicts() {
let temp = TempDir::default();
let mut file_path = PathBuf::from(temp.as_ref());
file_path.push("grm.y");
let mut f = File::create(&file_path).unwrap();
let _ = f.write_all(
"%start A
%%
A : 'a' 'b' | B 'b';
B : 'a' | C;
C : 'a';"
.as_bytes()
);

let mut ct = CTParserBuilder::new()
.error_on_conflicts(false)
.action_kind(ActionKind::GenericParseTree);
ct.process_file_in_src(file_path.to_str().unwrap()).unwrap();

match ct.conflicts() {
Some((_, _, _, conflicts)) => {
assert_eq!(conflicts.sr_len(), 1);
assert_eq!(conflicts.rr_len(), 1);
}
None => panic!("Expected error data")
}
}

#[test]
fn test_conflicts_error() {
let temp = TempDir::default();
let mut file_path = PathBuf::from(temp.as_ref());
file_path.push("grm.y");
let mut f = File::create(&file_path).unwrap();
let _ = f.write_all(
"%start A
%%
A : 'a' 'b' | B 'b';
B : 'a' | C;
C : 'a';"
.as_bytes()
);

match CTParserBuilder::new()
.action_kind(ActionKind::GenericParseTree)
.process_file_in_src(file_path.to_str().unwrap())
{
Ok(_) => panic!("Expected error"),
Err(e) => {
let cs = e.downcast_ref::<CTConflictsError<u32>>();
assert_eq!(cs.unwrap().stable.conflicts().unwrap().sr_len(), 1);
assert_eq!(cs.unwrap().stable.conflicts().unwrap().rr_len(), 1);
}
}
}
}
Loading