基于antlr的词法和语法分析

最近工作经常跟antlr打交道,一开始半懂不懂,各种瞎写胡试,也能work。工作做完之后就想系统了解下编译器前端到底做了哪些事情,也就是词法分析和语法分析究竟做了什么,以及antlr的lexer和parser的语法。

编译器前端到底做了哪些事情

从一个比较high level的角度看待编译器前端对字符串的处理流程,第一步是做词法解析,将字符串解析成Token序列,第二步是做语法分析,根据语法匹配命中相应的语法规则。

  1. 词法分析

    以这句SPL查询search id |stats count by id 为例,词法分析做的事情就是把这个字符串解析成Token序列:search、id、stats、count、by、|,同时也会拿到这些token对应的类型。

  2. 语法分析

    词法分析的结果作为语法分析器的输入,经过语法分析之后会确定这个SPL的AST语法树。

antlr语法

  1. Lexer语法规则

    (1)匹配规则

    最长匹配

    字符串可能会命中多个词法,这时候会选择最长命中词法。举例如下

    1
    输入:a=b
    1
    2
    3
    4
    词法文件
    EQ: '=';
    IDENTIFIER:[a-zA-z]*;
    ANY: .*; // 命中规则ANY

    这时候词法分析不会将a=b理解成a、=、b,而是将他们看成一个整体

    顺序匹配

    字符串命中多个词法,在字符串长度一致的情况下,会按照从上至下的顺序优先匹配最先出现的词法。举例如下

    1
    输入:a
    1
    2
    3
    语法文件
    IDENTIFIER:[a-zA-z]*; // 命中规则IDENTIFIER
    ANY: .*;

    这时候词法分析会优先命中最先出现的词法IDENTIFER

    (2)语法关键字理解 antlr lexer 语法

    • skip

      skip会告诉lexer丢弃掉当前匹配到的词,去读下一个词。比如我们在自定义语法的时候,必然会打很多空格,这些空格其实是没有什么含义的,不想对空格做特殊处理的话就可以把这些空格skip掉啦

      1
      WS: [ \t\r\n\u3000]+ -> skip;
    • more

      more这个我很少用到,大概做的跟skip反过来,遇到more的时候就会告诉lexer再去读一个词

      1
      2
      3
      4
      LQUOTE : '"' -> more, mode(STR);
      mode STR;
      STRING : '"' -> mode(DEFAULT_MODE) ; // token we want parser to see
      TEXT : . -> more ; // collect more text for string

      当遇到"时,我们告诉lexer还需要匹配更多词,同时通过mode进入STR模式

    • mode(x)

      通过mode建立一个新的命名空间,可以解决与其他模式的冲突,进入新的命名空间可以用其他命名空间定义的词法

      1
      2
      3
      4
      WHERE: 'where'

      mode(NEWSPACE);
      NEWSPACE_WHERE: WHERE -> type(WHERE)

      这种写法在进入NEWSPACE命名空间之后,就可以复用在其他命名空间定义的WHERE

    • pushMode(x)

      进入新的命名空间

      1
      2
      3
      4
      5
      CDATA   : '<![CDATA[' .*? ']]>' ;OPEN : '<' -> pushMode(INSIDE);
      mode INSIDE;
      CLOSE : '>' -> popMode ;
      SPECIAL_CLOSE: '?>' -> popMode ; // close <?xml...?>
      SLASH_CLOSE : '/>' -> popMode ;

      这时候会进入新的命名空间INSIDE

    • popMode(x)

      退出当前命名空间,如果直接写popMode,就会回到原来的命名空间,这时候可以理解pushMode是不断的压栈,栈顶元素是当前命名空间

    • type(x)

      解决的是不同命名空间下,可以使用同名Token的问题,参考mode(x)的说明

    • channel(x)

      当我们使用定义的语法去写SPL的时候,我们是不会关心像注释、空格这样的东西,这个时候可以将注释、空格导流到不同的channel,源文件解析后的Token会进入默认的channle。

      1
      2
      3
      4
      5
      6
      7
      @lexer::members{
      public static final int WHITESPACE = 1;
      public static final int COMMENTS = 2;
      }

      WS: [ \t\r\n\u3000]+ -> channel(WHITESPACE);
      COMMENT: '//' .*? '\n' -> channel(COMMENTS);
  2. 语法解析规则

    其实parser这边没什么好讲的,parser就是根据Token序列判断是哪个语法。举个整体的例子吧

    词法文件

    1
    2
    3
    4
    5
    6
    lexer grammar Splv1Lexer;

    SORT: 'sort';
    TOP: 'top';
    BY: 'by';
    IDENTIFIER: [a-zA-Z0-9]*

    语法文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    lexer grammar Splv1Parser;
    options { tokenVocab=Splv1Lexer; }
    main
    : operators EOF
    ;

    operators:
    : operator*
    ;

    operator:
    : SORT BY field #sort
    | TOP field # top
    ;

    field: IDENTIFIER;

    这个时候我们输入sort by abc,词法分析会将其分析为词:SORTBYIDENTIFIER,之后在语法分析阶段会命中语法SORT BY field #sort

antlr with java

这部分TODO吧,哈哈哈主要是我也没有搭过,但是简单看了下就是利用antlr自动生成的文件折腾折腾就可以啦

如下图所示

antlr自动生成文件

Author: suikammd
Link: https://www.suikammd.com/2020/10/10/antlr-lexer-parser/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.