当前位置: 首页 > 工具软件 > ANTLR Ruby > 使用案例 >

jvm与非jvm语言优劣_都灵JVM编程语言:使用ANTLR构建高级词法分析器

党俊健
2023-12-01

jvm与非jvm语言优劣

正如我在一篇文章中所写的那样,我最近开始研究一种名为Turin的新编程语言。 可以在GitHub上找到适用于languag初始版本的编译器。 我目前正在改善语言,并正在开发MavenIntelliJ插件。 在这里和下一篇文章中,我将介绍编译器和相关工具的不同组件。

编译器的结构

编译器需要做几件事:

  1. 获取源代码并生成抽象语法树(AST)
  2. 通过不同阶段转换AST以简化处理。 我们基本上希望从非常接近语法的表示形式过渡到更易于处理的表示形式。 例如,我们可以对语言进行“去糖化”,将几种(显然)不同的结构表示为同一结构的变体。 一个例子? Java编译器将字符串连接转换为对StringBuffer.append的调用
  3. 执行语义检查。 例如,我们要检查所有表达式是否都使用可接受的类型(我们不想对字符求和,对吗?)
  4. 产生字节码

第一步需要构建两个组件:词法分析器和解析器。 词法分析器对文本进行操作并生成标记序列,而解析器将标记组合到创建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_IDTYPE_ID。 由于可以容易地区分值和类型,因此语法上的歧义性较小。 在Java中,当遇到(foo)时,我们不知道它是表达式(对括号之间foo表示的值的引用)还是强制转换为foo类型 我们需要看下面的内容才能理解它。 我认为这很愚蠢,因为实际上每个人都只对类型使用大写的标识符,但是由于这不是由语言强制执行的,因此编译器无法利用它
  • 换行符与都灵相关,因此我们有针对它们的令牌,我们基本上希望语句以换行符终止,但我们在逗号后接受可选的换行符
  • 空格(但换行符)和注释是在它们自己的通道中捕获的,因此我们可以在解析器语法中忽略它们,但可以在需要时检索它们。 例如,我们需要它们来突出显示语法,并且通常需要IntelliJ插件,因为它需要为源文件中的每个单个字符定义标记,而没有间隙
  • 最棘手的部分是在Ruby中解析字符串插值,例如“我的名字是#{user.name}”。 我们使用模式:遇到字符串开始(“)时,我们切换到词法分析器模式IN_STRING。 在IN_STRING模式下,如果遇到插值(#{)的开始,我们将移至词法分析器模式IN_INTERPOLATION。 在IN_INTERPOLATION模式下,我们需要接受表达式中使用的大多数标记(这在词法分析器语法中意味着很多重复)。
  • 我不得不将关系运算符折叠为一个单一的令牌类型,以使生成的词法分析器的状态数不会太大。 这意味着我将不得不查看RELOP令牌的文本,以确定需要执行哪个操作。 没什么可怕的,但是您必须知道如何解决此类问题。

测试词法分析器

我写了许多针对词法分析器的测试。 特别是,我测试了最复杂的部分:有关字符串插值的部分。

一些测试的示例:

@Test
    public 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);
    }
 
    @Test
    public 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);
    }
 
    @Test
    public void parseStringWithSharpSymbol() throws IOException {
        String code = "\"Hel#lo!\"";
        verify(code, TurinLexer.STRING_START, TurinLexer.STRING_CONTENT, TurinLexer.STRING_STOP);
    }
 
    @Test
    public 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与非jvm语言优劣

 类似资料: