如何为nom v8.0.0的many0组合子实现自定义结构体作为输入?
嘿,我来帮你搞定nom v8.0.0里many0组合子配合自定义Token结构体的问题!
首先得明确核心问题:nom的many0默认是处理&[u8]这类字节切片的,但你用了自定义的Token结构体和基于LocatedSpan的Span类型,要让many0能正常工作,关键是让你的输入类型(也就是Token的切片&[Token<'a>])实现nom组合子依赖的几个核心trait,比如InputLength和Slice。
我一步步给你拆解:
1. 先补全你的Token和Span定义
先把你之前截断的代码补全(应该是LocatedSpan<&'a str>对吧?),顺便加上一个示例的TokenType枚举:
use nom_locate::LocatedSpan; // 先定义你的Token类型枚举,根据实际需求加变体 #[derive(Debug, Clone, PartialEq)] pub enum TokenType<'a> { Ident(&'a str), Number(u32), Keyword(&'a str), // 其他你需要的token类型 } #[derive(Debug, Clone, PartialEq)] pub struct Token<'a> { pub token_type: TokenType<'a>, pub span: Span<'a>, } // 基于str的LocatedSpan作为位置信息 pub type Span<'a> = LocatedSpan<&'a str>;
2. 为Token切片实现nom需要的核心Trait
many0这类重复解析的组合子需要知道输入的长度,以及如何对输入进行切片,所以我们要为&[Token<'a>]实现InputLength和Slice:
实现InputLength
这个trait用来获取输入的长度,对于Token切片来说,长度就是元素的个数,直接用原生切片的len()就行:
use nom::input::InputLength; impl<'a> InputLength for &[Token<'a>] { fn input_len(&self) -> usize { self.len() } }
另外,单个Token也可以实现InputLength(如果需要的话),这里我们把每个Token视为长度为1的输入单元:
impl<'a> InputLength for Token<'a> { fn input_len(&self) -> usize { 1 } }
实现Slice
这个trait用来对输入进行切片,比如解析完一个Token后,剩下的输入是原切片从第1个元素开始的部分。我们可以直接委托给原生切片的Slice实现:
use nom::input::Slice; impl<'a, R> Slice<R> for &[Token<'a>] where &'a [Token<'a>]: Slice<R>, { fn slice(&self, range: R) -> Self { <&[Token<'a>] as Slice<R>>::slice(self, range) } }
3. 写单个Token的解析器
接下来你需要一个能解析单个Token的解析器,比如我们写一个专门识别Ident类型Token的解析器:
use nom::{IResult, error::Error}; fn parse_ident<'a>(input: &[Token<'a>]) -> IResult<&[Token<'a>], Token<'a>, Error<&[Token<'a>]>> { // 检查输入的第一个Token是不是Ident类型 match input.first() { Some(token) if matches!(token.token_type, TokenType::Ident(_)) => { // 解析成功,返回剩下的输入(从第1个元素之后)和当前Token的克隆 Ok((&input[1..], token.clone())) } // 不匹配就返回错误 _ => Err(nom::Err::Error(Error::new(input, nom::error::ErrorKind::Tag))), } }
4. 用many0组合多个解析结果
现在就可以用many0来批量解析符合条件的Token了:
use nom::multi::many0; fn parse_all_idents<'a>(input: &[Token<'a>]) -> IResult<&[Token<'a>], Vec<Token<'a>>, Error<&[Token<'a>]>> { many0(parse_ident)(input) }
5. 测试一下效果
我们写个main函数测试一下:
fn main() { // 先构造几个测试用的Token let test_span = Span::new("foo 123 bar"); let test_tokens = vec![ Token { token_type: TokenType::Ident("foo"), span: test_span.clone() }, Token { token_type: TokenType::Number(123), span: test_span.clone() }, Token { token_type: TokenType::Ident("bar"), span: test_span }, ]; // 用我们的解析器解析 match parse_all_idents(&test_tokens) { Ok((remaining_tokens, collected_idents)) => { println!("解析后剩下的Token: {:?}", remaining_tokens); println!("收集到的Ident类型Token: {:?}", collected_idents); } Err(e) => println!("解析出错啦: {:?}", e), } }
运行这个代码,你会看到输出里剩下的是那个Number类型的Token,收集到的是foo和bar两个Ident Token,完全符合预期!
一些额外注意点
- 如果你需要自定义错误类型,记得为你的输入类型实现nom的
ParseErrortrait,这样错误信息会更贴合你的场景。 - 如果你是从原始的
LocatedSpan<&str>先解析成Token列表,再用many0处理,这是最常见的用法——nom适合先做词法分析生成Token流,再用组合子做语法分析。
备注:内容来源于stack exchange,提问作者Revanth Shalon

