这次实验的实验目的其实很明确——对源码进行语义分析,输出语义分析结果,并要求有适当的错误处理机制。可是指导书上实验目的要求自己分析,我的分析结果:本次实验要求自己定义上次实验的语法分析的文法的SDD,然后编写程序在上次语法分析的基础上完成语义分析,生成测试程序的中间代码(三地址码)。
刚开始可能课上听课程度不够,对这些概念不是很了解,导致做实验时有很大的障碍。
下面说说我的理解:
SDD是一个偏理论上的概念,龙书第二版这样说:
A syntax-directed definition ( SDD ) is a context-free grammar together with attributes and rules. Attributes are associated with grammar symbols and rules are associated with productions(“rules” is “semantic rules”,笔者加).
也就是说,为了理解语言的含义,我们要把语言符号和语言符号所代表的信息联系起来,我们要为文法的每个grammar symbol(s)附加一些属性。而附加到Production的语义规则则告知这些属性是怎么得来的以及文法符号属性之间的关系是怎样的。我们可以把SDD理解为对单纯的Grammar Symbols和Productions的扩展。SDD让文法的Grammar Symbols和Productions变得“活”了,变得有意义了。
SDT是一种技术(老师的讲义上把它理解为SDD的一种便于书写的形式)。这种翻译技术可以被应用到语义翻译过程中的类型检查和中间代码生成上,也可以被应用到一些某些具有特定任务的轻量级的语言中。可以这么理解,SDT是根据SDD所定义的那些规则和计算顺序对语言进行语义翻译的技术。通过SDT,我们理解了源语言到底要表达个什么意思,为后续的编译工作打下了基础。
中间代码生成,是一个生成一种独立于源语言也独立于目标语言(我们叫它中间代码)的代码过程,中间代码有很多形式,生成中间代码有很多好处,这里不再赘述。中间代码的生成工作要用到SDT技术。
总之,它们都没有什么明确的定义,意会这些就好。
此外,这次实验我最大的收获:LL(1)语法分析的过程本质上就是一个对语法分析树进行先根遍历的过程。
所以,先写一个简单的脚本实现非递归先根顺序遍历树,来模拟LL(1)的过程,以加深对其的理解。
下面上代码,其中非递归遍历树的过程就是LL(1)执行时遍历语法分析树的过程,读者可以画一下树的结构,并对着输出看一下遍历顺序,会对LL分析过程中分析栈的变化的认识有帮助。
树是这样被录入程序的(tree.txt):
Root:A
A -> B C
B -> D E F
C -> G H
E -> I J
J -> K L M
H -> N O
Tree = {}
Root = None
def read_tree(tree_file_path):
'''Read the Tree from the file, build the Tree'''
global Tree,Root
tree_file = open(tree_file_path,"r+")
raw_Root = tree_file.readline().strip().split(':')
if (raw_Root[0] == "Root"):
Root = raw_Root[1]
for eachLine in tree_file.readlines():
sub_tree = eachLine.strip().split(' -> ')
Tree[sub_tree[0]] = sub_tree[1].split(' ')
def travel_tree(Tree,Root):
'''Travel the Tree by root-priority order with the stack data structure instead of recursion'''
stack = []
stack.append(Root)
while len(stack) != 0 :
print 'stack:',stack
X = stack.pop()
print 'visit:',X # travel the Tree
if X not in Tree: # That means X is a leaf
continue
for item in list(reversed(Tree[X])):
stack.append(item)
def travel_tree_recursion(Tree,Root):
'''Travel the Tree recursively'''
print Root,
if Root not in Tree:
return
for X in Tree[Root]:
travel_tree_recursion(Tree,X)
if __name__ == '__main__':
print "Start reading the tree from file..."
read_tree('./tree.txt')
print "The tree is:"
print Tree,'\n'
print "Start traveling the tree with stack..."
travel_tree(Tree,Root)
print "\nStart traveling the tree recursively..."
travel_tree_recursion(Tree,Root)
Start reading the tree from file...
The tree is:
{'A': ['B', 'C'], 'C': ['G', 'H'], 'B': ['D', 'E', 'F'], 'E': ['I', 'J'], 'H': ['N', 'O'], 'J': ['K', 'L', 'M']}
Start traveling the tree with stack...
stack: ['A']
visit: A
stack: ['C', 'B']
visit: B
stack: ['C', 'F', 'E', 'D']
visit: D
stack: ['C', 'F', 'E']
visit: E
stack: ['C', 'F', 'J', 'I']
visit: I
stack: ['C', 'F', 'J']
visit: J
stack: ['C', 'F', 'M', 'L', 'K']
visit: K
stack: ['C', 'F', 'M', 'L']
visit: L
stack: ['C', 'F', 'M']
visit: M
stack: ['C', 'F']
visit: F
stack: ['C']
visit: C
stack: ['H', 'G']
visit: G
stack: ['H']
visit: H
stack: ['O', 'N']
visit: N
stack: ['O']
visit: O
Start traveling the tree recursively...
A B D E I J K L M F C G H N O
Stack
Object X = prodChars.peek(); // 从栈里弹出元素X
if (X instanceof MyCharacter) { // X是一个文法符号
// 在这里查找预测分析表,判断该使用哪个产生式
} else if (X instanceof HashMap, ?>) { // X是上一个文法符号的综合属性
// 在这里将综合属性暂存,作为参数传给下一个语义子程序
} else if (X instanceof String) { // X是一个语义子程序
// 在这里调用相应的语义子程序
}
根据前面演示脚本的输出,不难得出,按着LL的分析顺序:
1.文法符号的综合属性和由左兄弟节点传递的继承属性是沿着分析栈的栈顶向栈底传递的;
2.其他继承属性是由栈底向栈顶传递的。
为了达到传递两种不同属性的目的,我们需要设计不同的方法:
针对1,由于1的传递顺序是和弹栈顺序相同(栈顶->栈底),则直接在调用相应的语义子程序时顺水推舟传到栈底即可,即在语义子程序中加入诸如:
stack[top–n].put(“attribute_name”,”attribute_value”)
的操作即可完成栈顶方向属性向栈底传递的操作。
针对2,由于2的传递顺序和弹栈方向相反(栈底->栈顶),也就意味着,对于先入栈的元素,它的属性有可能传递给后入栈的元素,我们可以用全局变量的方式来实现这个顺序的属性传递。先入栈的元素把它的属性放到一个全局变量里面,后入栈的元素根据需要去全局变量里面查找属性即可。
还有一些其他问题,比如属性名重复等等,这个请读者具体问题具体分析自行解决。
做了这些储备之后,就基本可以着手实现了,首先是改造文法,思考并实现SDD,实际上这个过程还是有点难的,可以一步一步地先看看龙书上的样例SDD,再改造自己的文法:
实验二中的文法加入了语义子程序,实际上就是一个字符串,总控程序解析这个字符串判断调用哪个语义动作子程序,下面是我的文法产生式样例:
#
type_specifier -> CHAR | INT act2 | FLOAT act3 | CHAR*
#
declarator -> IDN act4 declarator'
declarator' -> [ CONST_INT ] act5 declarator' | $ act6
#
/**
* 根据当前文法分析句子,输出分析结果
*
* @param sentence
* 要分析的语句(Token表示)
* @param startChar
* 当前文法的起始符号
* @return 返回自顶向下推导序列
*/
@SuppressWarnings("unchecked")
public static ArrayList Analysis(ArrayList sentence,
String startChar) {
ArrayList productionSequences = new ArrayList();
Stack
此外,Java的Stack类只提供了对当前栈顶元素操作的接口,如果想对非栈顶元素进行操作,可以这样:
@SuppressWarnings("unchecked")
public static void act10(Stack
这次实验过程有点复杂,但是如果一步一步梳理下来收获还是不小的,虽然可能实验完成的不怎么样,但是也还是很有成就感的,欢迎讨论。
另附上Java中将标准输出重定向到文件流的方法:
try {
System.setOut(new PrintStream(new FileOutputStream(
FileAccessUtil.ROOT_DIR + "result.txt")));
} catch (FileNotFoundException e) {
e.printStackTrace();
}