续接上文。

        了解过顺序结构分析器之后,接下来要展示的是列表结构分析器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的实现逻辑要复杂一点,主要体现在如下两个方面:

  1. 需要组合的子分析器类型固定。ListParser类中只包含了一个固定类型的子分析器,即字段parser所指向的对象,这一点与SequenceParser的实现明显不同。之所以出现这样的情况,是因为列表中的元素类型都是相同的,只使用同一个分析器进行解析也在情理之中。为方便理解,读者可以将列表想象成一个数组对象,所有元素都共享同一个类型。顺序结构则没有这样的要求,其包含的元素可以是任何继承自Parser的对象。
  2. 需指定列表的结束标识。实现列表类型的分析器时,首先需要克服的一个问题是如何获取列表中元素数量的信息,只有这样才方便对列表进行遍历。否则,您只能明确指定一个标识符来告知分析器应何时结束遍历。仍然以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. “代码1”处调用aliasList()方法对大括号中的别名列表进行分析,成功的话则将列表中的内容加入到字段aliases之中。
  2. 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对象)是一个不错的解决方案,除此之外也可以采用笔者这种回调的思路。

未完待续……

上一章  下一章

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐