(四十六)深度解析领域特定语言(DSL)第八章——语法分析器组合子:案例实现(Part2)
本文介绍了列表结构分析器ListParser的实现,重点分析了其与顺序结构分析器SequenceParser的区别。ListParser用于处理无序列表元素,通过terminalTag标识列表结束位置。文章详细解析了NameMappingParser、ServiceTypeBlockParser等子分析器的代码实现,并重点讨论了AliasListParser如何处理带逗号分隔的别名列表。最后展示了
续接上文。
了解过顺序结构分析器之后,接下来要展示的是列表结构分析器ListParser类的代码,如代码8-16所示:
代码8-16
class ListParser extends Parser {
Parser parser;
TokenType terminalTag;
ListParser(Parser parser, TokenType terminalTag) {
super(null);
this.parser = parser;
this.terminalTag = terminalTag;
}
@Override
void parse(ParseContext context) {
if (!context.isPreviousMatched()) {
return;
}
List<Token> matchedTokens = new ArrayList<>();
boolean isNormalEnd = true;
while (true) {
this.parser.parse(context);
if (!context.isPreviousMatched()) {
Token current = context.tokenBuffer.getCurrentToken();
context.tokenBuffer.backIndex();
if (current.type == terminalTag) {
break;
}
isNormalEnd = false;
break;
}
matchedTokens.addAll(context.matchedTokens());
}
if (isNormalEnd) {
context.matchSuccess(matchedTokens);
this.callback(matchedTokens);
}
}
}
ListParser主要被用于处理列表元素。虽然都属于容器类型的分析器,但它与SequenceParser的差异还是很大的,最重点的一点便是它所包含的子分析器是无序的。仍以RULE_BLOCK产生式为例,其包含了多个NAME_MAPPING元素,并且彼此之间无任何顺序性,这正是列表结构的特性,所以我们使用列表结构分析器来对其进行解析。不过有一点要注意,即列表项的数量。当前案例使用了NAME_MAPPING*的形式,这意味着列表中的内容是可选的;而如果使用NAME_MAPPING+形式的话,则表示列表中应至少包含一个元素。虽然只有一个符号之差,但具体的实现逻辑却会有所不同。对于本案例,笔者选择对“*”类型的列表结构进行实现,因为“+”类型的只是前者的子集。
相对于顺序结构分析器,ListParser的实现逻辑要复杂一点,主要体现在如下两个方面:
- 需要组合的子分析器类型固定。ListParser类中只包含了一个固定类型的子分析器,即字段parser所指向的对象,这一点与SequenceParser的实现明显不同。之所以出现这样的情况,是因为列表中的元素类型都是相同的,只使用同一个分析器进行解析也在情理之中。为方便理解,读者可以将列表想象成一个数组对象,所有元素都共享同一个类型。顺序结构则没有这样的要求,其包含的元素可以是任何继承自Parser的对象。
- 需指定列表的结束标识。实现列表类型的分析器时,首先需要克服的一个问题是如何获取列表中元素数量的信息,只有这样才方便对列表进行遍历。否则,您只能明确指定一个标识符来告知分析器应何时结束遍历。仍然以rules脚本块为例,在循环分析NAME_MAPPING符号的时候,如果当前输入的词法单元是end类型的话,就意味着列表内的所有元素都已经分析完毕,可终止循环;否则就会循环解析下去,直到报错或遇到end为止。ListParser类的构造函数中,我们使用参数terminalTag来标识列表项的结束元素。需要注意的是,这一实现方式仅仅是笔者个人的一种设计观点,并不具备规则性。
通过代码8-13,您会发现笔者在实例化ListParser对象的时候,使用了NameMappingParser类型的对象作为其构造函数的参数,这样的话,我们就可以循环使用该对象对列表内的元素进行分析。子分析器NameMappingParser用于解析rules块中名称映射部分的脚本,对应于非终结符NAME_MAPPING,如代码8-17所示:
代码8-17
class NameMappingParser extends SequenceParser {
NameMappingParser(String targetNode) {
super(targetNode,
new TerminalParser(TokenType.ID),
new TerminalParser(TokenType.ID),
new TerminalParser(TokenType.SEMICOLON));
}
@Override
void parse(ParseContext context) {
super.parse(context);
if (!context.isPreviousMatched()) {
return;
}
String fullName = context.matchedTokens().get(0).lexeme;
String alias = context.matchedTokens().get(1).lexeme;
context.nameContainer.add(fullName, alias);
}
}
同RuleBlockParser,类型NameMappingParser也继承自SequenceParser类。通过构造函数可知,该分析器需要处理三个类型的词法单元,和非终结符NAME_MAPPING的定义完全一致。
学习过RuleBlockParser相关的代码之后,相信读者此时应该可以自行实现受理类型(service_type代码块)所对应的分析器了,笔者给出的结果如代码8-18所示。其中ServiceTypeBlockParser类对应于非终结符SERVICE_TYPE_BLOCK,ServiceTypeNameParser类对应于非终结符SERVICE_TYPE_NAME,逻辑比较简单,笔者不做过多说明。
代码8-18
class ServiceTypeBlockParser extends SequenceParser {
ServiceTypeBlockParser(String targetNode) {
super(targetNode,
new TerminalParser(TokenType.SERVICE_TYPES),
new ListParser(new ServiceTypeNameParser("SERVICE_TYPE_NAME"),
TokenType.END),
new TerminalParser(TokenType.END));
}
}
class ServiceTypeNameParser extends SequenceParser {
ServiceTypeNameParser(String targetNode) {
super(targetNode,
new TerminalParser(TokenType.ID),
new TerminalParser(TokenType.SEMICOLON));
}
@Override
void parse(ParseContext context) {
super.parse(context);
if (!context.isPreviousMatched()) {
return;
}
String name = context.matchedTokens().get(0).lexeme;
context.serviceTypeContainer.add(name);
}
}
接下来要学习的是规则绑定(bind_rules)代码块所对应的子分析器。相对于前面所介绍过的分析器,其要更复杂一点,毕竟该结构本身也具备一定的复杂度。这次让我们换个思路,从最细粒度的子分析器代码开始学习。
通过文法8-1中可知,非终结符BINDING由ALIAS_LIST构成,后者表示规则别名的列表,也就是大括号(注意:不包括大括号本身)中的内容。针对这种形式的文法,已经无法再使用ListParser分析器进行解析,因为元素之间是以逗号作为分隔的,但尾部元素后面却没有。按照ListParser的定义,列表中的每一个元素都应该有相同的结构才可以。因此,我们必需引入一个新的解析器。好消息是,针对ALIAS_LIST模式的脚本,笔者在前面内容中展示过类似的案例(即二进制字符串语法分析器),我们可以友情借鉴一下它的实现模式。
子分析器AliasListParser用于对非终结符ALIAS_LIST进行解析,内容较多,笔者分两部分进行说明。代码8-19展示了它的基本结构和构造函数:
代码8-19
class AliasListParser extends Parser {
private TokenType terminalTag;
private List<String> aliases = new ArrayList<>();
AliasListParser(String targetNode, TokenType terminalTag) {
super(targetNode);
this.terminalTag = terminalTag;
}
}
aliases字段用于存储大括号中的别名信息,后续我们会使用该字段的值来构建对应的语义模型。
代码8-20展示了AliasListParser子分析器的核心方法:
代码8-20
@Override
void parse(ParseContext context) {
aliases.clear();
if (!context.isPreviousMatched()) {
return;
}
aliasList(context); //代码1
if (context.isPreviousMatched()) {
List<Token> tokens = aliases.stream()
.map(e -> new Token(TokenType.ID, e))
.collect(Collectors.toList());
context.matchSuccess(tokens); //代码2
this.callback(tokens);
}
}
void aliasList(ParseContext context) {
id(context);
Token current = context.tokenBuffer.getCurrentToken();
if (current.type == this.terminalTag) {
context.matchSuccess(context.matchedTokens());
context.tokenBuffer.backIndex();
return;
}
if (!context.isPreviousMatched()) {
return;
}
Token next = context.tokenBuffer.nextToken();
if (next.type == TokenType.COMMA) {
aliasList(context);
} else if (next.type == this.terminalTag) {
context.tokenBuffer.backIndex();
}
}
void id(ParseContext context) {
this.matched(context, TokenType.ID);
if (context.isPreviousMatched()) {
Token current = context.tokenBuffer.getCurrentToken();
this.aliases.add(current.lexeme);
}
}
void matched(ParseContext context, TokenType target) {
Token current = context.tokenBuffer.nextToken();
if (current.type == target) {
return;
}
String error = this.error(current.lexeme, target.name());
context.matchFailed(error);
}
parse()方法的主体逻辑主要包含如下两部分内容:
- “代码1”处调用aliasList()方法对大括号中的别名列表进行分析,成功的话则将列表中的内容加入到字段aliases之中。
- parse()方法的执行过程中如果未出现语法错误的话,“代码2”处会通过回调和调用context.matchSuccess()方法的方式,将用于构建语义模型BindingConfig的信息传递到子语法分析器的外部。
细心的读者应该注意到了,笔者已经数次使用context对象和回调这两种方式来实现信息的外传。第一种方式比较好理解,为什么要额外增加一种方式呢?这就需要看一下context对象中用于保存已匹配的词法单元列表所对应的数据格式了。假设bind_rules代码块中的内容为“upgrade {ResNotF, ResNotE};”(只有一条配置项),子分析器AliasListParse解析成功后,会将已经匹配的token信息放到ParseContext.MatchResult对象之中,该对象经JSON序列化后将呈代码8-21所示形式:
代码8-21
[
{"lexeme": "ResNotF"},
{"lexeme": "ResNotE"}
]
上述数据的最大用途在于构建语义模型BindingItem。很明显,仅依靠上述信息无法完成该模型的实例化,因为缺少serviceType信息(参考代码8-5)。那么该信息去哪里了呢?这个问题需要通过代码进行解答。让我们看一下子分析器BindingParser的具体实现,其对应于非终结符BINDING,用于解析bind_rules代码块中的绑定项信息,如代码8-22所示:
代码8-22
class BindingParser extends SequenceParser {
private Token serviceType;
private List<Token> aliases;
BindingParser(String targetNode) {
super(targetNode,
new TerminalParser(TokenType.ID),
new TerminalParser(TokenType.OPEN_BRACE),
new AliasListParser("ALIAS_LIST", TokenType.CLOSE_BRACE),
new TerminalParser(TokenType.CLOSE_BRACE),
new TerminalParser(TokenType.SEMICOLON));
parsers[0].setupCallback(this::acceptServiceType);
parsers[2].setupCallback(this::acceptAliases);
}
}
通过构造函数可知,BindingParser由一系列的子分析器所构成。而当下我们最关心的serviceType信息,则是由TerminalParser来进行分析和维护的。所以,想要实例化BindingItem对象,我们必须将TerminalParser和AliasListParser的分析结果集成起来才可以。这也是为什么笔者一再强调,我们需要找到一种方式将信息从子解析器内部传到外部。使用全局变量(比如context对象)是一个不错的解决方案,除此之外也可以采用笔者这种回调的思路。
未完待续……
更多推荐
所有评论(0)