使用 Lezer 解析器
参考
- @lezer/lr-Github: Packed syntax tree data structure
@lezer/lrmodule- External Tokens-System Guide
- Context-System Guide
官方在 @lezer/lr 模块实现了一个 GLR parser 解析器,可以与 @lezer/generator 模块生成的解析表 parse table 配合工作。另外该模块还定义了一些与外部解析相关的类,以实现更复杂的解析功能。
外部解析器
在 grammar 文件里通过 @external tokens xxx from "external_file_path" 的方式引入外部定义的分词器,即在 external_file_path 文件里导出了名为 xxx 的变量,它是一个 ExternalTokenizer 类的实例,是一个外部分词器。
使用方法 new ExternalTokenizer(tokenFn, options?) 进行实例化,创建一个外部分词器,它可以基于解析栈/上下文对输入的文本流进行扫描,并识别特定的 token 词元
实例化时所传递的参数具体说明如下:
tokenFn是一个函数fn(input: InputStream, stack: Stack),该函数有两个入参:- 第一个参数
input是一个InputStream类的实例,通过它可以访问到输入到解析器的文本流InputStream
class
InputStream该类的实例化对象是与文本流交互的接口,一般是提供给外部分词器 external tokenizer 使用它表示输入到解析器里的文本/字符流,并进行适当的抽象,例如屏蔽了在文本流底层可能存在的非连续范围,以便外部分词器 tokenizer 通过它读取当前字符、前瞻若干字符、推进解析位置
该类的实例化对象具有以下属性和方法:
next属性:一个数值,表示输入流的下一个(即将读取的)字符的 code unit 字符码(即字符调用 JS 原生方法string.charCodeAt()得到的值),如果当前位置是输入流的末尾该属性值为-1pos属性:一个数值,表示当前所解析的输入流的位置(以字符为单位)
注意
由于输入流可能由多个范围 ranges 拼接而成,所以推进解析位置时,下一个位置可能并不是简单地在当前位置前移一个单元,有时候可能会跳到下一个 range 的位置
所以在获取下一个位置时,不能简单地基于当前位置
pos+1计算而得peek(offset)方法:查看当前位置附近的字符的字符码 code unit,参数offset是一个数值以指定所要查询的字符(数值表示与当前位置的距离,正负号查询的方向)
例如
peek(0)相当于属性next即获取当前(即将读取的)字符的字符码;peek(-1)获取上一个字符的字符码;peek(1)获取下一个字符的字符码注意
通过该方法进行长距离的前瞻/回溯字符会降低增量解析 incremental parsing 的效果,当通过该方法进行前瞻超过 25 个 code unit 时,会导致增量解析失效
acceptToken(token, endOffset?)方法:告诉解析器在当前位置成功识别/接受了一个token词元
默认情况下该
token词元的结束位置就是当前输入流的位置,可以传递第二个(可选)参数endOffset一个数值,以修正成功识别的token的结束位置(相对于当前输入流的位置的偏移量),例如acceptToken(targetToken, 1)就会把targetToken的结束位置调整为pos+1该方法是外部解析器常用来向解析器提交所识别到的 token 的方法
acceptTokenTo(token, endPos)方法:告诉解析器成功识别/接受了要给token词元,而且该token词元的结束位置是endPos
该方法的针对的场景一般是跨范围识别词元,并且已经计算出词元的绝对结束位置
advance(n?)方法:将解析位置沿着文本流向前移动n个 code units 单元(默认值是1),并返回一个数值,表示移动后在新位置的字符的 code unit 字符码,该值会作为属性next的新值
- 第二个参数
stack是一个Stack类的实例,通过它可以访问当前的解析栈/上下文Stack
class
Stack该类的实例化对象表示解析栈/解析上下文,一般用于解析器内部以追踪解析的进程,也可以暴露给外部分词器 external tokenizer以获取当前解析状态该类的实例化对象具有以下属性和方法:
pos属性:一个数值,表示当前解析的位置context属性:当前解析栈所处的上下文状态,可以是任意类型的值(其类型由当前解析栈所依赖的上下文跟踪器 context tracker 决定),如果该解析栈没有依赖上下文跟踪器该属性值为nullcanShift(term)方法:该方法返回一个布尔值,以检查给定的终结符term(一个数值,是该终结符所对应的 ID)是否可以被 shift 移入到当前解析栈里
外部解析器 external tokenizer 常用该方法判断接受特定的 token 词元(用对应的 term ID 表示)是否合理parser属性:当前解析栈所对应的解析器(一个LRParser类实例)dialectEnabled(dialectID)方法:该方法返回一个布尔值,以检查当前解析栈是否启用了给定的「方言」dialectID(一个数值,表示语言变体所对应的 ID,一个 grammar 文件可能支持同一语法不同变体),即当前是针对该语言的特定变体的语法进行解析
该函数可以对输入的文本流进行扫描,在满足条件时调用方法input.acceptToken或方法input.acceptTokenTo以表示成功识别到特定的 token 词元- 第一个参数
options是一个可选参数,它的值是一个对象(默认值是{}),可以包含以下属性,以配置该外部分词器:- (可选)属性
contextual:一个布尔值,表示该分词器是否依赖当前的解析栈 parse stack
当属性值设置为true,表示该外部分词器依赖解析栈,则(在不同的解析操作之间)Lezer 不会对该分词器(针对同一个位置的识别)结果进行缓存,由于该分词器在不同上下文可能会有不同的结果。这可能影响解析性能,所以仅在必要时才打开该设置。
如果该分词器需要根据当前解析上下文(例如在不同的嵌套结构里)改变词元识别结果,就把该属性值设置为true - (可选)属性
fallback:一个布尔值,用于控制(在优先级更高级的分词器识别/生成一个词元后)该分词器是否运行
如果有更高级的分词器在当前位置识别/接受了一个 token 词元,则默认情况下会阻止更低优先级的分词器运行
当该属性值设置为true,表示如果有更高级的分词器已经返回 token 词元,但是该词元不符合当前的上下文状态,并没有被解析器接受(即该词元实际没被使用),则当前分词器可以运行
一般高优先级的分词器识别的是一种通用的 token 词元,但是该词元可能在特定的上下文无效,而优先级更低的分词器可以通过配置该属性,来启用作为「后备」的解析识别方案,以识别/生成与语境更相关的词元 - (可选)属性
extend:一个布尔值,用于控制(在当前分词器识别/生成一个词元后)其他更低优先级的分词器是否运行
当该属性值设置为true,表示该分词器即使识别/生成 token 词元后,也不会阻止优先级更低的其他分词器运行
如果希望多个分词器都有机会在文档的同一个位置识别/生成 token,以增加语法树的信息,则可以通过配置该属性来标记该分词器允许其他分词器进行拓展(插入其他 token)
- (可选)属性
上下文跟踪器
context tracker 用于跟踪解析过程中所记录着的状态(例如 Python 是缩进敏感型的编程语言,在解析 Python 代码文本时需要记录当前的缩进量),以供外部分词器 tokenizer 使用(初始化外部分词器时所传递的第二个参数 stack,可以通过其属性 stack.context 访问当前上下文状态值)
不可变 immutable
上下文状态值是不可变 immutable 的,只能在解析器执行 shift 移入、reduce 归约、reuse 复用操作时,调用 context tracker 上下文跟踪器的相应方法进行更新
要将外部定义的 context tracker 挂载到解析器上,需要在 grammar 文件里插入一段代码 @context myTracker from "external_file_path" 以声明启用上下文跟踪器(其中在 external_file_path 文件里导出了名为 myTracker 的变量,它是一个 ContextTracker 类的实例,是一个上下文跟踪器),当解析器在执行 shift 移入、reduce 归约、reuse 复用操作时会调用该实例对象的相关方法来更新上下文状态值
使用方法 new ContextTracker(spec) 进行实例化,创建一个上下文跟踪器
其中实例化所传递的参数 spec 是一个对象,具有以下属性以配置上下文跟踪器的行为:
- 属性
start:解析开始时上下文状态的初始值 - (可选)属性
shift:一个方法fn(context, term, stack, input)当解析器执行 shift 移入操作(把一个终结符移入语法树)时,会调用该方法并返回一个新的上下文状态值(数据类型要和属性start相同)
该方法的各个参数的具体介绍如下:- 第一个参数
context:更新前的上下文状态值 - 第二个参数
term:一个数值,表示被移入 shift 语法树的终结符的 term id 值 - 第三个参数
stack:一个Stack类的实例,当前的解析栈 - 第四个参数
input: 一个InputStream类的实例,当前输入到解析器的文本流
- 第一个参数
- (可选)属性
reduce:一个方法fn(context, term, stack, input)当解析器执行 reduce 归约操作(把多个终结符/非终结符合并为一个非终结符)时,会调用该方法并返回一个新的上下文状态值(数据类型要和属性start相同)
该方法的各个参数和前一个属性shift的方法一样
当需要在某些语法节点结束时调整上下文,可以配置该可选方法 - (可选)属性
reuse:一个方法fn(context, node, stack, input)当解析器执行 reuse 重用节点操作时,会调用该方法并返回一个新的上下文状态值(数据类型要和属性start相同)
该方法的第二个参数node是一个Tree类的实例,表示重用的节点(局部语法树),其他参数和上文属性shift的方法一样 - (可选)属性
hash:一个方法fn(context)基于当前上下文状态值context计算出一个数值(将复杂的上下文压缩为简单的数值),以便将该上下文存储到节点树上,并在增量解析时进行快速对比(以判断节点是否可以复用)
仅在后一个属性strict设置为true时才需要配置该属性 - (可选)属性
strict:一个布尔值,表示在增量解析时如果要重用节点是否需要检查上下文状态值的一致性
默认值为true即在增量解析时,需要检查重用的节点所对应的上下文状态时是否与当前上下文状态值一样
如果该属性设置为false,则在复用节点时会跳过上下文检测,以减少上下文状态值的存储开销