如何用 ANTLR4 注入动作生成 symbol table
作者:互联网
什么是 semantic actions
当 Parser 处理输入的代码的时候不仅要判断是否语法和句法都正确,还可以执行一些有用的操作,这些操作就叫做 Semantic actions。其实也就是一段代码,一般嵌入在在语法文件的规则里面。那么当 parser 应用这个规则的时候就会执行你设置的这段代码。换个角度理解,semantic actions 其实就是“触发器”,触发条件就是 parser 应用了对应的规则。
今天这篇文章要探讨的就是一个关于 semantic actions 的应用——实现一个简单的 symbol table,用到的工具是 ANTLR4。
什么是 symbol table
编译器在处理我们的代码的时候会在内部维护一个 symbol table,用来存储程序里面所有关于变量的信息:变量名、数据类型、变量所属的作用域等。symbol table 可以是下面这种形式:
Symbol name | Type | Scope |
---|---|---|
bar | function, double | extern |
x | double | function parameter |
foo | function, double | global |
count | int | function parameter |
sum | double | block local |
i | int | for-loop statement |
如何在 ANTLR 的语法文件里面注入动作
ANTLR4 是一个强大的 parse generator,我们只要编写好语法文件,就能让它帮我们自动生成 Parser,生成的 Parser 可以支持多种目标语言,比如我自己用的是 Python,那么最后生成的 Parser 就是 Python 文件。
ANTLR4 也提供了方法让我们可以在语法文件里面插入动作,这些动作最后都会被 ANTLR4 注入到生成的 Parser 文件里面。因此,动作用什么语言写取决于你输出 Parser 的目标语言是什么
⚠️这里假定你对如何编写 ANTLR4 的语法文件(
*.g4
)有基本了解,不会进行赘述,本文只集中介绍如何在语法文件里面插入动作
动作注入的位置和方法
下面是简化的生成的 Parser 文件的代码模板。总体上来看,我们能注入的位置如下所示:
<header>
class xxxParser(Parser):
...
<member>
def rule(self):
...
<action>
下面我对每个位置的不同语法进行说明:
<header>
ANTLR4 本来就是用 Java 写的,所以如果你用的目标语言也是 Java 的话,这个会比较用得到。一般就是用来放 import
语句的。如果是 python 的话哪里都能 import
就比较没差。要往这个位置注入代码的格式如下(放在 *.g4
文件里面,下同)
@header{
everything here will go to <header>
}
<member>
这个放的就是类的成员,可以是字段也可以是方法。ANTLR4 支持往 Lexer 和 Parser 分别或者同时注入代码。要往这个位置注入代码的格式如下:
@members {
everything here will go to <member> in xxxLexer && xxxParser
}
@Lexer::members {
everything here will go to <member> in xxxLexer
}
@Parser::members {
everything here will go to <member> in xxxParser
}
⚠️在我使用的
antlr4-python3-runtime
中(4.10),还无法在插入类的字段进行注释
<action>
在 ANTLR4 中,动作是用花括号 {<specific-language-here>}
括起来的代码。正如前面提到的,看你最后输出的 Parser 想要是什么语言,你就用什么语言写动作。
动作一般会放在一条规则的某个 symbol 之后,意思是说在 parser 应用这条规则的时候执行到这里就执行相应的动作。这里的 symbol 可以是 terminal 也可以是 nonterminal。
我们可以用 $symbol.attr
的方式访问到对应的属性,有下面这几个:
$terminal.text # origin text
$terminal.type # an integer stands for type
$terminal.attributes
$terminal.line # the line number
$terminal.pos # the position of the first char in the line,0-based
$terminal.index
$terminal.channel # the channel of this terminal, won't discuss in this post
$terminal.int # return an interger if this terminal is an integer
$nonterminal.text # origin text
$nonterminal.start # 1st Token
$nonterminal.stop # last Token
$nonterminal.ctx # return context object
举个例子
⚠️下面都只会给出部分代码,完整的代码在我的 Github 项目
⚠️因为这个项目要求把 warning 输出在其他输出前面,所以我用
warning_list
和output_list
来存储,最后再一起输出
下面以普渡大学 2015 年的开设的编译器课程的项目作业为例,整个项目要实现一个 Micro 语言的编译器,关于 Micro 语言的语法,可以在这里看到。下面我会对步骤三的作业要求进行一个简单的介绍。
在这个作业中我们要构建一个 symbol table,并在相应的时刻输出相关的信息:
- 每当我们进入一个新的作用域之前(可以是函数,也可以是代码块),要进行相关的输出
- 如果遇到变量声明,就输出变量名和变量类型,有值的话也要输出。
- 如果声明的变量在外部作用域已经声明过的话要输出:
SHADOW WARNING <var_name>
,适用于有嵌套的作用域出现的情况 - 如果在当前作用域已经有同名的变量,就要输出:
DECLARATION ERROR <var_name>
。如果出现了这种情况,那么最后程序只输出这个信息
Symbol table <scope_name>
name <var_name> type <type_name>
name <var_name> type <type_name> value <string_value>
数据结构的选取及对应的方法
标签:作用域,symbol,list,ANTLR4,scope,table,self 来源: https://www.cnblogs.com/MartinLwx/p/16325297.html