write: parser combinator

This commit is contained in:
jackfiled 2024-07-31 14:02:34 +08:00
parent a483ddc671
commit 38ce96a124
2 changed files with 191 additions and 0 deletions

View File

@ -0,0 +1,188 @@
---
title: 使用Parser Combinator编写语法分析器
tags:
- 编译原理
- C#
---
在函数式编程思想的指导下使用Parser Combinator编写语法分析器是编写编译器前端的大杀器。
<!--more-->
在编译原理的课程上,我们往往会学习两种编写语法分析器的方式,分别是自顶向下的递归下降分析和`LL(1)`语法分析器和自底向上的`LR(1)`语法分析器。在课堂上,我们通常更加聚焦于学习`LL(1)`和`LR(1)`等几种给定BNF范式的语法就能自动生成表驱动分析器的分析技术。在实践中以`Yacc`和`ANTLR`为代表语法分析器就是上述思想的经典实现。
但是如果我们调研实际中的编译器,会发现大多数的编译器都是使用递归下降的方式手动编写解析器,而不是使用各种的解析器生成工具。这往往是因为解析器生成器是难以扩展的,且生成的代码通常质量不高,在阅读和调试上都存在一系列的困难。但是手写解析器通常是一个非常枯燥的过程,写出来的代码也不能直观的表示语法。
因此我们希望能够有一种方法在给定一种语言的语法之后就可以明确、简单地设计出该语言的词法分析器和语法分析器。Parser Combinator就是这样的一种方法。
## Parser Combinator初见
首先我们先来考虑一个简单的问题:如何设计一个能够解析下面这个语法的解析函数,`bool`字面量的定义:
```
bool_literal -> true | false
```
很容易的,我们可以这样编写代码:
```csharp
Result<bool>? ParserBool(string source)
{
if (source.StartsWith("true"))
{
return new Result<bool>(true);
}
else if (source.StartsWith("false"))
{
return new Result<bool>(false);
}
return null;
}
```
代码中的Result是一个可为空的*盒子*,包裹了解析的结果,当解析成功时盒子里面就装着识别的结果,当解析失败是盒子就是空的。
容易想到,这个解析函数存在一系列的问题。
第一,这个函数的输入是一个字符串,而且在代码中假定字符串的开头就是我们需要解析的单词。因此,我们可以把输入的字符串封装为一个实现了下述接口的对象:
```csharp
public interface ILexicalScanner
{
public LexicalToken Read();
public ILexicalScanner Fork();
}
```
这个接口有两个功能,第一个是读取下一个词法元素,第二个是一个分叉函数,分支出一个和当前状态完全一样的扫描器,这是因为如果我们当前的识别失败了,我们可能已经消耗了一个或者是多个词法元素,我们需要保持开始解析时的解析状态才能让其他的解析函数从我们失败的地方继续尝试解析。
在定义了上述接口之后,我们可以将`ParseBool`函数修改为使用该接口,并在返回的`Result`中也添加一个字段返回识别成功或者是识别失败之后的`ILexicalScanner`。
```csharp
ParseResult<bool> ParserBool(ILexicalScanner scanner)
{
ILexicalScanner oldScanner = scanner.Fork();
LexicalToken token = oldScanner.Read();
if (token is "true")
{
return new ParseResult<bool>(true, scanner);
}
else if (token is "false")
{
return new ParseResult<bool>(false, scanner);
}
return new ParseResult<bool>(scanner);
}
```
第二,假如我们接到通知,需要解析的语法进行了扩充:
```
bool_literal -> true | false
factor -> identidier := bool_literal
```
我们如何在已有`ParserBoool`函数的基础上编写一个新的解析函数识别`factor`?我们可以首先编写识别`identifier`的函数:
```csharp
ParseResult<LexicalToken> ParserIdentifier(ILexicalScanner scanner)
{
ILexicalScanner oldScanner = scanner.Fork();
LexicalToken token = oldScanner.Read();
if (token is "identifier")
{
return new ParseResult<LexicalToken>(token, scanner);
}
return new ParseResult<LexicalToken>(scanner);
}
```
然后新的问题出现了,怎么把上面两个函数组合为一个可以识别`factor`的函数你可能会说课本上编写递归下降分析的时候直接编写一个新的函数不就好了。这个时候Parser Combinator说别急我给你展示一个**序列组合子**
![image-20240731131650377](./parser-combinator/image-20240731131650377.png)
你需要把上面两个函数包裹为一个返回`ParseFunction<T>`的高阶函数就可以使用这个组合子了:
```csharp
public delegate ParseResult<T> ParseFunction<T>(LexicalScanner scanner) where T : class;
public ParseFunction<bool> BoolParser()
{
return scanner => ParseBool(scanner);
}
public ParseFunction<LexicalToken> IdentifierParser()
{
return scanner => ParseIdentifier(scanner);
}
```
```csharp
public ParseFunction<Factor> FactorParser()
{
return scanner =>
{
from token in IdentifierParser()
from item in BoolParser()
select new ParseResult(token, scanner)
};
}
```
看完代码,你可能会问,上面也没有哪里使用到这个组合子啊,似乎都是使用的`C#`原生的语法。没错,不过上面的`LINQ`语句中涉及到一个`C#`的语法糖,多个`from`子句会被展开为对于`ParseFunction<Factor>.SelectMany`的调用:
```csharp
public ParseFunction<Factor> FactorParser()
{
return scanner =>
{
IdentifierParser().SelectMany(token => BoolParser(), (token, item) => new ParserResult(token, scanner))
};
}
```
而方法`SelectMany`的实现如下:
```csharp
public static ParseFunction<TResult> SelectMany<T1, T2, TResult>(this ParseFunction<T1> parse1,
Func<T1, ParseFunction<T2>> parseSelector, Func<T1, T2, TResult> resultSelector)
where T1 : class where T2 : class where TResult : class
{
return scanner =>
{
ParseResult<T1> result1 = parse1(scanner);
if (result1.Result is null)
{
return new ParseResult<TResult>(result1.Scanner);
}
ParseFunction<T2> parser2 = parseSelector(result1.Result);
ParseResult<T2> result2 = parser2(result1.Scanner);
return result2.Result is null
? new ParseResult<TResult>(result2.Scanner)
: new ParseResult<TResult>(resultSelector(result1.Result, result2.Result), result2.Scanner);
};
}
```
这是一个类似于函数式编程中**单子**Monad的算子可以将一系列链式调用“展平“。既然通过这个组合子可以很方便的编写连接语法那么我们神奇的Parser Combinator还提供了什么组合子呢实际上Parser Combinator一共定义了四个**基本组合子**
- 空组合子:识别空串。
- 单词组合子:识别特定的终结符字符串。
- 选择组合子:从输入的两个解析器中选择一个解析器进行解析。
- 连接组合子:连续使用输入的两个解析器。
上面这些组合子都是高阶函数,输入零个或者多个解析函数,返回一个解析函数。
## Parser Combinator的C#实现
## Parser Combinator的错误处理

BIN
YaeBlog/source/parser-combinator/image-20240731131650377.png (Stored with Git LFS) Normal file

Binary file not shown.