(四十六)深度解析领域特定语言(DSL)第八章——语法分析器组合子:案例实现(Part2)
续接上文。
了解过顺序结构分析器之后,接下来要展示的是列表结构分析器ListParser类的代码,如代码8-16所示:
代码8-16class ListParser extends Parser {Parser parser;TokenType terminalTag; ListParser(Parser parser, TokenType terminalTag) {super(null);this.parser = parser;this.terminalTag = terminalTag;}@Overridevoid 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-17class NameMappingParser extends SequenceParser {NameMappingParser(String targetNode) {super(targetNode,new TerminalParser(TokenType.ID),new TerminalParser(TokenType.ID),new TerminalParser(TokenType.SEMICOLON));}@Overridevoid 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-18class 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));}@Overridevoid 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-19class 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); //代码1if (context.isPreviousMatched()) {List<Token> tokens = aliases.stream().map(e -> new Token(TokenType.ID, e)).collect(Collectors.toList());context.matchSuccess(tokens); //代码2this.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-22class 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对象)是一个不错的解决方案,除此之外也可以采用笔者这种回调的思路。
未完待续……
上一章 下一章