diff --git a/cfgrammar/src/lib/header.rs b/cfgrammar/src/lib/header.rs index 458f3d864..a1f91461b 100644 --- a/cfgrammar/src/lib/header.rs +++ b/cfgrammar/src/lib/header.rs @@ -6,7 +6,21 @@ use crate::{ }, }; use regex::{Regex, RegexBuilder}; -use std::{error::Error, fmt, sync::LazyLock}; +use std::{ + collections::{HashMap, HashSet}, + error::Error, + fmt, + sync::LazyLock, +}; + +#[derive(Debug)] +#[doc(hidden)] +pub struct FileHeaders { + pub grmtools: Header, + pub grmtools_span: Span, + pub user_section: HashMap, + pub user_section_span: Span, +} /// An error regarding the `%grmtools` header section. /// @@ -69,6 +83,15 @@ pub enum HeaderErrorKind { DuplicateEntry, InvalidEntry(&'static str), ConversionError(&'static str, &'static str), + InvalidUserSectionValueType, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum UserSectionValue { + String(String, Span), + Num(u64, Span), + Bool(bool, Span), + Array(Vec, Span), } impl fmt::Display for HeaderErrorKind { @@ -82,6 +105,9 @@ impl fmt::Display for HeaderErrorKind { } HeaderErrorKind::InvalidEntry(s) => &format!("Invalid entry: '{}'", s), HeaderErrorKind::DuplicateEntry => "Duplicate Entry", + HeaderErrorKind::InvalidUserSectionValueType => { + "Invalid value type for the %user section" + } HeaderErrorKind::ConversionError(t, err_str) => { &format!("Converting header value to type '{}': {}", t, err_str) } @@ -239,7 +265,7 @@ impl Namespaced { static RE_LEADING_WS: LazyLock = LazyLock::new(|| Regex::new(r"^[\p{Pattern_White_Space}]*").unwrap()); static RE_NAME: LazyLock = LazyLock::new(|| { - RegexBuilder::new(r"^[A-Z][A-Z_]*") + RegexBuilder::new(r"^[A-Z][A-Z_\.]*") .case_insensitive(true) .build() .unwrap() @@ -247,8 +273,6 @@ static RE_NAME: LazyLock = LazyLock::new(|| { static RE_DIGITS: LazyLock = LazyLock::new(|| Regex::new(r"^[0-9]+").unwrap()); static RE_STRING: LazyLock = LazyLock::new(|| Regex::new(r#"^\"(\\.|[^"\\])*\""#).unwrap()); -const MAGIC: &str = "%grmtools"; - fn add_duplicate_occurrence( errs: &mut Vec>, kind: HeaderErrorKind, @@ -418,83 +442,179 @@ impl<'input> GrmtoolsSectionParser<'input> { Self { src, required } } - #[allow(clippy::type_complexity)] - pub fn parse(&'_ self) -> Result<(Header, usize), Vec>> { + pub fn parse(&'_ self) -> Result<(FileHeaders, usize), Vec>> { + let mut sections_lookup = HashSet::from_iter(["%grmtools", "%user"]); + let mut cur_pos = 0; + let mut headers: HashMap<&'static str, (Header, Span)> = HashMap::new(); + // This will error when a duplicate section is encountered, but only in a round about fashion. + // Because we remove the section from `sections_lookup`, the next go around it'll be unrecognized. + // As such, it's likely to be an `unrecognized declaration` instead of a nicer error. + while let (Some((header, section)), pos) = self.parse_sections(cur_pos, §ions_lookup)? { + sections_lookup.remove(section); + headers.insert(section, (header, Span::new(cur_pos, pos))); + cur_pos = pos; + } + + // When a default empty header is produced, we currently give it an empty span at (0, 0) for convenience + let (grmtools, grmtools_span) = headers + .remove("%grmtools") + .unwrap_or_else(|| (Header::new(), Span::new(0, 0))); + let (user_header, user_section_span) = headers + .remove("%user") + .unwrap_or_else(|| (Header::new(), Span::new(0, 0))); + let mut user_section = HashMap::new(); let mut errs = Vec::new(); - if let Some(mut i) = self.lookahead_is(MAGIC, self.parse_ws(0)) { - let mut ret = Header::new(); - i = self.parse_ws(i); - let section_start_pos = i; - if let Some(j) = self.lookahead_is("{", i) { - i = self.parse_ws(j); - while self.lookahead_is("}", i).is_none() && i < self.src.len() { - let (key, key_loc, val, j) = match self.parse_key_value(i) { - Ok((key, key_loc, val, pos)) => (key, key_loc, val, pos), - Err(e) => { - errs.push(e); - return Err(errs); + for (key, HeaderValue(key_span, value)) in user_header.into_iter() { + match value { + Value::Flag(flag, val_span) => { + user_section.insert( + key.clone(), + (*key_span, UserSectionValue::Bool(*flag, *val_span)), + ); + } + Value::Setting(setting) => match setting { + Setting::Array(v, start_span, _) => { + let mut out = Vec::with_capacity(v.capacity()); + for val in v { + match val { + Setting::String(s, val_span) => { + out.push(UserSectionValue::String(s.clone(), *val_span)) + } + Setting::Num(n, val_span) => { + out.push(UserSectionValue::Num(*n, *val_span)) + } + _ => { + errs.push(HeaderError { + kind: HeaderErrorKind::InvalidUserSectionValueType, + locations: vec![*key_span], + }); + } + } } - }; - match ret.entry(key) { - Entry::Occupied(orig) => { - let HeaderValue(orig_loc, _): &HeaderValue = orig.get(); - add_duplicate_occurrence( - &mut errs, - HeaderErrorKind::DuplicateEntry, - *orig_loc, - key_loc, - ) + user_section.insert( + key.clone(), + (*key_span, UserSectionValue::Array(out, *start_span)), + ); + } + Setting::String(s, val_span) => { + user_section.insert( + key.clone(), + (*key_span, UserSectionValue::String(s.clone(), *val_span)), + ); + } + Setting::Num(n, val_span) => { + user_section.insert( + key.clone(), + (*key_span, UserSectionValue::Num(*n, *val_span)), + ); + } + + _ => { + errs.push(HeaderError { + kind: HeaderErrorKind::InvalidUserSectionValueType, + locations: vec![*key_span], + }); + } + }, + } + } + if !errs.is_empty() { + return Err(errs); + } + Ok(( + FileHeaders { + grmtools, + grmtools_span, + user_section, + user_section_span, + }, + cur_pos, + )) + } + + #[allow(clippy::type_complexity)] + pub fn parse_sections( + &'_ self, + start_pos: usize, + sections: &HashSet<&'static str>, + ) -> Result<(Option<(Header, &'static str)>, usize), Vec>> { + for magic_string in sections { + let grmtools_required = self.required && magic_string == &"%grmtools"; + let mut errs = Vec::new(); + if let Some(mut i) = self.lookahead_is(magic_string, self.parse_ws(start_pos)) { + let mut ret = Header::new(); + i = self.parse_ws(i); + let section_start_pos = i; + if let Some(j) = self.lookahead_is("{", i) { + i = self.parse_ws(j); + while self.lookahead_is("}", i).is_none() && i < self.src.len() { + let (key, key_loc, val, j) = match self.parse_key_value(i) { + Ok((key, key_loc, val, pos)) => (key, key_loc, val, pos), + Err(e) => { + errs.push(e); + return Err(errs); + } + }; + match ret.entry(key) { + Entry::Occupied(orig) => { + let HeaderValue(orig_loc, _): &HeaderValue = orig.get(); + add_duplicate_occurrence( + &mut errs, + HeaderErrorKind::DuplicateEntry, + *orig_loc, + key_loc, + ) + } + Entry::Vacant(entry) => { + entry.insert(HeaderValue(key_loc, val)); + } } - Entry::Vacant(entry) => { - entry.insert(HeaderValue(key_loc, val)); + if let Some(j) = self.lookahead_is(",", j) { + i = self.parse_ws(j); + continue; + } else { + i = self.parse_ws(j); + break; } } - if let Some(j) = self.lookahead_is(",", j) { - i = self.parse_ws(j); - continue; - } else { - i = self.parse_ws(j); - break; - } - } - if let Some(j) = self.lookahead_is("*", i) { - errs.push(HeaderError { - kind: HeaderErrorKind::UnexpectedToken( - '*', - "perhaps this is a glob, in which case it requires string quoting.", - ), - locations: vec![Span::new(i, j)], - }); - Err(errs) - } else if let Some(i) = self.lookahead_is("}", i) { - if errs.is_empty() { - Ok((ret, i)) + if let Some(j) = self.lookahead_is("*", i) { + errs.push(HeaderError { + kind: HeaderErrorKind::UnexpectedToken( + '*', + "perhaps this is a glob, in which case it requires string quoting.", + ), + locations: vec![Span::new(i, j)], + }); + return Err(errs); + } else if let Some(i) = self.lookahead_is("}", i) { + if errs.is_empty() { + return Ok((Some((ret, magic_string)), i)); + } else { + return Err(errs); + } } else { - Err(errs) + errs.push(HeaderError { + kind: HeaderErrorKind::ExpectedToken('}'), + locations: vec![Span::new(section_start_pos, i)], + }); + return Err(errs); } } else { errs.push(HeaderError { - kind: HeaderErrorKind::ExpectedToken('}'), - locations: vec![Span::new(section_start_pos, i)], + kind: HeaderErrorKind::ExpectedToken('{'), + locations: vec![Span::new(i, i)], }); - Err(errs) + return Err(errs); } - } else { + } else if grmtools_required { errs.push(HeaderError { - kind: HeaderErrorKind::ExpectedToken('{'), - locations: vec![Span::new(i, i)], + kind: HeaderErrorKind::MissingGrmtoolsSection, + locations: vec![Span::new(0, 0)], }); - Err(errs) + return Err(errs); } - } else if self.required { - errs.push(HeaderError { - kind: HeaderErrorKind::MissingGrmtoolsSection, - locations: vec![Span::new(0, 0)], - }); - Err(errs) - } else { - Ok((Header::new(), 0)) } + Ok((None, start_pos)) } fn parse_name(&self, i: usize) -> Result<(String, usize), HeaderError> { @@ -755,12 +875,12 @@ mod test { let res = parser.parse(); let errs = res.unwrap_err(); assert_eq!(errs.len(), 1); - match errs[0] { + match &errs[0] { HeaderError { kind: HeaderErrorKind::UnexpectedToken('*', _), locations: _, } => (), - _ => panic!("Expected glob specific error"), + e => panic!("Expected glob specific error got '{e}'"), } } } diff --git a/cfgrammar/src/lib/yacc/ast.rs b/cfgrammar/src/lib/yacc/ast.rs index a3ae7415a..b1d1804f1 100644 --- a/cfgrammar/src/lib/yacc/ast.rs +++ b/cfgrammar/src/lib/yacc/ast.rs @@ -14,7 +14,7 @@ use super::{ use crate::{ Span, - header::{GrmtoolsSectionParser, HeaderError, HeaderErrorKind, HeaderValue}, + header::{GrmtoolsSectionParser, HeaderError, HeaderErrorKind, HeaderValue, UserSectionValue}, yacc::YaccOriginalActionKind, }; @@ -118,7 +118,7 @@ impl FromStr for ASTWithValidityInfo { let (header, _) = GrmtoolsSectionParser::new(src, true) .parse() .map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::>())?; - if let Some(HeaderValue(_, yk_val)) = header.get("yacckind") { + if let Some(HeaderValue(_, yk_val)) = header.grmtools.get("yacckind") { let yacc_kind = YaccKind::try_from(yk_val).map_err(|e| vec![e.into()])?; let ast = { // We don't want to strip off the header so that span's will be correct. @@ -178,6 +178,7 @@ pub struct GrammarAST { // The set of symbol names that, if unused in a // grammar, will not cause a warning or error. pub expect_unused: Vec, + pub user_section: HashMap, } #[derive(Debug, Clone)] @@ -255,6 +256,7 @@ impl GrammarAST { parse_generics: None, programs: None, expect_unused: Vec::new(), + user_section: HashMap::new(), } } @@ -984,4 +986,67 @@ start -> () : "a" {$;;;; }; }] ); } + + #[test] + fn test_user_section() { + use super::*; + let ast_validity = ASTWithValidityInfo::new( + YaccKind::Grmtools, + r#" +%user { + flag, + !negative, + string: "foo", + vec: ["a", "b"], + num: 0, +} +%token a +%% +start -> () : "a" { () }; +"#, + ); + + if let Some((_, UserSectionValue::Bool(true, _))) = + ast_validity.ast().user_section.get("flag") + { + } else { + panic!("Expected true flag"); + } + + if let Some((_, UserSectionValue::Bool(false, _))) = + ast_validity.ast().user_section.get("negative") + { + } else { + panic!("Expected false flag"); + } + + if let Some((_, UserSectionValue::String(s, _))) = + ast_validity.ast().user_section.get("string") + { + assert_eq!(s, "foo"); + } else { + panic!("Expected string"); + } + if let Some((_, UserSectionValue::Array(arr, _))) = + ast_validity.ast().user_section.get("vec") + { + let string_vals = arr + .iter() + .cloned() + .map(|s| match s { + UserSectionValue::String(s, _) => s, + _ => panic!("Expected string"), + }) + .collect::>(); + assert_eq!(string_vals, vec!["a".to_string(), "b".to_string()]); + } else { + panic!("Expected vec of strings"); + } + + if let Some((_, UserSectionValue::Num(n, _))) = ast_validity.ast().user_section.get("num") { + assert_eq!(*n, 0); + } else { + panic!("Expected numeric"); + } + } } diff --git a/cfgrammar/src/lib/yacc/parser.rs b/cfgrammar/src/lib/yacc/parser.rs index 4ee8eec99..fa3c1c456 100644 --- a/cfgrammar/src/lib/yacc/parser.rs +++ b/cfgrammar/src/lib/yacc/parser.rs @@ -337,9 +337,10 @@ impl YaccParser<'_> { pub(crate) fn parse(&mut self) -> YaccGrammarResult { let mut errs = Vec::new(); - let (_, pos) = GrmtoolsSectionParser::new(self.src, false) + let (headers, pos) = GrmtoolsSectionParser::new(self.src, false) .parse() .map_err(|mut errs| errs.drain(..).map(|e| e.into()).collect::>())?; + self.ast.user_section = headers.user_section; // 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). diff --git a/lrlex/src/lib/ctbuilder.rs b/lrlex/src/lib/ctbuilder.rs index 325e6bab4..a66466b1f 100644 --- a/lrlex/src/lib/ctbuilder.rs +++ b/lrlex/src/lib/ctbuilder.rs @@ -511,7 +511,7 @@ where } ErrorString(out) })?; - header.merge_from(parsed_header)?; + header.merge_from(parsed_header.grmtools)?; header.mark_used(&"lexerkind".to_string()); let lexerkind = match self.lexerkind { Some(lexerkind) => lexerkind, diff --git a/lrlex/src/lib/lexer.rs b/lrlex/src/lib/lexer.rs index 60cd726ca..ac3440241 100644 --- a/lrlex/src/lib/lexer.rs +++ b/lrlex/src/lib/lexer.rs @@ -9,7 +9,10 @@ use std::{ use cfgrammar::{ NewlineCache, Span, - header::{GrmtoolsSectionParser, Header, HeaderError, HeaderErrorKind, HeaderValue, Value}, + header::{ + GrmtoolsSectionParser, Header, HeaderError, HeaderErrorKind, HeaderValue, UserSectionValue, + Value, + }, span::Location, }; use num_traits::{AsPrimitive, PrimInt, Unsigned}; @@ -399,6 +402,7 @@ where start_states: Vec, lex_flags: LexFlags, pub(crate) expected_missing_tokens: Vec, + user_section: HashMap, phantom: PhantomData, } @@ -416,6 +420,7 @@ where start_states, lex_flags: DEFAULT_LEX_FLAGS, expected_missing_tokens: vec![], + user_section: HashMap::new(), phantom: PhantomData, } } @@ -426,13 +431,14 @@ where let (mut header, pos) = GrmtoolsSectionParser::new(s, false) .parse() .map_err(|mut errs| errs.drain(..).map(LexBuildError::from).collect::>())?; - let flags = LexFlags::try_from(&mut header).map_err(|e| vec![e.into()])?; + let flags = LexFlags::try_from(&mut header.grmtools).map_err(|e| vec![e.into()])?; LexParser::::new_with_lex_flags(s[pos..].to_string(), flags.clone()).map(|p| { LRNonStreamingLexerDef { rules: p.rules, start_states: p.start_states, lex_flags: flags, expected_missing_tokens: p.expected_missing_tokens, + user_section: header.user_section, phantom: PhantomData, } }) @@ -544,13 +550,14 @@ where s: &str, lex_flags: LexFlags, ) -> LexBuildResult> { - let (_, pos) = GrmtoolsSectionParser::new(s, false).parse().unwrap(); + let (header, pos) = GrmtoolsSectionParser::new(s, false).parse().unwrap(); LexParser::::new_with_lex_flags(s[pos..].to_string(), lex_flags.clone()).map( |p| LRNonStreamingLexerDef { rules: p.rules, start_states: p.start_states, lex_flags, expected_missing_tokens: p.expected_missing_tokens, + user_section: header.user_section, phantom: PhantomData, }, ) @@ -681,6 +688,10 @@ where pub(crate) fn lex_flags(&self) -> Option<&LexFlags> { Some(&self.lex_flags) } + + pub fn user_section(&self) -> &HashMap { + &self.user_section + } } /// An `LRNonStreamingLexer` holds a reference to a string and can lex it into [lrpar::Lexeme]s. diff --git a/lrlex/src/lib/parser.rs b/lrlex/src/lib/parser.rs index 8192f33cc..a90987a9e 100644 --- a/lrlex/src/lib/parser.rs +++ b/lrlex/src/lib/parser.rs @@ -1885,4 +1885,56 @@ b "A" assert_eq!(expected, trimmed) } } + + #[test] + fn test_user_section() { + use cfgrammar::header::UserSectionValue; + let src = r#" +%user { + flag, + !negative, + string: "foo", + vec: ["a", "b"], + num: 0, +} +%% +. 'dot' +"#; + let lexerdef = LRNonStreamingLexerDef::>::from_str(src).unwrap(); + if let Some((_, UserSectionValue::Bool(true, _))) = lexerdef.user_section().get("flag") { + } else { + panic!("Expected true flag"); + } + + if let Some((_, UserSectionValue::Bool(false, _))) = lexerdef.user_section().get("negative") + { + } else { + panic!("Expected false flag"); + } + + if let Some((_, UserSectionValue::String(s, _))) = lexerdef.user_section().get("string") { + assert_eq!(s, "foo"); + } else { + panic!("Expected string"); + } + if let Some((_, UserSectionValue::Array(arr, _))) = lexerdef.user_section().get("vec") { + let string_vals = arr + .iter() + .cloned() + .map(|s| match s { + UserSectionValue::String(s, _) => s, + _ => panic!("Expected string"), + }) + .collect::>(); + assert_eq!(string_vals, vec!["a".to_string(), "b".to_string()]); + } else { + panic!("Expected vec of strings"); + } + + if let Some((_, UserSectionValue::Num(n, _))) = lexerdef.user_section().get("num") { + assert_eq!(*n, 0); + } else { + panic!("Expected numeric"); + } + } } diff --git a/lrlex/src/main.rs b/lrlex/src/main.rs index 93b798395..0ede23da0 100644 --- a/lrlex/src/main.rs +++ b/lrlex/src/main.rs @@ -82,7 +82,7 @@ fn main() -> Result<(), Box> { let lex_l_path = &matches.free[0]; let lex_src = read_file(lex_l_path); let lex_diag = SpannedDiagnosticFormatter::new(&lex_src, Path::new(lex_l_path)); - let (mut header, _) = match GrmtoolsSectionParser::new(&lex_src, false).parse() { + let (header, _) = match GrmtoolsSectionParser::new(&lex_src, false).parse() { Ok(x) => x, Err(es) => { eprintln!( @@ -95,6 +95,7 @@ fn main() -> Result<(), Box> { process::exit(1); } }; + let mut header = header.grmtools; header.mark_used(&"lexerkind".to_string()); let lexerkind = if let Some(HeaderValue(_, lk_val)) = header.get("lexerkind") { LexerKind::try_from(lk_val)? diff --git a/lrpar/cttests/src/grmtools_section.test b/lrpar/cttests/src/grmtools_section.test index 463eef14a..59e47688f 100644 --- a/lrpar/cttests/src/grmtools_section.test +++ b/lrpar/cttests/src/grmtools_section.test @@ -4,13 +4,17 @@ grammar: | recoverer: RecoveryKind::CPCTPlus, test_files: ["*.input_grmtools_section"] } - %token MAGIC IDENT NUM STRING - %epp MAGIC "%grmtools" + %token IDENT NUM STRING %% start -> Result, Vec>> : MAGIC '{' contents '}' { $3 } ; + MAGIC -> () + : '%grmtools' { () } + | '%user' { () } + ; + contents -> Result, Vec>> : %empty { Ok(Header::new()) } | val_seq comma_opt { $1 } @@ -151,7 +155,8 @@ grammar: | lexer: | %grmtools{case_insensitive} %% - %grmtools 'MAGIC' + %grmtools '%grmtools' + %user '%user' ! '!' [A-Z][A-Z_]* 'IDENT' [0-9]+ 'NUM' diff --git a/lrpar/cttests/src/lib.rs b/lrpar/cttests/src/lib.rs index 6b0eeb61f..0082a8b35 100644 --- a/lrpar/cttests/src/lib.rs +++ b/lrpar/cttests/src/lib.rs @@ -389,7 +389,7 @@ fn test_grmtools_section_files() { let (yacc_parsed, errs) = grmtools_section_y::parse(&l); let parser = cfgrammar::header::GrmtoolsSectionParser::new(&s, true); let (header_parsed, _) = parser.parse().unwrap(); - assert_eq!(yacc_parsed.unwrap().unwrap(), header_parsed); + assert_eq!(yacc_parsed.unwrap().unwrap(), header_parsed.grmtools); assert!(errs.is_empty()); } } @@ -428,7 +428,7 @@ fn test_grmtools_section_strings() { let yacc_parsed = yacc_parsed.unwrap().unwrap(); let parser = cfgrammar::header::GrmtoolsSectionParser::new(src, true); let (header_parsed, _) = parser.parse().unwrap(); - assert_eq!(yacc_parsed, header_parsed); + assert_eq!(yacc_parsed, header_parsed.grmtools); assert!(errs.is_empty()); } } diff --git a/lrpar/cttests/src/user_section.test b/lrpar/cttests/src/user_section.test new file mode 100644 index 000000000..300a44c07 --- /dev/null +++ b/lrpar/cttests/src/user_section.test @@ -0,0 +1,46 @@ +vname: Test %user section +grammar: | + %grmtools {yacckind: Original(UserAction), recoverer: RecoveryKind::None} + %user { + test.anything: "foo", + test.flag, + !flag, + num: 5, + globs: ["*.foo", "*.bar"], + } + %start Expr + %actiontype Result + %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::() { + 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 ]+ ; diff --git a/lrpar/src/lib/codegen.rs b/lrpar/src/lib/codegen.rs index d36cc4874..1d9219645 100644 --- a/lrpar/src/lib/codegen.rs +++ b/lrpar/src/lib/codegen.rs @@ -301,7 +301,8 @@ where } fn parse_header(&self) -> Result<(Header, usize), Vec>> { - GrmtoolsSectionParser::new(self.src, false).parse() + let (headers, pos) = GrmtoolsSectionParser::new(self.src, false).parse()?; + Ok((headers.grmtools, pos)) } /// Looks up the `yacckind` field from the header, marks the field diff --git a/nimbleparse/src/main.rs b/nimbleparse/src/main.rs index 8150d972f..77c996fbd 100644 --- a/nimbleparse/src/main.rs +++ b/nimbleparse/src/main.rs @@ -242,7 +242,7 @@ fn main() { match parsed_header { Ok((parsed_header, _)) => { header - .merge_from(parsed_header) + .merge_from(parsed_header.grmtools) .expect("Specified merge behavior cannot fail"); } Err(errs) => {