陈颂光
全栈工程师,能够独立开发从解释器到网站和桌面/移动端应用的各类软件。
关注我的 GitHub

用ANTLR解析领域特定语言

在开发各种软件的过程中难免要与各式各样的小语言打交道,例如要读取不同格式的数据(特别是配置)文件。虽然我们可以从头开始自己写程序去解析它们,但这样往往过于耗时且难以维护。ANTLR是Lex(或Flex)与YACC(或Bison)在Java世界的一个代用品,可以根据词法和语法自动生成解析器。

入门

编写语法

处理语言的常规方法是先把输入文本分成单词序列,然后生成以词为叶子的解析树。分词的规则称为词法,而构建解析树的规则称为语法,不同语言有不同的词法和语法。在下例中,我们参考json.org写下JSON的词法和语法到文件JSON.g4中:

grammar JSON;
json
   : value
   ;
value
   :
   | object
   | array
   | STRING
   | NUMBER
   | 'true'
   | 'false'
   | 'null'
   ;
object
   : '{' member (',' member)* '}'
   | '{' '}'
   ;
member
   : STRING ':' value
   ;
array
   : '[' value (',' value)* ']'
   | '[' ']'
   ;
STRING
   : '"' (ESC | ~ ["\\])* '"'
   ;
fragment ESC
   : '\\' (["\\/bnrt] | 'u' HEX HEX HEX HEX)
   ;
fragment HEX
   : [0-9a-fA-F]
   ;
NUMBER
   : '-'? INT ('.' [0-9]+)? EXP?
   ;
fragment INT
   : '0' | [1-9] [0-9]*
   ;
fragment EXP
   : [Ee] [+\-]? [0-9]+
   ;
WS
   : [ \t\n\r] + -> skip
   ;

应该说为ANTLR写的这种上下文无关语法很易读,我们下面还会更详细地介绍语法文件怎样写。以下指出几点:

  • 开首的grammar JSON;JSON必须与文件名JSON.g4中的JSON相同,要改就要一起改。
  • 然后是一些形如规则名 : 分支1 | ... | 分支N ;的规则,一段文本匹配规则相当于它匹配其中一个分支,分支中可用单引号包围要按字面匹配的字符串、用子规则名表示按子规则匹配,另外还可用一些类似正则表达式的记号如?*+|()
    • 语法规则名以小写字母开始,不同语法规则生成解析树不同类型的结点,各分支说明这种结点的子结点序列可以是什么样子
    • 词法规则名以大写字母开始,不同词法规则生成不同语类的词,各分支说明这类词可以是什么样子
    • fragment 开首的规则可以被词法规则引用以便重用代码,但本身不会生成对解析器可见的词

生成解析器

写好语法后我们让ANTLR自动为我们生成解析器代码。在首次使用前需要先下载ANTLR如wget https://www.antlr.org/download/antlr-4.7.2-complete.jar。接着就可以运行ANTLRjava -jar antlr-4.7.2-complete.jar -package com.github.chungkwong.json -o src JSON.g4,其中-o选项用于指定输出位置、-package选项用于指定生成类所属的包。(版本号可以自行改成最新的)

选项 说明
-o 目录 指定输出目录
-lib 目录 指定存放被导入文件的目录
-atn 生成ATN图
-encoding 编码 语法文件的编码方式
-message-format 格式 指定信息的格式:antlrgnuvs2005
-long-messages 显示异常的详情
-listener 生成解析树事件侦听器(默认)
-no-listener 不生成解析树事件侦听器
-visitor 生成解析树访问器
-no-visitor 不生成解析树访问器(默认)
-package 包名 指定生成代码所在的包
-depend 生成文件依赖关系
-D键=值 设置或覆盖语法文件的选项
-Werror 把警告当作错误
-XdbgST 打开StringTemplate可视化器查看生成代码
-XdbgSTWait 等待可视化器被关闭
-Xforce-atn 对所有预测用ATN模拟器
-Xlog 把日志写到antlr-时间.log
-Xexact-output-dir 把所有输出放到-o指定的目录中(忽略包)

为了方便使用也可以把JAR文件加到类路径并设置别名,如:

cd /usr/local/lib
wget https://www.antlr.org/download/antlr-4.7.2-complete.jar
export CLASSPATH=".:/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH"
alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.2-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'

以下我们了解一下生成文件的结构。JSONParser是解析器本身,对每条语法规则有一个同名方法用于以它作为开始规则展开解析,还有一个Context类记录上下文(包括取得子规则上下文的方法),以下略去部分代码:

package com.github.chungkwong.json;
import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.misc.*;
import org.antlr.v4.runtime.tree.*;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"})
public class JSONParser extends Parser {
	static { RuntimeMetaData.checkVersion("4.7.2", RuntimeMetaData.VERSION); }
	protected static final DFA[] _decisionToDFA;
	protected static final PredictionContextCache _sharedContextCache =
		new PredictionContextCache();
	public static final int
		T__0=1, T__1=2, T__2=3, T__3=4, T__4=5, T__5=6, T__6=7, T__7=8, T__8=9, 
		STRING=10, NUMBER=11, WS=12;
	public static final int
		RULE_json = 0, RULE_value = 1, RULE_object = 2, RULE_member = 3, RULE_array = 4;
	public static final String[] ruleNames = {
		"json", "value", "object", "member", "array"
	};
	private static final String[] _LITERAL_NAMES = {
		null, "'true'", "'false'", "'null'", "'{'", "','", "'}'", "':'", "'['", 
		"']'"
	};
	private static final String[] _SYMBOLIC_NAMES = {
		null, null, null, null, null, null, null, null, null, null, "STRING", 
		"NUMBER", "WS"
	};
	public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
	@Override
	public Vocabulary getVocabulary() {
		return VOCABULARY;
	}
	@Override
	public String getGrammarFileName() { return "JSON.g4"; }
	@Override
	public String[] getRuleNames() { return ruleNames; }
	@Override
	public String getSerializedATN() { return _serializedATN; }
	@Override
	public ATN getATN() { return _ATN; }
	public JSONParser(TokenStream input) {
		super(input);
		_interp = new ParserATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
	}
	public static class JsonContext extends ParserRuleContext {
		public ValueContext value() {
			return getRuleContext(ValueContext.class,0);
		}
		public JsonContext(ParserRuleContext parent, int invokingState) {
			super(parent, invokingState);
		}
		@Override public int getRuleIndex() { return RULE_json; }
		@Override
		public void enterRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).enterJson(this);
		}
		@Override
		public void exitRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).exitJson(this);
		}
	}
	public final JsonContext json() throws RecognitionException {
		JsonContext _localctx = new JsonContext(_ctx, getState());
		enterRule(_localctx, 0, RULE_json);
		try {
			enterOuterAlt(_localctx, 1);
			{
			setState(10);
			value();
			}
		}
		catch (RecognitionException re) {
			_localctx.exception = re;
			_errHandler.reportError(this, re);
			_errHandler.recover(this, re);
		}
		finally {
			exitRule();
		}
		return _localctx;
	}
	public static class ValueContext extends ParserRuleContext {
		public ObjectContext object() {
			return getRuleContext(ObjectContext.class,0);
		}
		public ArrayContext array() {
			return getRuleContext(ArrayContext.class,0);
		}
		public TerminalNode STRING() { return getToken(JSONParser.STRING, 0); }
		public TerminalNode NUMBER() { return getToken(JSONParser.NUMBER, 0); }
		public ValueContext(ParserRuleContext parent, int invokingState) {
			super(parent, invokingState);
		}
		@Override public int getRuleIndex() { return RULE_value; }
		@Override
		public void enterRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).enterValue(this);
		}
		@Override
		public void exitRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).exitValue(this);
		}
	}
	public final ValueContext value() throws RecognitionException {/* omitted */}
	public static class ObjectContext extends ParserRuleContext {
		public List<MemberContext> member() {
			return getRuleContexts(MemberContext.class);
		}
		public MemberContext member(int i) {
			return getRuleContext(MemberContext.class,i);
		}
		public ObjectContext(ParserRuleContext parent, int invokingState) {
			super(parent, invokingState);
		}
		@Override public int getRuleIndex() { return RULE_object; }
		@Override
		public void enterRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).enterObject(this);
		}
		@Override
		public void exitRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).exitObject(this);
		}
	}
	public final ObjectContext object() throws RecognitionException {/* omitted */}
	public static class MemberContext extends ParserRuleContext {
		public TerminalNode STRING() { return getToken(JSONParser.STRING, 0); }
		public ValueContext value() {
			return getRuleContext(ValueContext.class,0);
		}
		public MemberContext(ParserRuleContext parent, int invokingState) {
			super(parent, invokingState);
		}
		@Override public int getRuleIndex() { return RULE_member; }
		@Override
		public void enterRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).enterMember(this);
		}
		@Override
		public void exitRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).exitMember(this);
		}
	}
	public final MemberContext member() throws RecognitionException {/* omitted */}
	public static class ArrayContext extends ParserRuleContext {
		public List<ValueContext> value() {
			return getRuleContexts(ValueContext.class);
		}
		public ValueContext value(int i) {
			return getRuleContext(ValueContext.class,i);
		}
		public ArrayContext(ParserRuleContext parent, int invokingState) {
			super(parent, invokingState);
		}
		@Override public int getRuleIndex() { return RULE_array; }
		@Override
		public void enterRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).enterArray(this);
		}
		@Override
		public void exitRule(ParseTreeListener listener) {
			if ( listener instanceof JSONListener ) ((JSONListener)listener).exitArray(this);
		}
	}

	public final ArrayContext array() throws RecognitionException {/* omitted */}
	public static final String _serializedATN =
		"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\3\16;\4\2\t\2\4\3\t"+
		"\3\4\4\t\4\4\5\t\5\4\6\t\6\3\2\3\2\3\3\3\3\3\3\3\3\3\3\3\3\3\3\3\3\5\3"+
		"\27\n\3\3\4\3\4\3\4\3\4\7\4\35\n\4\f\4\16\4 \13\4\3\4\3\4\3\4\3\4\5\4"+
		"&\n\4\3\5\3\5\3\5\3\5\3\6\3\6\3\6\3\6\7\6\60\n\6\f\6\16\6\63\13\6\3\6"+
		"\3\6\3\6\3\6\5\69\n\6\3\6\2\2\7\2\4\6\b\n\2\2\2@\2\f\3\2\2\2\4\26\3\2"+
		"\2\2\6%\3\2\2\2\b\'\3\2\2\2\n8\3\2\2\2\f\r\5\4\3\2\r\3\3\2\2\2\16\27\3"+
		"\2\2\2\17\27\5\6\4\2\20\27\5\n\6\2\21\27\7\f\2\2\22\27\7\r\2\2\23\27\7"+
		"\3\2\2\24\27\7\4\2\2\25\27\7\5\2\2\26\16\3\2\2\2\26\17\3\2\2\2\26\20\3"+
		"\2\2\2\26\21\3\2\2\2\26\22\3\2\2\2\26\23\3\2\2\2\26\24\3\2\2\2\26\25\3"+
		"\2\2\2\27\5\3\2\2\2\30\31\7\6\2\2\31\36\5\b\5\2\32\33\7\7\2\2\33\35\5"+
		"\b\5\2\34\32\3\2\2\2\35 \3\2\2\2\36\34\3\2\2\2\36\37\3\2\2\2\37!\3\2\2"+
		"\2 \36\3\2\2\2!\"\7\b\2\2\"&\3\2\2\2#$\7\6\2\2$&\7\b\2\2%\30\3\2\2\2%"+
		"#\3\2\2\2&\7\3\2\2\2\'(\7\f\2\2()\7\t\2\2)*\5\4\3\2*\t\3\2\2\2+,\7\n\2"+
		"\2,\61\5\4\3\2-.\7\7\2\2.\60\5\4\3\2/-\3\2\2\2\60\63\3\2\2\2\61/\3\2\2"+
		"\2\61\62\3\2\2\2\62\64\3\2\2\2\63\61\3\2\2\2\64\65\7\13\2\2\659\3\2\2"+
		"\2\66\67\7\n\2\2\679\7\13\2\28+\3\2\2\28\66\3\2\2\29\13\3\2\2\2\7\26\36"+
		"%\618";
	public static final ATN _ATN =
		new ATNDeserializer().deserialize(_serializedATN.toCharArray());
	static {
		_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
		for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
			_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
		}
	}
}

JSONListener接口中对于每条语法规则声明了一个enter方法和一个exit方法,以便监听开始尝试和结束尝试一条规则:

package com.github.chungkwong.json;
import org.antlr.v4.runtime.tree.ParseTreeListener;
public interface JSONListener extends ParseTreeListener {
	void enterJson(JSONParser.JsonContext ctx);
	void exitJson(JSONParser.JsonContext ctx);
	void enterValue(JSONParser.ValueContext ctx);
	void exitValue(JSONParser.ValueContext ctx);
	void enterObject(JSONParser.ObjectContext ctx);
	void exitObject(JSONParser.ObjectContext ctx);
	void enterMember(JSONParser.MemberContext ctx);
	void exitMember(JSONParser.MemberContext ctx);
	void enterArray(JSONParser.ArrayContext ctx);
	void exitArray(JSONParser.ArrayContext ctx);
}

JSONBaseListener类是上述接口的一个空实现,可以作为一个出发点去修改:

package com.github.chungkwong.json;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
public class JSONBaseListener implements JSONListener {
	@Override public void enterJson(JSONParser.JsonContext ctx) { }
	@Override public void exitJson(JSONParser.JsonContext ctx) { }
	@Override public void enterValue(JSONParser.ValueContext ctx) { }
	@Override public void exitValue(JSONParser.ValueContext ctx) { }
	@Override public void enterObject(JSONParser.ObjectContext ctx) { }
	@Override public void exitObject(JSONParser.ObjectContext ctx) { }
	@Override public void enterMember(JSONParser.MemberContext ctx) { }
	@Override public void exitMember(JSONParser.MemberContext ctx) { }
	@Override public void enterArray(JSONParser.ArrayContext ctx) { }
	@Override public void exitArray(JSONParser.ArrayContext ctx) { }
	@Override public void enterEveryRule(ParserRuleContext ctx) { }
	@Override public void exitEveryRule(ParserRuleContext ctx) { }
	@Override public void visitTerminal(TerminalNode node) { }
	@Override public void visitErrorNode(ErrorNode node) { }
}

顺便也给出分词器JSONLexer类的内容:

package com.github.chungkwong.json;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.misc.*;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"})
public class JSONLexer extends Lexer {
	static { RuntimeMetaData.checkVersion("4.7.2", RuntimeMetaData.VERSION); }
	protected static final DFA[] _decisionToDFA;
	protected static final PredictionContextCache _sharedContextCache =
		new PredictionContextCache();
	public static final int
		T__0=1, T__1=2, T__2=3, T__3=4, T__4=5, T__5=6, T__6=7, T__7=8, T__8=9, 
		STRING=10, NUMBER=11, WS=12;
	public static String[] channelNames = {
		"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
	};
	public static String[] modeNames = {
		"DEFAULT_MODE"
	};
	public static final String[] ruleNames = {
		"T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", "T__7", "T__8", 
		"STRING", "ESC", "HEX", "NUMBER", "INT", "EXP", "WS"
	};

	private static final String[] _LITERAL_NAMES = {
		null, "'true'", "'false'", "'null'", "'{'", "','", "'}'", "':'", "'['", 
		"']'"
	};
	private static final String[] _SYMBOLIC_NAMES = {
		null, null, null, null, null, null, null, null, null, null, "STRING", 
		"NUMBER", "WS"
	};
	public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
	@Override
	public Vocabulary getVocabulary() {
		return VOCABULARY;
	}
	public JSONLexer(CharStream input) {
		super(input);
		_interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
	}
	@Override
	public String getGrammarFileName() { return "JSON.g4"; }
	@Override
	public String[] getRuleNames() { return ruleNames; }
	@Override
	public String getSerializedATN() { return _serializedATN; }
	@Override
	public String[] getChannelNames() { return channelNames; }
	@Override
	public String[] getModeNames() { return modeNames; }
	@Override
	public ATN getATN() { return _ATN; }
	public static final String _serializedATN =
		"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2\16~\b\1\4\2\t\2\4"+
		"\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13\t"+
		"\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\3\2\3\2\3"+
		"\2\3\2\3\2\3\3\3\3\3\3\3\3\3\3\3\3\3\4\3\4\3\4\3\4\3\4\3\5\3\5\3\6\3\6"+
		"\3\7\3\7\3\b\3\b\3\t\3\t\3\n\3\n\3\13\3\13\3\13\7\13C\n\13\f\13\16\13"+
		"F\13\13\3\13\3\13\3\f\3\f\3\f\3\f\3\f\3\f\3\f\3\f\5\fR\n\f\3\r\3\r\3\16"+
		"\5\16W\n\16\3\16\3\16\3\16\6\16\\\n\16\r\16\16\16]\5\16`\n\16\3\16\5\16"+
		"c\n\16\3\17\3\17\3\17\7\17h\n\17\f\17\16\17k\13\17\5\17m\n\17\3\20\3\20"+
		"\5\20q\n\20\3\20\6\20t\n\20\r\20\16\20u\3\21\6\21y\n\21\r\21\16\21z\3"+
		"\21\3\21\2\2\22\3\3\5\4\7\5\t\6\13\7\r\b\17\t\21\n\23\13\25\f\27\2\31"+
		"\2\33\r\35\2\37\2!\16\3\2\n\4\2$$^^\t\2$$\61\61^^ddppttvv\5\2\62;CHch"+
		"\3\2\62;\3\2\63;\4\2GGgg\4\2--//\5\2\13\f\17\17\"\"\2\u0085\2\3\3\2\2"+
		"\2\2\5\3\2\2\2\2\7\3\2\2\2\2\t\3\2\2\2\2\13\3\2\2\2\2\r\3\2\2\2\2\17\3"+
		"\2\2\2\2\21\3\2\2\2\2\23\3\2\2\2\2\25\3\2\2\2\2\33\3\2\2\2\2!\3\2\2\2"+
		"\3#\3\2\2\2\5(\3\2\2\2\7.\3\2\2\2\t\63\3\2\2\2\13\65\3\2\2\2\r\67\3\2"+
		"\2\2\179\3\2\2\2\21;\3\2\2\2\23=\3\2\2\2\25?\3\2\2\2\27I\3\2\2\2\31S\3"+
		"\2\2\2\33V\3\2\2\2\35l\3\2\2\2\37n\3\2\2\2!x\3\2\2\2#$\7v\2\2$%\7t\2\2"+
		"%&\7w\2\2&\'\7g\2\2\'\4\3\2\2\2()\7h\2\2)*\7c\2\2*+\7n\2\2+,\7u\2\2,-"+
		"\7g\2\2-\6\3\2\2\2./\7p\2\2/\60\7w\2\2\60\61\7n\2\2\61\62\7n\2\2\62\b"+
		"\3\2\2\2\63\64\7}\2\2\64\n\3\2\2\2\65\66\7.\2\2\66\f\3\2\2\2\678\7\177"+
		"\2\28\16\3\2\2\29:\7<\2\2:\20\3\2\2\2;<\7]\2\2<\22\3\2\2\2=>\7_\2\2>\24"+
		"\3\2\2\2?D\7$\2\2@C\5\27\f\2AC\n\2\2\2B@\3\2\2\2BA\3\2\2\2CF\3\2\2\2D"+
		"B\3\2\2\2DE\3\2\2\2EG\3\2\2\2FD\3\2\2\2GH\7$\2\2H\26\3\2\2\2IQ\7^\2\2"+
		"JR\t\3\2\2KL\7w\2\2LM\5\31\r\2MN\5\31\r\2NO\5\31\r\2OP\5\31\r\2PR\3\2"+
		"\2\2QJ\3\2\2\2QK\3\2\2\2R\30\3\2\2\2ST\t\4\2\2T\32\3\2\2\2UW\7/\2\2VU"+
		"\3\2\2\2VW\3\2\2\2WX\3\2\2\2X_\5\35\17\2Y[\7\60\2\2Z\\\t\5\2\2[Z\3\2\2"+
		"\2\\]\3\2\2\2][\3\2\2\2]^\3\2\2\2^`\3\2\2\2_Y\3\2\2\2_`\3\2\2\2`b\3\2"+
		"\2\2ac\5\37\20\2ba\3\2\2\2bc\3\2\2\2c\34\3\2\2\2dm\7\62\2\2ei\t\6\2\2"+
		"fh\t\5\2\2gf\3\2\2\2hk\3\2\2\2ig\3\2\2\2ij\3\2\2\2jm\3\2\2\2ki\3\2\2\2"+
		"ld\3\2\2\2le\3\2\2\2m\36\3\2\2\2np\t\7\2\2oq\t\b\2\2po\3\2\2\2pq\3\2\2"+
		"\2qs\3\2\2\2rt\t\5\2\2sr\3\2\2\2tu\3\2\2\2us\3\2\2\2uv\3\2\2\2v \3\2\2"+
		"\2wy\t\t\2\2xw\3\2\2\2yz\3\2\2\2zx\3\2\2\2z{\3\2\2\2{|\3\2\2\2|}\b\21"+
		"\2\2}\"\3\2\2\2\17\2BDQV]_bilpuz\3\b\2\2";
	public static final ATN _ATN =
		new ATNDeserializer().deserialize(_serializedATN.toCharArray());
	static {
		_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
		for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
			_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
		}
	}
}

JSON.tokensJSONLexer.tokens文件为每个词类指定了一个代号:

T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
T__6=7
T__7=8
T__8=9
STRING=10
NUMBER=11
WS=12
'true'=1
'false'=2
'null'=3
'{'=4
','=5
'}'=6
':'=7
'['=8
']'=9

JSON.interpJSONLexer.interp文件给出了内部表示,一般不用管它。

使用解析器

现在我们可以使用分词器和解析器了:

  1. 创建分词器。如JSONLexer lexer = new JSONLexer(输入);,其中输入可以是用以下方法获取:
    • CharStreams.fromChannel(ReadableByteChannel channel)
    • CharStreams.fromChannel(ReadableByteChannel channel, Charset charset)
    • CharStreams.fromChannel(ReadableByteChannel channel, Charset charset, int bufferSize, CodingErrorAction decodingErrorAction, String sourceName, long inputSize)
    • CharStreams.fromChannel(ReadableByteChannel channel, int bufferSize, CodingErrorAction decodingErrorAction, String sourceName)
    • CharStreams.fromPath(Path path)
    • CharStreams.fromPath(Path path, Charset charset)
    • CharStreams.fromReader(Reader r)
    • CharStreams.fromReader(Reader r, String sourceName) CharStreams.fromStream(InputStream is)
    • CharStreams.fromStream(InputStream is, Charset charset)
    • CharStreams.fromStream(InputStream is, Charset charset, long inputSize)
    • CharStreams.fromString(String s)
  2. 取得词流。如CommonTokenStream tokens = new CommonTokenStream(lexer);
  3. 创建解析器。如JSONParser parser = new JSONParser(tokens)
  4. 取得解析树。如JSONParser.ObjectContext tree = parser.object();object为开始规则解析。
  5. 使用解析树
    • 遍历解析树。如ParseTreeWalker.DEFAULT.walk(new JSONBaseListener(), tree);
    • 按XPath寻找子树。如Collection<ParseTree> subtrees=XPath.findAll(tree,xpath,parser);,其中字符串xpath是由/(表示孩子)或//(表示后代)分隔的一些规则名(匹配指定规则生成的结点)、'字符串'(匹配字面上的词)、*(通配)或它们之一前面加!(表示否定)。

如果要处理大文件,生成完整的解析树可能占用太多内存。类似于XML的SAX解析器,这时可以用parser.setBuildParseTree(false);禁止生成完整语法树,并通过 parser.addParseListener(new JSONBaseListener());设置侦听器。

动态加载语法

如果更改语法时不容许重新编译代码,ANTLR也可以动态加载语法,不过性能可能会差一点点。例如以下代码可以按动态给出的词法和语法解析输入流以得出语法树:

LexerGrammar lg=new LexerGrammar(词法);
Grammar g=new Grammar(语法);
LexerInterpreter lexEngine =lg.createLexerInterpreter(CharStreams.fromStream(输入流));
CommonTokenStream tokens = new CommonTokenStream(lexEngine);
ParserInterpreter parser = g.createParserInterpreter(tokens);
ParseTree t = parser.parse(g.rules.get(开始规则名).index);

注意,动态加载语法中所有动作代码(包括谓词)会被忽略。

Maven插件

利用Maven插件,解析器可以在构建项目时自动生成。为此,在pom.xmlbuild元素的plugins元素中加入:

<plugin>
  <groupId>org.antlr</groupId>
  <artifactId>antlr4-maven-plugin</artifactId>
  <version>4.7.2</version>
  <executions>
    <execution>
      <id>antlr</id>
      <goals>
        <goal>antlr4</goal>
      </goals>
    </execution>
  </executions>
</plugin>

然后把语法文件放到src/main/antlr4/目录下并按包名组织。如果有其它需要被包含的语法文件,则放到src/main/antlr4/imports目录下。

语法文件

描述ANTLR语法和/或词法文件名形如标题.g4,内容由以下部分组成:

  1. 头,形如以下之一:
    • grammar 标题;表示这文件同时描述词法和语法
    • lexer grammar 标题;表示这文件只描述词法
    • parser grammar 标题;表示这文件只描述语法
  2. 选项(可选),形如options { 键1=值1; ... 键N=值N; },其中可指定的键有:
    • superClass:分词器或解析器的父类
    • language:生成用指定语言编写的代码
    • tokenVocab:使用指定文件(加后缀.token的属性文件)给出的词类代码
    • TokenLabelType:表示词类的类型,默认为Token
    • contextSuperClass:表示语法树的类型(应派生自RuleContext),默认为ParserRuleContext
  3. 导入(可选),形如import 导入文件的标题,...;。导入的效果是依次把被导入文件中规则加到最后(从而规则同名时以当前文件中的为准),词类、通道和命名动作分别合并。纯词法只能导入纯词法,纯语法只能导入纯语法,混合语法可以导入纯语法或没有模式的纯词法。导入可以递归。
  4. 词类声明(可选),形如tokens { 词类名, ... },列出额外词类(没有词法规则的)以便动作代码使用。
  5. 通道(可选,只适用于纯词法),形如channels {通道名,...},列出自定义通道。
  6. 命名动作(可选),形如@动作名 {代码},用于把代码注入到解析器中。其中动作名可以是:
    • header表示把代码注入到类声明前
    • members表示把代码注入到类内作为字段或方法
  7. 一条或以上规则,简单的规则形如规则名 : 分支1 | ... | 分支N ;,其中词法规则名由大写字母开始而语法规则名由小写字母开始。更复杂的语法规则形如:
规则名[参数声明,...] returns [返回值声明,...] locals [局部总量声明,...] : 分支1 | ... | 分支N ;

各种名称可以由字母、数字、下划线组成(支持Unicode),但不能是关键词importfragmentlexerparsergrammarreturnslocalsthrowscatchfinallymodeoptionstokens。另外文件中可以使用Java风格的注释//行末注释/* 注释 *//** Javadocs */

词法规则

分支 匹配
词类名 词类中的词
'字符序列' 字面上的字符序列,除了转义序列\n(换行)、\r(回车)、\t(制表符)、\b(退格)、\f(换页)、\uXXXX(Unicode四位十六进制代码点)或\u{XXXXXX}’(Unicode十六进制代码点)
[字符集] 字符集中的一个字符,其中字符集由单字符(包括上述转义序列、\\\]\-)、形如单字符-单字符的字符区间、形如\p{属性名}\p{枚举属性=值}的Unicode子集、以及它们形如\P{属性名}\P{枚举属性=值}的补集组成
'字符'..'字符' 字符区间中的字符(包括这两个字符)
. 任何一个字符
词法规则 匹配指定词法规则(包括fragment规则)的字符串,可以递归但不能左递归(需要手动改成右递归)
{动作代码} 空,用于在读取到这位置时执行指定代码,当代码中花括号不配对时额外的花括号要用\{\}转义
{谓词代码}? 空,布尔表达式的值为假时放弃继续尝试当前规则
~子规则 一个不匹配指定子规则的字符
子规则 子规则 由分别匹配子规则的字符串接起来的
子规则* 由零个或多个匹配子规则的字符串串接起来,匹配尽可能长
子规则+ 由一个或多个匹配子规则的字符串串接起来,匹配尽可能长
子规则? 由零个或一个匹配子规则的字符串串接起来,匹配尽可能长
子规则*? 由零个或多个匹配子规则的字符串串接起来,匹配尽可能短
子规则+? 由一个或多个匹配子规则的字符串串接起来,匹配尽可能短
子规则?? 由零个或一个匹配子规则的字符串串接起来,匹配尽可能短

在动作代码或谓词代码中可以通过$规则名引用匹配子规则的词(当有多个同名子规则时可在规则前加上名称=来指定别名),进而可通过.引用其字段或方法,例如以下只读属性:

属性 方法 类型
text getText String 匹配的文本
type getType int 词类代号
line getLine int 词开始的行号(从1开始)
pos getCharPositionInLine int 词在行的偏移(从0开始)
index getTokenIndex int 当前词的序号(从0开始
channel getChannel int 通道代码,默认为0 (Token.DEFAULT_CHANNEL),隐藏通道为Token.HIDDEN_CHANNEL
int   int 匹配文本表示的整数

在一个分支的最后可以加上->命令,...,其中可用的命令有:

  • skip用于放弃当前词
  • mode(模式)修改栈顶模式(栈可用于实现仅用正则表达式无法描述的模式,如某些语言容许的嵌套注释)
  • pushMode(模式)推入栈顶模式
  • popMode弹出栈顶模式
  • more要求继续匹配以延长当前词
  • type(词类)用于修改当前词所属的词类
  • channel(通道)用于把当前词送到指定通道

如果一条词法规则纯粹为了共用代码或提高可读性,而不是要实际生成词,可在规则前加上fragment

语法规则

语法分支可以由以下元素组成:

构造 描述
词类名 匹配词类中的词,特殊词EOF用于标记输入结束
'字符串 匹配恰由指定字符串组成的词
语法规则 匹配指定规则的一列词
语法规则 [参数,...] 匹配指定规则的一列词,参数的值将传递给该规则
{动作代码} 马上执行指定代码,其中可以通过$x$x.y引用属性,并可通过$x::y引用非局部属性(动态作用域)
{谓词代码}? 若代码的值为false则放弃尝试当前分支
. 匹配任何词(除了EOF
(分支|...) 匹配匹配其中一个子分支的词列
元素? 匹配匹配零个或一个元素的词列,匹配尽可能长
元素* 匹配匹配零个或以上元素的词列,匹配尽可能长
元素+ 匹配匹配一个或以上元素的词列,匹配尽可能长
元素?? 匹配匹配零个或一个元素的词列,匹配尽可能短
元素*? 匹配匹配零个或以上元素的词列,匹配尽可能短
元素+? 匹配匹配一个或以上元素的词列,匹配尽可能短

在动作代码或谓词代码中可以通过$规则名引用匹配子规则的文本(当有多个同名子规则时可在规则前加上名称=来指定别名),进而可通过.引用其字段或方法,例如以下只读属性:

属性 类型
text String 已匹配的文本(包括隐藏通道中的)
start Token 主通道中首个匹配的词
stop Token 主通道中最后一个匹配的词(只适用于末尾的动作和finally动作)
ctx ParserRuleContext 上下文对象

如果希望通过侦听器监视什么时候开始和结束结束尝试分支,可以在分支后加上#名称(一个规则要么所有分支都有名称,要么都没有),多个分支可以有相同名称。

总结

如果对用ANTLR描述语法有疑问,可以参考各种常见语言的ANTLR语法。关于ANTLR的细节,可参阅官方文档API。顺带一提,ANTLR不仅可以生成用Java编写的解析器,也支持C#、Python、JavaScript、Go、C++、Swift等等。

关键词 java