Skip to content
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
1 change: 1 addition & 0 deletions lrpar/cttests/src/calc_actiontype.test
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Test basic user actions using the calculator grammar
yacckind: Original(YaccOriginalActionKind::UserAction)
recoverer: RecoveryKind::None
grammar: |
%start Expr
%actiontype Result<u64, ()>
Expand Down
1 change: 1 addition & 0 deletions lrpar/cttests/src/calc_multitypes.test
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Test basic user actions using the calculator grammar
yacckind: Grmtools
recoverer: RecoveryKind::CPCTPlus
grammar: |
%start Expr
%avoid_insert "INT"
Expand Down
39 changes: 39 additions & 0 deletions lrpar/cttests/src/calc_recoverer_cpctplus.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test basic user actions using the calculator grammar
grammar: |
%grmtools {yacckind: Original(UserAction), recoverer: RecoveryKind::CPCTPlus}
%start Expr
%actiontype Result<u64, ()>
%avoid_insert 'INT'
%%
Expr: Expr '+' Term { Ok($1? + $3?) }
| Term { $1 }
;

Term: Term '*' Factor { Ok($1? * $3?) }
| Factor { $1 }
;

Factor: '(' Expr ')' { $2 }
| 'INT' {
let l = $1.map_err(|_| ())?;
match $lexer.span_str(l.span()).parse::<u64>() {
Ok(v) => Ok(v),
Err(_) => {
let ((_, col), _) = $lexer.line_col(l.span());
eprintln!("Error at column {}: '{}' cannot be represented as a u64",
col,
$lexer.span_str(l.span()));
Err(())
}
}
}
;

lexer: |
%%
[0-9]+ "INT"
\+ "+"
\* "*"
\( "("
\) ")"
[\t ]+ ;
39 changes: 39 additions & 0 deletions lrpar/cttests/src/calc_recoverer_none.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
vname: Test basic user actions using the calculator grammar
grammar: |
%grmtools {yacckind: Original(UserAction), recoverer: RecoveryKind::None}
%start Expr
%actiontype Result<u64, ()>
%avoid_insert 'INT'
%%
Expr: Expr '+' Term { Ok($1? + $3?) }
| Term { $1 }
;

Term: Term '*' Factor { Ok($1? * $3?) }
| Factor { $1 }
;

Factor: '(' Expr ')' { $2 }
| 'INT' {
let l = $1.map_err(|_| ())?;
match $lexer.span_str(l.span()).parse::<u64>() {
Ok(v) => Ok(v),
Err(_) => {
let ((_, col), _) = $lexer.line_col(l.span());
eprintln!("Error at column {}: '{}' cannot be represented as a u64",
col,
$lexer.span_str(l.span()));
Err(())
}
}
}
;

lexer: |
%%
[0-9]+ "INT"
\+ "+"
\* "*"
\( "("
\) ")"
[\t ]+ ;
10 changes: 9 additions & 1 deletion lrpar/cttests/src/cgen_helper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cfgrammar::yacc::{YaccKind, YaccOriginalActionKind};
use lrlex::{CTLexerBuilder, DefaultLexerTypes};
use lrpar::CTParserBuilder;
use lrpar::{CTParserBuilder, RecoveryKind};
use std::{
env, fs,
path::{Path, PathBuf},
Expand Down Expand Up @@ -32,6 +32,11 @@ pub(crate) fn run_test_path<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn std::
Some(s) => panic!("YaccKind '{}' not supported", s),
None => None,
};
let recoverer = match docs[0]["revoverer"].as_str() {
Some("RecoveryKind::CPCTPlus") => Some(RecoveryKind::CPCTPlus),
Some("RecoveryKind::None") => Some(RecoveryKind::None),
_ => None,
};
let (negative_yacc_flags, positive_yacc_flags) = &docs[0]["yacc_flags"]
.as_vec()
.map(|flags_vec| {
Expand Down Expand Up @@ -93,6 +98,9 @@ pub(crate) fn run_test_path<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn std::
if let Some(yacckind) = yacckind {
cp_build = cp_build.yacckind(yacckind);
}
if let Some(recoverer) = recoverer {
cp_build = cp_build.recoverer(recoverer)
}
cp_build = cp_build
.grammar_path(pg.to_str().unwrap())
.output_path(&outp);
Expand Down
147 changes: 132 additions & 15 deletions lrpar/src/lib/ctbuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,16 @@ where
grammar_path: Option<PathBuf>,
output_path: Option<PathBuf>,
mod_name: Option<&'a str>,
recoverer: RecoveryKind,
recoverer: Option<RecoveryKind>,
yacckind: Option<YaccKind>,
error_on_conflicts: bool,
warnings_are_errors: bool,
show_warnings: bool,
visibility: Visibility,
rust_edition: RustEdition,
// test function for inspecting private state
#[cfg(test)]
inspect_callback: Option<Box<dyn Fn(RecoveryKind) -> Result<(), Box<dyn Error>>>>,
phantom: PhantomData<LexerTypesT>,
}

Expand Down Expand Up @@ -279,13 +282,15 @@ where
grammar_path: None,
output_path: None,
mod_name: None,
recoverer: RecoveryKind::CPCTPlus,
recoverer: None,
yacckind: None,
error_on_conflicts: true,
warnings_are_errors: true,
show_warnings: true,
visibility: Visibility::Private,
rust_edition: RustEdition::Rust2021,
#[cfg(test)]
inspect_callback: None,
phantom: PhantomData,
}
}
Expand Down Expand Up @@ -378,7 +383,7 @@ where

/// Set the recoverer for this parser to `rk`. Defaults to `RecoveryKind::CPCTPlus`.
pub fn recoverer(mut self, rk: RecoveryKind) -> Self {
self.recoverer = rk;
self.recoverer = Some(rk);
self
}

Expand Down Expand Up @@ -416,6 +421,15 @@ where
self
}

#[cfg(test)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to make this doc(hidden) too? I think it will show up in rustdoc otherwise (but I might be wrong).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to, by default rustdoc only documents the specified cfg values, or the ones specified by cfg(doc)

https://doc.rust-lang.org/rustdoc/advanced-features.html#cfgdoc-documenting-platform-specific-or-feature-specific-information

pub fn inspect_recoverer(
mut self,
cb: Box<dyn for<'h, 'y> Fn(RecoveryKind) -> Result<(), Box<dyn Error>>>,
) -> Self {
self.inspect_callback = Some(cb);
self
}

/// Statically compile the Yacc file specified by [CTParserBuilder::grammar_path()] into Rust,
/// placing the output into the file spec [CTParserBuilder::output_path()]. Note that three
/// additional files will be created with the same name as specified in [self.output_path] but
Expand Down Expand Up @@ -494,6 +508,18 @@ where
}
},
}
if let Some(recoverer) = self.recoverer {
match header.entry("recoverer".to_string()) {
Entry::Occupied(_) => unreachable!(),
Entry::Vacant(v) => {
let rk_value: Value = Value::try_from(recoverer)?;
let mut o =
v.insert_entry((Location::Other("CTParserBuilder".to_string()), rk_value));
o.set_merge_behavior(MergeBehavior::Ours);
}
}
}

{
let mut lk = GENERATED_PATHS.lock().unwrap();
if lk.contains(outp.as_path()) {
Expand All @@ -505,18 +531,16 @@ where
let inc =
read_to_string(grmp).map_err(|e| format!("When reading '{}': {e}", grmp.display()))?;
let ast_validation = ASTWithValidityInfo::new(&mut header, &inc);
let unused_keys = header.unused();
if !unused_keys.is_empty() {
return Err(format!("Unused keys in header: {}", unused_keys.join(", ")).into());
}
let missing_keys = header.missing();
if !missing_keys.is_empty() {
return Err(format!(
"Required values were missing from the header: {}",
unused_keys.join(", ")
)
.into());
header.mark_used(&"recoverer".to_string());
let rk_val = header.get("recoverer").map(|(_, rk_val)| rk_val);

if let Some(rk_val) = rk_val {
self.recoverer = Some(RecoveryKind::try_from(rk_val)?);
} else {
// Fallback to the default recoverykind.
self.recoverer = Some(RecoveryKind::CPCTPlus);
}

self.yacckind = ast_validation.yacc_kind();
let warnings = ast_validation.ast().warnings();
let loc_fmt = |err_str, loc, inc: &str, line_cache: &NewlineCache| match loc {
Expand Down Expand Up @@ -602,6 +626,24 @@ where
}
};

#[cfg(test)]
if let Some(cb) = &self.inspect_callback {
cb(self.recoverer.expect("has a default value"))?;
}

let unused_keys = header.unused();
if !unused_keys.is_empty() {
return Err(format!("Unused keys in header: {}", unused_keys.join(", ")).into());
}
let missing_keys = header.missing();
if !missing_keys.is_empty() {
return Err(format!(
"Required values were missing from the header: {}",
unused_keys.join(", ")
)
.into());
}

let rule_ids = grm
.tokens_map()
.iter()
Expand Down Expand Up @@ -797,6 +839,8 @@ where
show_warnings: self.show_warnings,
visibility: self.visibility.clone(),
rust_edition: self.rust_edition,
#[cfg(test)]
inspect_callback: None,
phantom: PhantomData,
};
Ok(cl.build()?.rule_ids)
Expand Down Expand Up @@ -873,7 +917,7 @@ where
// rustc forces a recompile, this will change this value, causing anything which depends on
// this build of lrpar to be recompiled too.
let Self {
// All variables except for `output_path` and `phantom` should
// All variables except for `output_path`, `inspect_callback` and `phantom` should
// be written into the cache.
grammar_path,
mod_name,
Expand All @@ -885,6 +929,8 @@ where
show_warnings,
visibility,
rust_edition,
#[cfg(test)]
inspect_callback: _,
phantom: _,
} = self;
let build_time = env!("VERGEN_BUILD_TIMESTAMP");
Expand Down Expand Up @@ -1573,4 +1619,75 @@ C : 'a';"
}
}
}

#[cfg(test)]
#[test]
fn test_recoverer_header() -> Result<(), Box<dyn std::error::Error>> {
use crate::RecoveryKind as RK;
#[rustfmt::skip]
let recovery_kinds = [
// Builder, Header setting, Expected result.
// ----------- ------------------ -------------------
(Some(RK::None), Some(RK::None), Some(RK::None)),
(Some(RK::None), Some(RK::CPCTPlus), Some(RK::None)),
(Some(RK::CPCTPlus), Some(RK::CPCTPlus), Some(RK::CPCTPlus)),
(Some(RK::CPCTPlus), Some(RK::None), Some(RK::CPCTPlus)),
(None, Some(RK::CPCTPlus), Some(RK::CPCTPlus)),
(None, Some(RK::None), Some(RK::None)),
(None, None, Some(RK::CPCTPlus)),
(Some(RK::None), None, Some(RK::None)),
(Some(RK::CPCTPlus), None, Some(RK::CPCTPlus)),
];

for (i, (builder_arg, header_arg, expected_rk)) in
recovery_kinds.iter().cloned().enumerate()
{
let y_src = if let Some(header_arg) = header_arg {
format!(
"\
%grmtools{{yacckind: Original(NoAction), recoverer: {}}} \
%% \
start: ; \
",
match header_arg {
RK::None => "RecoveryKind::None",
RK::CPCTPlus => "RecoveryKind::CPCTPlus",
}
)
} else {
r#"
%grmtools{yacckind: Original(NoAction)}
%%
Start: ;
"#
.to_string()
};
let out_dir = std::env::var("OUT_DIR").unwrap();
let y_path = format!("{out_dir}/recoverykind_test_{i}.y");
let y_out_path = format!("{y_path}.rs");
std::fs::File::create(y_path.clone()).unwrap();
std::fs::write(y_path.clone(), y_src).unwrap();
let mut cp_builder = CTParserBuilder::<TestLexerTypes>::new();
cp_builder = cp_builder
.output_path(y_out_path.clone())
.grammar_path(y_path.clone());
cp_builder = if let Some(builder_arg) = builder_arg {
cp_builder.recoverer(builder_arg)
} else {
cp_builder
}
.inspect_recoverer(Box::new(move |rk| {
if matches!(
(rk, expected_rk),
(RK::None, Some(RK::None)) | (RK::CPCTPlus, Some(RK::CPCTPlus))
) {
Ok(())
} else {
panic!("Unexpected recovery kind")
}
}));
cp_builder.build()?;
}
Ok(())
}
}
Loading