如果一件事值得做,那就值得做过头儿.
语义分析简介
看看下面这个例子,引入我们的定义:
x := x + y;
Paser是怎么知道x和y之前有没有声明过呢?
并不能,所以我们需要语义分析这个过程,来回答好这个问题。所以基本上,语义分析就是这样一个工作,让我们检查一个程序是不是合理,根据语言的定义,它的意义是什么。
什么样的程序是合理的?这涉及到语言的定义和语言的需求。比如Pascal语言,或者更具体的说Pascal编译器,有一些具体的要求。不满足这些要求,编译器fpc就会出错。有时候出错的程序,句法看起来都挺对的。
这些要求包括:
- 变量先声明后使用。
- 变量计算的时候,类型必须正确。
- 不能重复声明。
- 过程调用必须对应存在一个过程。加入调用一个整数,那是万万不可的。
- 过程调用的形参实参要匹配。
如果我们有足够的上下文,比如说AST的IR,让我们可以随意查看,我们也能随意访问符号表里面不同的程序实体(变量、过程、函数)的话,就很容易实现这些检查了。
语义检查的位置在下图所示:
Symbols 和symbol table
语义检查并不神秘,它只是在解析和AST过程之后增加了一个新的步骤。只是它掌握的上下文信息更多。涉及到了符号和符号表的操作。
今天我们看下面两个检查*:
- 变量声明后使用
- 不可以重复声明
请注意:我们今天讨论的是静态检查,不是动态检查。后者的例子是除0操作,只能在interpreter过程中进行。静态检查在interpreter执行前就完成了。
下面是一个句法正确,语义错误的例子。一个变量声明,两个变量引用。
program Main;
var x : integer;
begin
x := y;
end.
为什么句法检查检查不出来呢?
>>> from spi import Lexer, Parser
>>> text = """
program Main;
var x : integer;
begin
x := y;
end.
"""
>>>
>>> lexer = Lexer(text)
>>> parser = Parser(lexer)
>>> tree = parser.parse()
>>>
$ python genastdot.py semanticerror01.pas > semanticerror01.dot
$ dot -Tpng -o ast.png semanticerror01.dot
你看,语法上是正确的,但是我们根本看不到y的类型是什么。这是为什么我们需要声明。确实声明只是一个语义错误,不是句法错误,AST都生成了嘛。
正确的写法应该是下面的源码:
program Main;
var x, y : integer;
begin
x := x + y;
end.
对于正确的代码,我们又是怎么检查语义是正确的呢?
- 过一遍所有的变量声明
- 每次遇到一个声明,收集变量声明的必要信息
- 把收集的信息搜藏在宝箱里,以供将来有人拿变量的名字作为key来读取
- 每次遇到一个引用,比如x:=x+y,去查找宝箱,看看能不能找到相关的变量信息。找不到就说明没有声明,报错即可。
在写代码实现之前,我们要问自己几个问题:
- 什么信息要收集起来?
- 信息要放在哪儿?怎么放?
- 遍历所有的变量要怎么做呢?
第一个问题:
什么信息是必须的呢?
- Name
- Category (variable, type, procedure等等)
- Type (类型检查用的)
Symbols 要包含以上的三类变量相关的信息 (name, category, and type) 。Symbol是什么呢?是关于variable, subroutine, 或者 built-in type的标识符。
下图显示了两个变量,xy的程序:
Symbol的实现是Symbol 类:
class Symbol(object):
def __init__(self, name, type=None):
self.name = name
self.type = type
参数name是必须的,type是可选的。
那category呢? 我们直接把他作为Symbol类的一个域:
class Symbol(object):
def __init__(self, name, type=None):
self.name = name
self.type = type
self.category = category
当然更直观的方法是创建层次化的类,用类名来区分不同的category。不过目前的做法也可以吧。。。
就剩下 built-in 类型了。 再看一眼我们的Pascal程序代码:
program Main;
var x, y : integer;
begin
x := x + y;
end.
x和y被生命成了整形。什么是整数类型呢?整数类型是另一种symbol,一个内部类型的symbol。内部的symbol不需要程序员去操心声明的问题,解释器负责自动声明它,保证程序员随时取用。
我们用了一个新的类:BuiltinTypeSymbol 来表示内部类型symbol:
class BuiltinTypeSymbol(Symbol):
def __init__(self, name):
super(BuiltinTypeSymbol, self).__init__(name)
def __str__(self):
return self.name
def __repr__(self):
return "<{class_name}(name='{name}')>".format(
class_name=self.__class__.__name__,
name=self.name,
)
BuiltinTypeSymbol 从 Symbol 类继承而来,构造函数只有一个类型的名字, 比如 integer 或者 real. 'builtin type' category 放在了类名中,从父类继承过来的 type 参数自动设置为了None。
注意:就是为了兼容built-in 类型,scope类的构造函数里面,type参数才被设计成了可选的。
$ python
>>> from spi import BuiltinTypeSymbol
>>> int_type = BuiltinTypeSymbol('integer')
>>> int_type
<BuiltinTypeSymbol(name='integer')>
>>>
>>> real_type = BuiltinTypeSymbol('real')
>>> real_type
<BuiltinTypeSymbol(name='real')>
以上就是内部类型的测试。
怎么实现普通变量的Symbol呢?
class VarSymbol(Symbol):
def __init__(self, name, type):
super(VarSymbol, self).__init__(name, type)
def __str__(self):
return "<{class_name}(name='{name}', type='{type}')>".format(
class_name=self.__class__.__name__,
name=self.name,
type=self.type,
)
__repr__ = __str__
类名是 VarSymbol 清楚地表明了类的作用是表示变量symbol的。type参数属于BuiltinTypeSymbol 类。
Let’s go back to the interactive Python shell to see how we can manually construct instances of our variable symbols now that we know how to construct BuiltinTypeSymbol class instances:
我们试试怎么构造变量symbol的实例。
$ python
>>> from spi import BuiltinTypeSymbol, VarSymbol
>>> int_type = BuiltinTypeSymbol('integer')
>>> real_type = BuiltinTypeSymbol('real')
>>>
>>> var_x_symbol = VarSymbol('x', int_type)
>>> var_x_symbol
<VarSymbol(name='x', type='integer')>
>>>
>>> var_y_symbol = VarSymbol('y', real_type)
>>> var_y_symbol
<VarSymbol(name='y', type='real')>
>>>
至今为止的symbol关系图:
第二个问题:在哪儿、怎么存储信息?
现在我们已经有了表示所有变量声明的symbol了,那么我们需要把symbol存放在哪里呢?
答案你可能猜到了:symbol table。你可以把它想象成一个词典,里面存储着一条条的key-value对儿,key就是symbol的名字,value是symbol类或者子类的实例。SymbolTable 类会增加 insert 方法,去写入 _symbols 词典,symbol名字是key,symbol实例是value。
class SymbolTable(object):
def __init__(self):
self._symbols = OrderedDict()
def __str__(self):
symtab_header = 'Symbol table contents'
lines = ['\n', symtab_header, '_' * len(symtab_header)]
lines.extend(
('%7s: %r' % (key, value))
for key, value in self._symbols.items()
)
lines.append('\n')
s = '\n'.join(lines)
return s
__repr__ = __str__
def insert(self, symbol):
print('Insert: %s' % symbol.name)
self._symbols[symbol.name] = symbol
我们把下面Pascal程序的symbol table读取出来看看它长什么样子。因为我们还不知道怎么搜索symbol table,所以我们的程序没有包含变量的引用,只有变量的声明。
program SymTab1;
var x, y : integer;
begin
end.
下载 symtab01.py, 里面包含了新的 SymbolTable 类。 我们运行上面这个Pascal程序:
$ python symtab01.py
Insert: INTEGER
Insert: x
Insert: y
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>
下面让我们试着操作一下symbol table:
$ python
>>> from symtab01 import SymbolTable, BuiltinTypeSymbol, VarSymbol
>>> symtab = SymbolTable()
>>> int_type = BuiltinTypeSymbol('INTEGER')
>>> # now let's store the built-in type symbol in the symbol table
...
>>> symtab.insert(int_type)
Insert: INTEGER
>>>
>>> symtab
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
>>> var_x_symbol = VarSymbol('x', int_type)
>>> symtab.insert(var_x_symbol)
Insert: x
>>> symtab
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
x: <VarSymbol(name='x', type='INTEGER')>
>>> var_y_symbol = VarSymbol('y', int_type)
>>> symtab.insert(var_y_symbol)
Insert: y
>>> symtab
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>
>>>
之前的问题终于有了答案:
-
要收集什么信息?
Name, category 和 type. 我们用symbols去存储它们。
-
信息存哪?怎么存?
symbol table,用它的 insert方法。
最后一个问题, ‘怎么遍历变量声明的节点呢?’
这个反而是个最简单的问题。我们已经有了AST,我们只需要创建一个新的AST visitor类,来遍历AST树形结构,遇到VarDecl AST节点的时候,做一些需要的操作就可以了。这个visitor类就是SemanticAnalyzer。
program SymTab2;
var x, y : integer;
begin
end.
想解析上面的程序,我们不需要实现所有的visit_方法,只需要一个子集就可以了。下面是代码的大概:
class SemanticAnalyzer(NodeVisitor):
def __init__(self):
self.symtab = SymbolTable()
def visit_Block(self, node):
for declaration in node.declarations:
self.visit(declaration)
self.visit(node.compound_statement)
def visit_Program(self, node):
self.visit(node.block)
def visit_Compound(self, node):
for child in node.children:
self.visit(child)
def visit_NoOp(self, node):
pass
def visit_VarDecl(self, node):
# Actions go here
pass
最后一个方法的具体实现如下:
def visit_VarDecl(self, node):
# For now, manually create a symbol for the INTEGER built-in type
# and insert the type symbol in the symbol table.
type_symbol = BuiltinTypeSymbol('INTEGER')
self.symtab.insert(type_symbol)
# We have all the information we need to create a variable symbol.
# Create the symbol and insert it into the symbol table.
var_name = node.var_node.value
var_symbol = VarSymbol(var_name, type_symbol)
self.symtab.insert(var_symbol)
完整的源码在 symtab02.py :
$ python symtab02.py
Insert: INTEGER
Insert: x
Insert: INTEGER
Insert: y
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>
最后的最后,是类型检查。这个好像也很容易。我们只需要增加个查找方法查symbol table,找不到变量就报错。
SymbolTable 类就需要增加 lookup 方法,lookup方法的学名叫名字解析,也就是从变量的引用映射到变量的声明。
def lookup(self, name):
print('Lookup: %s' % name)
symbol = self._symbols.get(name)
# 'symbol' is either an instance of the Symbol class or None
return symbol
SymbolTable 类里面还有一个小改动,就是默认初始化内部类型。
class SymbolTable(object):
def __init__(self):
self._symbols = OrderedDict()
self._init_builtins()
def _init_builtins(self):
self.insert(BuiltinTypeSymbol('INTEGER'))
self.insert(BuiltinTypeSymbol('REAL'))
def __str__(self):
symtab_header = 'Symbol table contents'
lines = ['\n', symtab_header, '_' * len(symtab_header)]
lines.extend(
('%7s: %r' % (key, value))
for key, value in self._symbols.items()
)
lines.append('\n')
s = '\n'.join(lines)
return s
__repr__ = __str__
def insert(self, symbol):
print('Insert: %s' % symbol.name)
self._symbols[symbol.name] = symbol
def lookup(self, name):
print('Lookup: %s' % name)
symbol = self._symbols.get(name)
# 'symbol' is either an instance of the Symbol class or None
return symbol
回过头来,我们就要优化SemanticAnalyzer中的visit_VarDecl 方法了,我们要把手动增加的两行内部变量的创造过程给优化掉。 还要解决掉双重输出 Insert: INTEGER 的问题。
visit_VarDecl 方法修改前是这样的:
def visit_VarDecl(self, node):
# For now, manually create a symbol for the INTEGER built-in type
# and insert the type symbol in the symbol table.
type_symbol = BuiltinTypeSymbol('INTEGER')
self.symtab.insert(type_symbol)
# We have all the information we need to create a variable symbol.
# Create the symbol and insert it into the symbol table.
var_name = node.var_node.value
var_symbol = VarSymbol(var_name, type_symbol)
self.symtab.insert(var_symbol)
修改后是这样的:
def visit_VarDecl(self, node):
type_name = node.type_node.value
type_symbol = self.symtab.lookup(type_name)
# We have all the information we need to create a variable symbol.
# Create the symbol and insert it into the symbol table.
var_name = node.var_node.value
var_symbol = VarSymbol(var_name, type_symbol)
self.symtab.insert(var_symbol)
下载 symtab03.py 最新的代码在执行一次:
$ python symtab03.py
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: x
Lookup: INTEGER
Insert: y
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
REAL: <BuiltinTypeSymbol(name='REAL')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>
可以看到INTEGER内部类型被lookup了两次,一次是x的声明,一次是y的声明。
对x := x + y; 这种赋值语句来讲,有三个变量的引用,x、x和y。
program SymTab4;
var x, y : integer;
begin
x := x + y;
end.
我们已经有了 lookup 方法了,只需要增加一个 visit_Var方法来每次都lookup变量引用。
def visit_Var(self, node):
var_name = node.value
var_symbol = self.symtab.lookup(var_name)
因为赋值语句的右边是个计算,所以我们需要把SemanticAnalyzer 增加两个方法,visit_Assign 和 visit_BinOp,以便可以针对每一个Var调用visit_var方法。
def visit_Assign(self, node):
# right-hand side
self.visit(node.right)
# left-hand side
self.visit(node.left)
def visit_BinOp(self, node):
self.visit(node.left)
self.visit(node.right)
最后版本的代码是 symtab04.py:
$ python symtab04.py
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: x
Lookup: INTEGER
Insert: y
Lookup: x
Lookup: y
Lookup: x
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
REAL: <BuiltinTypeSymbol(name='REAL')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>
迄今为止,我们实现了所有逻辑,做到了变量使用前先校验是否已经声明过了。
语义错误
这个好像简单的不需要多说了,symbol table里面找不到变量的名字就报错。
def visit_Var(self, node):
var_name = node.value
var_symbol = self.symtab.lookup(var_name)
if var_symbol is None:
raise Exception(
"Error: Symbol(identifier) not found '%s'" % var_name
)
最后版本的代码是 symtab05.py:
program SymTab5;
var x : integer;
begin
x := y;
end.
$ python symtab05.py
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: x
Lookup: y
Error: Symbol(identifier) not found 'y'
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
REAL: <BuiltinTypeSymbol(name='REAL')>
x: <VarSymbol(name='x', type='INTEGER')>
那怎么检查变量有没有重复声明呢?
program SymTab6;
var x, y : integer;
var y : real;
begin
x := x + y;
end.
我们需要修改 visit_VarDecl 方法,在insert之前先读一下,看看变量是否已经存在了。
def visit_VarDecl(self, node):
type_name = node.type_node.value
type_symbol = self.symtab.lookup(type_name)
# We have all the information we need to create a variable symbol.
# Create the symbol and insert it into the symbol table.
var_name = node.var_node.value
var_symbol = VarSymbol(var_name, type_symbol)
# Signal an error if the table alrady has a symbol
# with the same name
if self.symtab.lookup(var_name) is not None:
raise Exception(
"Error: Duplicate identifier '%s' found" % var_name
)
self.symtab.insert(var_symbol)
最后的代码是 symtab06.py :
$ python symtab06.py
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Lookup: x
Insert: x
Lookup: INTEGER
Lookup: y
Insert: y
Lookup: REAL
Lookup: y
Error: Duplicate identifier 'y' found
Symbol table contents
_____________________
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
REAL: <BuiltinTypeSymbol(name='REAL')>
x: <VarSymbol(name='x', type='INTEGER')>
y: <VarSymbol(name='y', type='INTEGER')>