Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
59f354a
Add API for user defined grmtools section entries in GrammarAST
ratmice Sep 11, 2026
eff4480
Clean up new test cases
ratmice Sep 11, 2026
14537c3
Remove unneeded `to_string()` in test
ratmice Sep 11, 2026
a96e799
Move ownership of field to ASTWithValidityInfo
ratmice Sep 11, 2026
86a4457
Use the grmtools section from `YaccParser::build`
ratmice Sep 11, 2026
adb16f4
First attempt at allowing downstream crate keys in CTParserBuilder
ratmice Sep 12, 2026
fd3fa86
Use same naming convention as the codegen module
ratmice Sep 12, 2026
fcba11b
Preemptively call `mark_used` on grmtools crate entries in header
ratmice Sep 12, 2026
751a4d4
stray whitespace
ratmice Sep 12, 2026
e8012c9
Make unused header entries check return spans
ratmice Sep 12, 2026
9bb74bf
Rename lookup function to use similar naming scheme.
ratmice Sep 12, 2026
2c47d29
Make crate name optional in `unused_header_keys_for_crate`
ratmice Sep 12, 2026
1cde528
Document the unused value checks
ratmice Sep 12, 2026
9e0c164
Move the find_span helper to test_utils
ratmice Sep 13, 2026
f3079aa
Make unknown keys without a crate prefix an error
ratmice Sep 13, 2026
ab83ba4
Simplify unused header check arguments
ratmice Sep 14, 2026
75127c6
make header_value_for_crate take one arg
ratmice Sep 14, 2026
a1d8a8f
Update docs
ratmice Sep 14, 2026
0ca6955
First try at avoiding multiple header structures
ratmice Sep 16, 2026
6cca7a6
No longer need to build a header in CTBuilder
ratmice Sep 16, 2026
a4e14d5
remove mark_required call, MissingYaccKind error covers this case
ratmice Sep 16, 2026
45fca2b
Perform unused header checks automatically during the codegen process
ratmice Sep 17, 2026
c587fab
Remove manual unused checks
ratmice Sep 17, 2026
c3b3f6c
Update after review
ratmice Sep 21, 2026
e8e1e7f
Update comment
ratmice Sep 21, 2026
22d5f11
Take set of crate/key prefixes by ref
ratmice Sep 22, 2026
74c2ba0
Update doc strings.
ratmice Sep 22, 2026
70ce6f1
Rename a few functions ofr uniformity
ratmice Sep 22, 2026
d7512ad
Remove reference to hidden function in docs
ratmice Sep 23, 2026
804a34d
initial work towards external checking of header values.
ratmice Oct 2, 2026
534631b
Avoid using helper function for now
ratmice Oct 2, 2026
19289b5
First stab at 'exhaustive' checking
ratmice Oct 2, 2026
a8065ff
add registration error, and method to retreive registered prefixes
ratmice Oct 2, 2026
16d66fd
Add test for prefix registration
ratmice Oct 2, 2026
5c9ac98
Fix docs example
ratmice Oct 2, 2026
8f90a95
rustfmt
ratmice Oct 2, 2026
642f94e
small example improvement
ratmice Oct 2, 2026
d8d00bf
ignore docstring example
ratmice Oct 2, 2026
f00c036
Add missing docs
ratmice Oct 9, 2026
e96f2eb
Add docs for registered_header_prefixes
ratmice Oct 9, 2026
35fe0ec
use `register_header_prefix` in `build_env()`
ratmice Oct 12, 2026
abbd6c2
use shorter `register_prefix` naming convention
ratmice Oct 14, 2026
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
13 changes: 8 additions & 5 deletions 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 Expand Up @@ -484,12 +484,15 @@ impl<K: Ord + Clone, V> MarkMap<K, V> {
}

/// Returns a `Vec` containing all the keys that are not marked as used.
pub fn unused(&self) -> Vec<K> {
pub fn unused(&self) -> Vec<(K, V)>
where
V: Clone,
{
let mut ret = Vec::new();
for (k, mark, v) in &self.contents {
let used_mark = Mark::Used.repr();
if v.is_some() && mark & used_mark == 0 {
ret.push(k.to_owned())
ret.push((k.to_owned(), v.as_ref().unwrap().clone()))
}
}
ret
Expand Down Expand Up @@ -711,7 +714,7 @@ mod test {
assert!(mm.insert("a", "test").is_none());
mm.mark_used(&"a");
assert_eq!(mm.get_mark(&"a"), Some(Mark::Used.repr()));
let empty: &[&String] = &[];
let empty: &[(&str, &str)] = &[];
assert_eq!(mm.unused().as_slice(), empty);
}

Expand All @@ -722,7 +725,7 @@ mod test {
assert!(mm.insert("b", "unused").is_none());
assert_eq!(mm.get_mark(&"a"), Some(Mark::Used.repr()));
assert_eq!(mm.get_mark(&"b"), Some(0));
assert_eq!(mm.unused().as_slice(), &["b"]);
assert_eq!(mm.unused().as_slice(), &[("b", "unused")]);
}
}

Expand Down
263 changes: 256 additions & 7 deletions cfgrammar/src/lib/yacc/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use super::{

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

Expand All @@ -39,6 +42,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 +56,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 +113,48 @@ 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_header_keys_for_crate`.
pub fn header_value_for_crate(
&mut self,
crate_name: &str,

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.

Dumb question: do we even need to force two variables (crate_name and key_name) on the user? Would key: &str make sense alone?

@ratmice ratmice Sep 13, 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.

We don't, I made the same comment in a review above too, for a different function.
I kind of flipped a coin, and did separate crate names and keys, due to the separate crate name elsewhere
like check_unused_... where the key name isn't passed in at all.

It didn't seem like a strong argument then, happy to change it.

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.

I think that if we use a single string, we get a nicer API in #667 (comment) too? Warning: 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.

Should be fixed in 75127c6

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.

At first glance this does look like it improves things?

@ratmice ratmice Sep 16, 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.

Yeah, I think so.

Edit: Moved my other part of the coment elsewhere.

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
}
}

/// Returns all key names given in the header specified by a `%grmtools` directive with the
/// `crate_name.` prefix for the given crate. If the `crate_name` is None returns any unused
/// keys with no crate prefix specified.
pub fn unused_header_keys_for_crate(&self, crate_name: Option<&str>) -> Vec<(String, Span)> {
self.grmtools_section

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.

So, one thing with this function that could be improved is that it gets called for each unused key, for each crate. It could if passed a set of crate names, do a single pass checking if the prefix of each unused key exists in the set of crate_prefixes.

So instead of doing a pass for each crate, it could do a single pass for all crates.

This wouldn't be hard to do now, I think it'd even be deterministic for the testsuite,
since MarkMap doesn't sort based on randomness.

I'm not sure if it is worth the trouble with the low number of crates and keys though?

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.

I had done this in c3b3f6c

.unused()
.iter()
.filter_map(|(key_name, HeaderValue(key_span, _))| {
if let Some(crate_name) = crate_name {
let crate_prefix = format!("{crate_name}.");
if key_name.starts_with(&crate_prefix) {
Some((key_name.clone(), *key_span))
} else {
None
}
} else {
if !RE_CRATE_DOT.is_match(key_name) {
Some((key_name.clone(), *key_span))
} else {
None
}
}
})
.collect::<Vec<_>>()
}
}

impl FromStr for ASTWithValidityInfo {
Expand All @@ -120,19 +167,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 +1032,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.header_value_for_crate("test", key);
assert_eq!(value, Some((expected_span, &expected_value)));
}
assert_eq!(
ast_validity.unused_header_keys_for_crate(Some("test")),
vec![("test.unused".to_string(), src.find_span("test.unused"))]
);
assert_eq!(
ast_validity.header_value_for_crate("cfgrammar", "yacckind"),
Some((
src.find_span("yacckind"),
&Value::Namespaced("Grmtools".to_string(), src.find_span("Grmtools"))
))
);

assert!(
ast_validity
.unused_header_keys_for_crate(Some("cfgrammar"))
.is_empty()
);

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

assert!(
ast_validity
.unused_header_keys_for_crate(Some("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.header_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_header_keys_for_crate(Some("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.header_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_header_keys_for_crate(Some("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.header_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_header_keys_for_crate(Some("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())
}
}
}
Loading