如何在FParsec中编写自定义'attempt'解析器?
如何在FParsec中编写自定义'attempt'解析器?
嘿,我看了你在写自定义FParsec attempt解析器时遇到的麻烦——作为一个经常用FParsec做语法解析的开发者,我来帮你理清楚问题,再给出适配你需求的解决方案。
先解决你的编译错误
你代码里的核心问题是误解了ParserResult的类型:它不是元组,而是FParsec定义的一个区分联合类型(Discriminated Union),只有Success和Failure两种情况。你直接把p stream的结果解构为(result, diagnostics),这完全不符合它的类型结构,所以编译器才会抛出FS0001错误。
另外结合你的需求——你要处理的是永远不会失败的解析器,也就是p总是返回Success,只是会在自定义状态里携带诊断信息。那我们可以调整逻辑,先保存当前流的状态,运行解析器后根据诊断是否存在来决定是否回滚。
修正后的自定义attempt解析器
这里是适配你需求的代码版本,我会加上注释帮你理解每一步:
open FParsec // 假设你的类型定义是这样的,根据实际情况调整 type SyntaxNode = | ValidNode of string // 示例:有效语法节点 | RecoveryNode // 示例:错误恢复节点 type Diagnostic = { Message: string; Position: Position } type State = { Diagnostics: Diagnostic list } let customAttempt (p: Parser<SyntaxNode, State>) : Parser<SyntaxNode, State> = fun (stream: CharStream<State>) -> // 1. 保存当前的流位置和自定义状态(回滚的基准) let originalStreamPos = stream.Position let originalState = stream.State // 2. 运行目标解析器p let parseResult = p stream // 3. 因为你的解析器p永远不会失败,所以直接处理Success分支 match parseResult with | Success(parsedNode, newState, _) -> if List.isEmpty newState.Diagnostics then // 没有诊断信息,说明解析完全成功,直接返回结果 Success(parsedNode, newState, originalStreamPos) else // 有诊断信息,说明解析出了问题——先回滚流的位置 stream.BacktrackTo originalStreamPos // 然后返回原始状态下的结果(你也可以换成RecoveryNode这类错误节点) Success(parsedNode, originalState, originalStreamPos) | Failure(msg, err, state) -> // 按照你的设计,p永远不会走到这里,这里做个兜底处理 failwith $"Unexpected failure from parser: {msg}"
为什么这个逻辑符合你的需求?
根据你描述的错误恢复策略:让解析器永远返回成功,但通过自定义状态里的Diagnostics列表标记解析问题。这个customAttempt完美适配这个思路:
- 运行解析器前先“快照”当前的流位置和自定义状态,确保回滚有基准
- 执行解析器后,检查新状态里的诊断列表:
- 空列表意味着解析没问题,直接保留结果和新状态
- 非空列表意味着解析出问题了,回滚流位置到之前的快照,同时恢复自定义状态,返回结果(你可以根据需求把返回的节点换成专门的错误恢复节点)
关于扩展到customChoice/customSkipManyTill的思路
你提到还需要写customChoice、customSkipManyTill这类自定义解析器,思路和customAttempt一致:
- 对每个子解析器,先保存当前状态快照
- 运行子解析器,检查诊断信息
- 如果子解析器的诊断为空,就用它的结果;如果有诊断,回滚状态,尝试下一个子解析器(对应customChoice)或者调整逻辑(对应customSkipManyTill)
备注:内容来源于stack exchange,提问作者bookofproofs
相关产品推荐
相关产品推荐

