前言
在上一篇中,我们用自己简陋的Scanner和Parser代替了ManagedMyC中的Scanner和Parser,最后得到个只实现简单语法高亮的语言服务。在继续讨论以前,先来看看托管Babel的代码。我曾经耐下心来阅读过Babel的代码,我发现其实Babel不仅提供了面向底层MPLex和MPPG的接口,还实现了一些面向IDE的接口。理解前者有助于实现语言服务的核心功能,理解后者有助于在IDE表现语言服务的功能。在这篇中,我打算分析面向底层那部分接口。
面向MPPG的接口
先来看看Babel中的类图,下面两个名字空间中的有些类将要在本文讨论,其中Babel.Parser下的类是MPPG编译parser.y得到的parser.cs中的类,Babel.ParserGenerator下的类是Babel中的辅助类,也就是Babel面向MPPG的接口。
移进和规约
在继续之前,先补充一个概念。解析程序在解析的过程中会进行(shift and reduce)移进和规约的动作。所谓移进,就是解析程序在某个规则序列中能够找到一个终结符或非终结符满足当前序列的期望终结符或非终结符,而接受该符号,并入栈的动作;所谓规约,是指,解析程序在多次移进后完成了一个完整的序列,并返回一个非终结符的动作。关于移进和规约的详细定义,请参考《Lex和Yacc》。在下面的叙述中,我将某个有效序列中包含的终结符或非终结符简称为标记。
两个重要的泛型
你也许注意到在上面列出的类中包含有两个泛型:YYSTYPE和YYLTYPE。YYSTYPE表示SemanticType,指的是某个标记表示的语义含义类型;YYLTYPE表示LocationType,指的是某个标记表示的位置含义类型。
YYLTYPE
首先解释一下YYLTYPE存在的意义:在解析程序规约时,它需要执行默认的“合并(Merge)”动作。所谓合并,指的是将若干个标记的位置信息合并成一个位置,默认情况下是将第一个标记的起始位置作为规约出来的非终结符的起始位置,最后一个标记的终止位置作为规约出来的非终结符的终止位置。这样的合并是有意义的,因为在我们的语法定义中必然出现互相的嵌套,有了合并,每个标记都能够有自己单个的位置信息。解析程序对外暴露了一个IMerge<YYLTYPE>接口,其中定义的方法Merge(),在规约时被解析程序调用,并执行默认的合并动作。我门parser.y中可以在规约行为中调用Merge()改变默认的合并行为。Babel定义了这个接口,并实现了一个LexLocation,这个类几乎足够我们用的,因为在代码编辑器中一段文本可以用一对起始位置和终止位置唯一确定了:
YYSTYPE
YYSTYPE表示标记的语义类型,这个类型可以是标记本身的文本,也可以是更为复杂的类型。在解析程序规约时,规约出来的非终结符的值类型默认使用第一个标记的值类型。我们可以在parser.y中通过定义%union关键字定义这个类型,关于如何定义%union,将在以后的文章中详细介绍。parser.cs中生成的LexValue就是YYSTYPE。
ShiftReduceParser<YYSTYPE,YYLTYPE>
这个类是在Babel中定义的,实现了移进和规约的处理算法、和辅助函数。所以这个类十分重要,是parser.cs和其他模块协同的关键,没有它,parser.cs甚至无法通过编译。在算法的具体实现中需要用到Rule、State、ParserStack<T>、AScanner<YYSTYPE,YYLTYPE>的实例。其中Rule、State、ParserStack<T>用于实现栈的算法,AScanner<YYSTYPE,YYLTYPE>是面向这个解析程序的扫描程序必须要实现的接口。可以这样认为:parser.cs实际上是一张配置表,ShiftReduceParser是根据这张表运行的程序。另外,ShiftReduceParser中定义了一个DoAction抽象方法,parser.cs重写了这个方法,并根据规约行为代码,实现这个方法。
Parser
parser.cs最后生成的Parser类继承自ShiftReduceParser<YYSTYPE,YYLTYPE>。Parser本身是很多的“配置”组成的,其中AddState、rules是ShiftReduceParser<YYSTYPE,YYLTYPE>中定义的。另外Parser是个部分类,这是通过%partial关键字设置的。之所以要这样设计,Babel可以在另外的文件中给Parser类扩展一些面向表现层的方法接口。
面向MPLex的接口
事实上只有如下两个接口是lexer需要实现的:
public interface IColorScan
{
void SetSource(string source, int offset);
int GetNext(ref int state, out int start, out int end);
}
public abstract class AScanner<YYSTYPE,YYLTYPE>
where YYSTYPE : struct
where YYLTYPE : IMerge<YYLTYPE>
其中IColorScan是用于着色器的接口,AScanner是用于扫描器的接口。MPLex会自动实现这两个接口。事实上,对于第二个接口,MPLex没有直接实现,而是间接实现了parser中的ScanBase:
public sealed class Scanner : ScanBase, IColorScan
public abstract class ScanBase : AScanner<LexValue,LexLocation>
小结
最后,用一张图说明这篇中的主要内容
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义