Skip to content
Draft
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
2 changes: 1 addition & 1 deletion cfgrammar/src/lib/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Spanned for HeaderError<Span> {

// This is essentially a tuple that needs a newtype so we can implement `From` for it.
// Thus we aren't worried about it being `pub`.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
#[doc(hidden)]
pub struct HeaderValue<T>(pub T, pub Value<T>);

Expand Down
2 changes: 1 addition & 1 deletion cfgrammar/src/lib/markmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::fmt;
///
/// Merge behaviors configure how the merge operator handles cases where both `MarkMaps` being merged
/// contain a particular key.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
#[doc(hidden)]
pub struct MarkMap<K, V> {
default_merge_behavior: MergeBehavior,
Expand Down
246 changes: 239 additions & 7 deletions cfgrammar/src/lib/yacc/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{

use crate::{
Span,
header::{GrmtoolsSectionParser, HeaderError, HeaderErrorKind, HeaderValue},
header::{GrmtoolsSectionParser, Header, HeaderError, HeaderErrorKind, HeaderValue, Value},
yacc::YaccOriginalActionKind,
};

Expand All @@ -39,6 +39,7 @@ impl fmt::Display for ASTModificationError {
pub struct ASTWithValidityInfo {
yacc_kind: YaccKind,
ast: GrammarAST,
grmtools_section: Header<Span>,
errs: Vec<YaccGrammarError>,
}

Expand All @@ -52,18 +53,19 @@ impl ASTWithValidityInfo {
/// already extracted the `YaccKind` if any.
pub fn new(yacc_kind: YaccKind, s: &str) -> Self {
let mut errs = Vec::new();
let ast = {
let (ast, grmtools_section) = {
let mut yp = YaccParser::new(yacc_kind, s);
yp.parse().map_err(|e| errs.extend(e)).ok();
let mut ast = yp.build();
let (mut ast, grmtools_section) = yp.build();
ast.complete_and_validate(Some(yacc_kind))
.map_err(|e| errs.push(e))
.ok();
ast
(ast, grmtools_section)
};
ASTWithValidityInfo {
ast,
errs,
grmtools_section,
yacc_kind,
}
}
Expand Down Expand Up @@ -108,6 +110,34 @@ impl ASTWithValidityInfo {
})
}
}

/// Performs a lookup in the grmtools section for an entry with the key `crate_name.key_name` and returns it.
/// If the entry is found it marks the key as `used`, for the purposes of `unused_grmtools_section_keys_for_crate`.
pub fn grmtools_section_value_for_crate(
&mut self,
crate_name: &str,
key_name: &str,
) -> Option<(Span, &Value<Span>)> {
let key = format!("{crate_name}.{key_name}");
self.grmtools_section.mark_used(&key);
if let Some(HeaderValue(span, value)) = self.grmtools_section.get(&key) {
Some((*span, value))
} else {
None
}
}

pub fn unused_grmtools_section_keys_for_crate(&self, crate_name: &str) -> Vec<String> {
self.grmtools_section
.unused()
.iter()
.filter(|key_name| {
let crate_prefix = format!("{crate_name}.");
key_name.starts_with(&crate_prefix)
})
.cloned()
.collect::<Vec<_>>()
}
}

impl FromStr for ASTWithValidityInfo {
Expand All @@ -120,19 +150,20 @@ impl FromStr for ASTWithValidityInfo {
.map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::<Vec<_>>())?;
if let Some(HeaderValue(_, yk_val)) = header.get("cfgrammar.yacckind") {
let yacc_kind = YaccKind::try_from(yk_val).map_err(|e| vec![e.into()])?;
let ast = {
let (ast, grmtools_section) = {
// We don't want to strip off the header so that span's will be correct.
let mut yp = YaccParser::new(yacc_kind, src);
yp.parse().map_err(|e| errs.extend(e)).ok();
let mut ast = yp.build();
let (mut ast, grmtools_section) = yp.build();
ast.complete_and_validate(Some(yacc_kind))
.map_err(|e| errs.push(e))
.ok();
ast
(ast, grmtools_section)
};
Ok(ASTWithValidityInfo {
ast,
errs,
grmtools_section,
yacc_kind,
})
} else {
Expand Down Expand Up @@ -984,4 +1015,205 @@ start -> () : "a" {$;;;; };
}]
);
}

#[test]
fn test_grmtools_section_values() {
use super::*;
use crate::header::Value;
let src = r#"
%grmtools {
yacckind: Grmtools,
lrpar.recoverer: CPCTPlus,
test.Flag,
!test.Negative,
test.string: "Foo",
test.vec: ["Aaaa", "Bbbb"],
test.num: 1234,
test.unused: 5678
}
%token a
%%
start -> () : "a" { () };
"#;
let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap();
for (key, (expected_span, expected_value)) in [
(
"Flag",
(
src.find_span("test.Flag"),
Value::Bool(true, src.find_span("test.Flag")),
),
),
(
"Negative",
(
src.find_span("test.Negative"),
Value::Bool(false, src.find_span("!test.Negative")),
),
),
(
"string",
(
src.find_span("test.string"),
Value::String("Foo".to_string(), src.find_span("Foo")),
),
),
(
"vec",
(
src.find_span("test.vec"),
Value::Array(
vec![
Value::String("Aaaa".to_string(), src.find_span("Aaaa")),
Value::String("Bbbb".to_string(), src.find_span("Bbbb")),
],
src.find_span("[\"Aaaa\", \"Bbbb\"]"),
),
),
),
(
"num",
(
src.find_span("test.num"),
Value::Num(1234, src.find_span("1234")),
),
),
] {
let value = ast_validity.grmtools_section_value_for_crate("test", key);
assert_eq!(value, Some((expected_span, &expected_value)));
}
assert_eq!(
ast_validity.unused_grmtools_section_keys_for_crate("test"),
vec!["test.unused"]

@ratmice ratmice Sep 11, 2026

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.

CTParserBuilder and nimbleparse are still using the ast_validity.ast_grmtools_section.unused() directly, rather than unused_grmtools_section_keys_for_crate.

So this usage seen in the testsuite with test.foo keys is likely to still trigger an error in practice, I had kind of forgotten about this until just now.
Unsure if we want to relax those errors in this patch, or a subsequent one?

@ratmice ratmice Sep 11, 2026

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.

This seems like something I need to investigate sooner rather than later, it isn't just that we're using unused, we're actually dealing with duplicate Header entries entirely, and there is some passing of that header value around mutably between lrlex/lrpar.

So we kind of need to move over to the new lookup API through the GrammarAST too.
There are still some aspects that aren't publicly exported through the GrammarAST, like required fields.

Edit: moved some comments here to a more relevant place.

);
assert_eq!(
ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"),
Some((
src.find_span("yacckind"),
&Value::Namespaced("Grmtools".to_string(), src.find_span("Grmtools"))
))
);

assert!(
ast_validity
.unused_grmtools_section_keys_for_crate("cfgrammar")
.is_empty()
);

assert_eq!(
ast_validity.grmtools_section_value_for_crate("lrpar", "recoverer"),
Some((
src.find_span("lrpar.recoverer"),
&Value::Namespaced("CPCTPlus".to_string(), src.find_span("CPCTPlus"))
))
);

assert!(
ast_validity
.unused_grmtools_section_keys_for_crate("lrpar")
.is_empty()
);
}

#[test]
fn test_grmtools_section_values2() {
use super::*;
let src = r#"
%grmtools {
yacckind: Original(YaccOriginalActionKind::UserAction),
}
%token a
%actiontype ()
%%
start: "a" { () };
"#;
let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap();
assert_eq!(
ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"),
Some((
src.find_span("yacckind"),
&Value::Namespaced(
"Original(YaccOriginalActionKind::UserAction)".to_string(),
src.find_span("Original(YaccOriginalActionKind::UserAction)"),
),
))
);
assert!(
ast_validity
.unused_grmtools_section_keys_for_crate("cfgrammar")
.is_empty()
);
}

#[test]
fn test_grmtools_section_values3() {
use super::*;
let src = r#"
%grmtools {
yacckind: YaccKind::Original(YaccOriginalActionKind::UserAction),
}
%token a
%actiontype ()
%%
start: "a" { () };
"#;
let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap();
assert_eq!(
ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"),
Some((
src.find_span("yacckind"),
&Value::Namespaced(
"YaccKind::Original(YaccOriginalActionKind::UserAction)".to_string(),
src.find_span("YaccKind::Original(YaccOriginalActionKind::UserAction)"),
),
))
);
assert!(
ast_validity
.unused_grmtools_section_keys_for_crate("cfgrammar")
.is_empty()
);
}

#[test]
fn test_grmtools_section_values4() {
use super::*;
let src = r#"
%grmtools {
yacckind: YaccKind::Original(UserAction),
}
%token a
%actiontype ()
%%
start: "a" { () };
"#;
let mut ast_validity = ASTWithValidityInfo::from_str(src).unwrap();
assert_eq!(
ast_validity.grmtools_section_value_for_crate("cfgrammar", "yacckind"),
Some((
src.find_span("yacckind"),
&Value::Namespaced(
"YaccKind::Original(UserAction)".to_string(),
src.find_span("YaccKind::Original(UserAction)"),
),
))
);
assert!(
ast_validity
.unused_grmtools_section_keys_for_crate("cfgrammar")
.is_empty()
);
}

trait FindSpan {
fn find_span(&self, s: &str) -> Span;
}

impl FindSpan for &'_ str {
#[track_caller]
fn find_span(&self, s: &str) -> Span {
let start_pos = self.find(s).unwrap();
Span::new(start_pos, start_pos + s.len())
}
}
}
14 changes: 9 additions & 5 deletions cfgrammar/src/lib/yacc/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use wincode::{SchemaRead, SchemaWrite};

use crate::{
Span, Spanned,
header::{GrmtoolsSectionParser, HeaderErrorKind},
header::{GrmtoolsSectionParser, Header, HeaderErrorKind},
};

pub type YaccGrammarResult<T> = Result<T, Vec<YaccGrammarError>>;
Expand Down Expand Up @@ -294,6 +294,7 @@ pub(crate) struct YaccParser<'a> {
src: &'a str,
num_newlines: usize,
ast: GrammarAST,
header: Option<Header<Span>>,
global_actiontype: Option<(String, Span)>,
}

Expand Down Expand Up @@ -331,15 +332,17 @@ impl YaccParser<'_> {
src,
num_newlines: 0,
ast: GrammarAST::new(),
header: None,
global_actiontype: None,
}
}

pub(crate) fn parse(&mut self) -> YaccGrammarResult<usize> {
let mut errs = Vec::new();
let (_, pos) = GrmtoolsSectionParser::new(self.src, false)
let (header, pos) = GrmtoolsSectionParser::new(self.src, false)
.parse()
.map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::<Vec<_>>())?;
self.header = Some(header);
// We pass around an index into the *bytes* of self.src. We guarantee that at all times
// this points to the beginning of a UTF-8 character (since multibyte characters exist, not
// every byte within the string is also a valid character).
Expand Down Expand Up @@ -371,8 +374,8 @@ impl YaccParser<'_> {
}
}

pub(crate) fn build(self) -> GrammarAST {
self.ast
pub(crate) fn build(self) -> (GrammarAST, Header<Span>) {
(self.ast, self.header.expect("set by parse()"))
}

fn parse_declarations(
Expand Down Expand Up @@ -1083,7 +1086,8 @@ mod test {
fn parse(yacc_kind: YaccKind, s: &str) -> Result<GrammarAST, Vec<YaccGrammarError>> {
let mut yp = YaccParser::new(yacc_kind, s);
yp.parse()?;
Ok(yp.build())
let (ast, _) = yp.build();
Ok(ast)
}

fn rule(n: &str) -> Symbol {
Expand Down