都灵JVM编程语言:使用ANTLR构建高级词法分析器
正如我在上一篇文章中所写的那样,我最近开始研究一种名为Turin的新编程语言。 可以在GitHub上找到适用于languag初始版本的编译器。 我目前正在改进语言,并正在开发Maven和IntelliJ插件。 在这里和下一篇文章中,我将介绍编译器和相关工具的不同组件。
编译器的结构
编译器需要做几件事:
第一步需要构建两个组件:词法分析器和解析器。 词法分析器对文本进行操作并生成标记序列,而解析器将标记组合到用于创建AST的构造(类型声明,语句,表达式等)中。 为了编写词法分析器和解析器,我使用了ANTLR。
在本文的其余部分,我们将研究词法分析器。 解析器和编译器的其他组件将在以后的文章中讨论。
为什么要使用ANTLR?
ANTLR是用于编写词法分析器和解析器的非常成熟的工具。 它可以生成多种语言的代码,并具有良好的性能。 它维护良好,我确信它具有处理可能遇到的所有极端情况所需的所有功能。 除此之外,ANTLR 4可以编写简单的语法,因为它可以为您解决左递归定义。 因此,您不必编写许多中间节点类型即可为表达式指定优先级规则。 我们将在分析器中对此进行更多介绍。
Xtext使用了ANTLR(我已经使用了很多),并且在为.NET平台 (一种用于.NET的EMF)构建模型驱动的开发框架时 ,我使用了ANTLR。 因此,我知道并信任ANTLR,因此没有理由寻找其他选择。
当前的词法分析器语法
这是词法分析器语法的当前版本。
lexer grammar TurinLexer;@header {}@lexer::members {public static final int WHITESPACE = 1;public static final int COMMENTS = 2; }// It is suggested to define the token types reused in different mode. // See mode in-interpolation below tokens { VALUE_ID, TYPE_ID, INT, LPAREN, RPAREN, COMMA, RELOP, AND_KW, OR_KW, NOT_KW }// Of course keywords has to be defined before the rules for identifiers NAMESPACE_KW : 'namespace'; PROGRAM_KW : 'program'; PROPERTY_KW : 'property'; TYPE_KW : 'type'; VAL_KW : 'val'; HAS_KW : 'has'; ABSTRACT_KW : 'abstract'; SHARED_KW : 'shared'; IMPORT_KW : 'import'; AS_KW : 'as'; VOID_KW : 'Void'; RETURN_KW : 'return'; FALSE_KW : 'false'; TRUE_KW : 'true'; IF_KW : 'if'; ELIF_KW : 'elif'; ELSE_KW : 'else';// For definitions reused in mode in-interpolation we define and refer to fragments AND_KW : F_AND; OR_KW : F_OR; NOT_KW : F_NOT;LPAREN : '('; RPAREN : ')'; LBRACKET : '{'; RBRACKET : '}'; LSQUARE : '['; RSQUARE : ']'; COMMA : ','; POINT : '.'; COLON : ':'; // We use just one token type to reduce the number of states (and not crash Antlr...) // https://github.com/antlr/antlr4/issues/840 EQUAL : '==' -> type(RELOP); DIFFERENT : '!=' -> type(RELOP); LESSEQ : '<=' -> type(RELOP); LESS : '<' -> type(RELOP); MOREEQ : '>=' -> type(RELOP); MORE : '>' -> type(RELOP); // ASSIGNMENT has to comes after EQUAL ASSIGNMENT : '='; // Mathematical operators cannot be merged in one token type because // they have different precedences ASTERISK : '*'; SLASH : '/'; PLUS : '+'; MINUS : '-';PRIMITIVE_TYPE : F_PRIMITIVE_TYPE; BASIC_TYPE : F_BASIC_TYPE;VALUE_ID : F_VALUE_ID; // Only for types TYPE_ID : F_TYPE_ID; INT : F_INT;// Let's switch to another mode here STRING_START : '"' -> pushMode(IN_STRING);WS : (' ' | '\t')+ -> channel(WHITESPACE); NL : '\r'? '\n';COMMENT : '/*' .*? '*/' -> channel(COMMENTS);LINE_COMMENT : '//' ~[\r\n]* -> channel(COMMENTS);mode IN_STRING;STRING_STOP : '"' -> popMode; STRING_CONTENT : (~["\\#]|ESCAPE_SEQUENCE|SHARP)+; INTERPOLATION_START : '#{' -> pushMode(IN_INTERPOLATION);mode IN_INTERPOLATION;INTERPOLATION_END : '}' -> popMode; I_PRIMITIVE_TYPE : F_PRIMITIVE_TYPE -> type(PRIMITIVE_TYPE); I_BASIC_TYPE : F_BASIC_TYPE -> type(BASIC_TYPE); I_FALSE_KW : 'false' -> type(FALSE_KW); I_TRUE_KW : 'true' -> type(TRUE_KW); I_AND_KW : F_AND -> type(AND_KW); I_OR_KW : F_OR -> type(OR_KW); I_NOT_KW : F_NOT -> type(NOT_KW); I_IF_KW : 'if' -> type(IF_KW); I_ELSE_KW : 'else' -> type(ELSE_KW); I_VALUE_ID : F_VALUE_ID -> type(VALUE_ID); I_TYPE_ID : F_TYPE_ID -> type(TYPE_ID); I_INT : F_INT -> type(INT); I_COMMA : ',' -> type(COMMA); I_LPAREN : '(' -> type(LPAREN); I_RPAREN : ')' -> type(RPAREN); I_LSQUARE : '[' -> type(LSQUARE); I_RSQUARE : ']' -> type(RSQUARE);I_ASTERISK : '*' -> type(ASTERISK); I_SLASH : '/' -> type(SLASH); I_PLUS : '+' -> type(PLUS); I_MINUS : '-' -> type(MINUS);I_POINT : '.' -> type(POINT); I_EQUAL : '==' -> type(RELOP); I_DIFFERENT : '!=' -> type(RELOP); I_LESSEQ : '<=' -> type(RELOP); I_LESS : '<' -> type(RELOP); I_MOREEQ : '>=' -> type(RELOP); I_MORE : '>' -> type(RELOP); I_STRING_START : '"' -> type(STRING_START), pushMode(IN_STRING); I_WS : (' ' | '\t')+ -> type(WS), channel(WHITESPACE);fragment F_AND : 'and'; fragment F_OR : 'or'; fragment F_NOT : 'not'; fragment F_VALUE_ID : ('_')*'a'..'z' ('A'..'Z' | 'a'..'z' | '0'..'9' | '_')*; // Only for types fragment F_TYPE_ID : ('_')*'A'..'Z' ('A'..'Z' | 'a'..'z' | '0'..'9' | '_')*; fragment F_INT : '0'|(('1'..'9')('0'..'9')*); fragment F_PRIMITIVE_TYPE : 'Byte'|'Int'|'Long'|'Boolean'|'Char'|'Float'|'Double'|'Short'; fragment F_BASIC_TYPE : 'UInt';fragment ESCAPE_SEQUENCE : '\\r'|'\\n'|'\\t'|'\\"'|'\\\\'; fragment SHARP : '#'{ _input.LA(1)!='{' }?;我已经做了一些选择:
- 有两种不同类型的ID: VALUE_ID和TYPE_ID。 由于可以轻松地区分值和类型,因此语法上的歧义性较小。 在Java中,当遇到(foo)时,我们不知道它是表达式(对括号之间foo表示的值的引用)还是类型foo的强制转换。 我们需要看下面的内容才能理解它。 我认为这很愚蠢,因为实际上每个人都只对类型使用大写的标识符,但是由于这不是由语言强制执行的,因此编译器无法从中受益
- 换行符与都灵相关,因此我们有针对它们的标记,我们基本上希望语句以换行符终止,但我们在逗号后接受可选的换行符
- 空格(但换行符)和注释是在它们自己的通道中捕获的,因此我们可以在解析器语法中忽略它们,但可以在需要时检索它们。 例如,我们需要它们来突出显示语法,并且通常需要IntelliJ插件,因为它需要为源文件中的每个单个字符定义标记,而没有空格
- 最棘手的部分是在Ruby中解析字符串插值,例如“我的名字是#{user.name}”。 我们使用模式:遇到字符串开始(“)时,我们切换到词法分析器模式IN_STRING。 在IN_STRING模式下,如果遇到插值(#{)的开始,我们将移至词法分析器模式IN_INTERPOLATION。 在IN_INTERPOLATION模式下,我们需要接受表达式中使用的大多数标记(可悲的是,这意味着我们的词法分析器语法有很多重复)。
- 我不得不将关系运算符折叠为一种令牌类型,以使生成的词法分析器的状态数不会太大。 这意味着我将不得不查看RELOP令牌的文本,以确定需要执行哪个操作。 没什么可怕的,但是您必须知道如何解决此类问题。
测试词法分析器
我写了很多针对词法分析器的测试。 特别是,我测试了最复杂的部分:有关字符串插值的部分。
一些测试的示例:
@Testpublic void parseStringWithEmptyInterpolation() throws IOException {String code = "\"Hel#{}lo!\"";verify(code, TurinLexer.STRING_START, TurinLexer.STRING_CONTENT, TurinLexer.INTERPOLATION_START, TurinLexer.INTERPOLATION_END, TurinLexer.STRING_CONTENT, TurinLexer.STRING_STOP);}@Testpublic void parseStringWithInterpolationContainingID() throws IOException {String code = "\"Hel#{foo}lo!\"";verify(code, TurinLexer.STRING_START, TurinLexer.STRING_CONTENT, TurinLexer.INTERPOLATION_START,TurinLexer.VALUE_ID,TurinLexer.INTERPOLATION_END, TurinLexer.STRING_CONTENT, TurinLexer.STRING_STOP);}@Testpublic void parseStringWithSharpSymbol() throws IOException {String code = "\"Hel#lo!\"";verify(code, TurinLexer.STRING_START, TurinLexer.STRING_CONTENT, TurinLexer.STRING_STOP);}@Testpublic void parseMethodDefinitionWithExpressionBody() throws IOException {String code = "Void toString() = \"foo\"";verify(code, TurinLexer.VOID_KW, TurinLexer.VALUE_ID, TurinLexer.LPAREN, TurinLexer.RPAREN, TurinLexer.ASSIGNMENT, TurinLexer.STRING_START, TurinLexer.STRING_CONTENT, TurinLexer.STRING_STOP);}如您所见,我只是在字符串上测试令牌,并验证它是否生成了正确的令牌列表。 简单而直接。
结论
我在ANTLR上使用该语言的经验并不完美:存在问题和局限性。 必须在单个令牌类型中折叠多个运算符并不好。 必须为不同的词法分析器模式重复几个标记定义是不好的。 但是,ANTLR被证明是在实践中可用的工具:它可以完成它需要做的所有事情,并且对于每个问题都有一个可接受的解决方案。 解决方案可能不是理想的,也许不是理想的解决方案,但是有一个解决方案。 因此,我可以使用它并继续进行编译器中更有趣的部分。
翻译自: https://www.javacodegeeks.com/2015/09/turin-programming-language-for-the-jvm-building-advanced-lexers-with-antlr.html
总结
以上是生活随笔为你收集整理的都灵JVM编程语言:使用ANTLR构建高级词法分析器的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 安卓新特性设备管理(安卓新特性)
- 下一篇: jboss 发布web_JBoss模块示