PL\0编译原理实验(南航)三:语法分析、语义分析和中间代码生成

原理

实验采用的是自顶向下的语法分析

理论参考:

https://www.cnblogs.com/X-Jun/p/11040240.html

陈火旺那本编译原理教材

中间代码和翻译说明:

https://www.jianshu.com/p/de9132228b99

语法分析

数据结构

table_list:符号表,全局变量,里面记录定义的常量(CONSTANT)、变量(VARIABLE)、过程(PROCEDURE)

table:符号表中的符号,总共有6个属性,分别是名称(name),类型(kind),值(value),层差(level),地址(address),空间大小(size)

注:代码中只需要用到前5个属性,名称就是定义变量的名字,类型包括常量、变量、过程,值是表示常量直接就把值记录在符号表中,层差是引用变量所在的层减去定义该变量的层(为什么采用层差后面解释),地址参照后面的address

mid_code:存放生成的中间代码,全局变量

level:记录当前处理过程的层数,全局变量,主函数所在层为0,遇到过程就加一,过程结束就减一

address:对于变量来说是相对于基地址的偏移地址,对于过程名来说是调用过程下一句要执行代码的位置

注:代码中其实不需要定义address,这里只是为了说明address的作用

dx:全局变量,记录当前过程需要开辟的空间,同时也是提供变量的偏移地址,默认为3,因为每一次过程调用都会开辟SL、DL、RA这三个固定的参数,然后每有一个变量就加一

注:SL、DL、RA这三个参数的详细情况会在第四部分详细解释,这里只需要关注填的值是什么即可

分析过程

分析过程需要先了解定义的语法规则,因为语句太多,这里说几个典型的

prog

<prog> → program <id>;<block>

prog是函数的入口,首先获取一个单词,该单词必须是program,然后再获取一个单词,该单词必须是标识符,最后获取一个单词,该单词必须是分号,至此prog分析结束,下面进入block分析

block

<block> → [<condecl>][<vardecl>][<proc>]<body>

block是主函数以及子函数的入口,在函数体body之前可以定义常量、变量、过程,获取一个单词,如果该单词是const则进入常量表达式分析;如果该单词是var则进入变量表达式分析;如果该单词是procedure则进入过程表达式分析。

常量表达式中定义的常量要记录到符号表中,需要记录常量的名称、类型(CONSTANT),值、定义的层次,主函数是第0层,每嵌套一个子过程,层数+1

变量表达式定义的变量记录到符号表中,需要记录变量的名称、类型(VARIABLE)、定义的层次、地址,地址的意思是每一层的dx初始值是3,每定义一个变量就+1,地址记录的就是当前的dx,意义是当前层次的数据栈中该变量相对于基地址的偏移量,到时候查询该变量的时候根据基地址+该变量的地址就可以找到它了

过程表达式定义的名称记录到符号表中(后面括号里的参数为了简单省略了,因为对于学习没什么太大意义),需要记录过程的名称、类型(PROCEDURE)、定义的层次、地址。这里的地址跟变量不一样,该地址记录的是调用该过程需要跳转执行的指令的地址,过程会产生中间代码,产生的第一条中间代码的地址就是该地址( len(mid_code) )。需要注意的是procedure表达式里面定义了block,也就是说procedure可以嵌套定义,只需要注意dx的值即可

上面分析完block就结束了,下面进入body分析

body

bodybegin开始,end结束,中间都是以分号隔开的statement

statement

获取一个单词,如果该单词是表示符,则进行<id> := <exp>

如果该单词是if则进入if <lexp> then <statement>[else <statement>]

依次类推分析whilecall,如果都不是则进入body

注:这里就不分析read和write了,因为作为语法和中间代码生成和整体没什么联系,纯粹增加体力劳动(其实是我偷懒)

总结

语法分析就是根据获取语法规则,然后通过获取的单词判断走哪个推导,如果中间缺少分号或者begin或者不符合文法规则的都要报错,根据单词的line_num属性来提示哪一行报了什么错

语义分析

语义分析比较简单,就是判断语句中变量的类型是否正确

例如 赋值语句 a:=b ,查询符号表发现b是定义的过程名或者未定义,那么就要提示出错,赋值语句b的类型必须是常量或者变量

同理,条件表达式也是一样

中间代码生成

只有几个特定的地方会生成中间代码,其中最难的是地址回填,但理解了会发现没什么难度

这里建议如果脑子里感觉有些迷糊的话先去第五篇文章看看我给出的PL\0代码和生成的中间代码,理解了代码和中间代码的对应关系,下面就容易理解怎么去生成中间代码了

1.block

进入block先生成一条 <JMP,0,0>指令,这里就牵涉到地址回填了。为什么要先生成jmp指令呢?因为进入block模块后会先去执行常量(condecl)、变量(vardecl)、过程(proc)的翻译,其中proc和主函数基本一致,也会产生一些中间代码,但是中间代码解释器去执行的话不可能先去执行proc产生的中间代码,肯定要直接跳转到主函数地方去执行。第二个问题来了,JMP指令的地址怎么获得呢?这就涉及到地址回填,因为一开始并不知道proc会生成多少条中间代码,必须翻译完才能知道,所以思路是先插入一条JMP指令,然后记录该指令的位置,等常量(condecl)、变量(vardecl)、过程(proc)翻译结束后进入body翻译前把地址填上,要填的地址就是要插入新的中间代码的下标。实现的方式是先用cx1 = len(mid_code)记录当前JMP指令的位置用于回填,然后等proc翻译结束后用len(mid_code)就是JMP指令要跳转的地址,因为这个时候mid_code里面已经存放了不会执行的中间代码,后面就是主函数的代码了

block到最后会进入到body翻译,进入之前除了回填JMP的地址以外,还要产生一条INT指令,就是告诉数据栈我要占用多少空间,空间的大小是由dx控制的,dx默认值是3,然后每有一个变量就+1

body翻译完了还要在尾部加上一个<OPR,0,0>指令表示当前过程结束了,要返回

2.if语句

如果理解了block中jmp指令的过程,那么if和while就很好理解了,if语句先插入一条JPC指令,因为if语句是条件判断语句,所以应该用条件跳转指令JPC,JPC是条件不满足需要跳转到if语句结束的地方。因为if语句后面会生成一些其他中间代码,但是条件不满足的话就不能执行这些中间代码,需要跳过去,同理JPC指令的地址也应该等if语句生成完后再回填地址。如果有else语句,那么JPC的地址就应该填写else语句翻译的入口地址,然后再插入一条JMP指令,JMP指令的作用是如果不满足else语句的条件,需要直接跳转出去,不能执行else语句部分翻译的中间代码

3.while语句

while语句翻译稍微有些特殊,需要在while语句翻译的开始先记录当前要插入中间代码的地址,因为while循环如果满足条件的话需要跳转到开头继续执行,所以在while语句结束的时候要插入一条JMP指令,地址就是while语句开始的地址。

while语句的开头还是先插入一条JPC指令,原因和if语句一样,不满足条件直接跳出

4.call语句

遇到call调用需要插入一条CALL指令,这里需要注意的是指令第二个参数是层差,第三个是偏移地址,为什么采用层差和偏移地址第四篇文章讲解的比较详细

5.statement

statement主要是因为里面包含了if、while、call等语句的翻译

6.expression

expression部分涉及到的指令主要是取反操作和加减操作,主要是OPR指令的情况

7.term

term涉及到的主要是乘除运算,还是OPR指令的情况

8.factor

factor需要用到常量和变量,主要操作是把常量和变量的值放入栈顶,主要是LIT和LOD指令,LIT比较简单直接把常量值从符号表放入栈顶,LOD取变量还是根据层差和偏移量来获取

9.lexp

lexp主要是比较运算的翻译,比如奇偶判断odd、大于小于不等于等等,主要还是OPR指令的情况

实现代码

'''语法分析生成的符号表'''
table_list = []  # 符号表
mid_code = []  # 中间代码集合
level = 0  # 记录层数,每次遇到函数定义就加1
address = 0  # 每次符号表中登记变量和过程需要设置,作用是结合level知道每个变量和过程在数据栈中的位置或者函数跳转的位置
dx = 3  # 用来记录每一层次的开辟空间个数 默认是3 SL DL RA


# 记录符号表,
def record_table(name, kind, value=None, level=None, address=None, size=None):
    table = dict()
    table['name'] = name
    table['kind'] = kind
    table['value'] = value
    table['level'] = level
    table['address'] = address
    table['size'] = size
    table_list.append(table)


# 生成中间代码
def emit(f, l, a):
    operation = dict()
    operation['F'] = f
    operation['L'] = l
    operation['A'] = a
    mid_code.append(operation)


# 查询符号表 返回在符号表中的下标
def find_table(val):
    index = 0
    for var in table_list:
        if var['name'] == val:
            return index
        index += 1
    return -1


# <prog> → program <id>;<block>
def prog():
    token = get_token()
    if token['value'] == 'program':  # 第一个单词是program
        token = get_token()
        if token['attribute'] == 'identifier':  # 第二个是程序名称
            token = get_token()
            if token['value'] == ';':  # 第三个是分号 然后进入block阶段
                block()
            else:
                error(token['line_num'], '前面缺少;')
        else:
            error(token['line_num'], '语法错误,缺少程序名称')
    else:
        error(token['line_num'], '缺少program关键字')


# <block> → [<condecl>][<vardecl>][<proc>]<body>
# 考虑到因为;缺失导致获取的下一个单词不方便放回去,所以condecl vardecl proc在block层进行解析
def block():
    global dx
    global level
    global mid_code
    global token_index
    dx = 3  # 主函数以及定义的函数会调用block 默认入栈三个参数 SL DL RA
    # 进入block先写一条跳转语句 地址后面回填
    # 原因是假如block里面先进行函数定义,那么后续的指令不会执行,需要跳转到body部分,而body部分指令
    # 的地址需要计算完函数的指令才会计算出来,所以先在这里插入一条,同时用cx1标记一下jmp指令的位置后面回填
    cx1 = len(mid_code)  # cx1表示jmp指令需要回填在指令集中的位置
    emit('JMP', 0, 0)
    token = get_token()
    if token['value'] == 'const':  # 常量表达式 <condecl> → const <const>{,<const>};
        const()
        token = get_token()
        while token['value'] == ',':
            const()
            token = get_token()
        if token['value'] == ';':
            token = get_token()
        else:
            error(token['line_num'], '缺少;')
    if token['value'] == 'var':  # 变量表达式 <vardecl> → var <id>{,<id>};
        token = get_token()
        if token['attribute'] == 'identifier':  # 对于变量要插入到符号表中 同时dx要加一表示栈空间增长
            record_table(token['value'], 'VARIABLE', level=level, address=dx)
            dx += 1
            token = get_token()
        else:
            error(token['line_num'], 'var后面需要跟标识符')
        while token['value'] == ',':
            token = get_token()
            if token['attribute'] == 'identifier':
                record_table(token['value'], 'VARIABLE', level=level, address=dx)
                dx += 1
                token = get_token()
                continue
        if token['value'] == ';':
            token = get_token()
        else:
            error(token['line_num'], '缺少;')
    # 这里用while循环表示函数定义可以嵌套
    while token['value'] == 'procedure':  # 函数定义 <proc> → procedure <id>([<id>{,<id>}]);<block>{;<proc>}
        token = get_token()
        if token['attribute'] == 'identifier':
            record_table(token['value'], 'PROCEDURE', level=level, address=len(mid_code))
            token = get_token()
        else:
            error(token['line_num'], '函数名必须是标识符')
        if token['value'] != ';':  # 这里先不考虑函数带参数的情况 无非就是多写几个变量
            error(token['line_num'], '缺少;')
            token_index -= 1
        # 下面进入block定义 进入block之前需要更新嵌套层数level 同时记录当前栈的情况便于恢复
        level += 1  # 层级+1
        cur_dx = dx  # 记录当前层的变量个数
        block()
        level -= 1  # 结束后要恢复
        dx = cur_dx  # 恢复当前栈的变量数量
        token = get_token()
        if token['value'] == ';':  # 如果是分号 继续进行proc
            token = get_token()
        else:
            break
    token_index -= 1  # 由于不再函数嵌套 需要回退一个单词
    # 进入body之前先回填block开头的jmp指令
    ins = dict()
    ins['F'] = 'JMP'
    ins['L'] = 0
    ins['A'] = len(mid_code)  # 跳转的地址就是body里指令的开头
    mid_code[cx1] = ins
    emit('INT', 0, dx)  # 进入当前函数的body部分需要给定义的变量和SL DL RA开辟栈空间
    body()  # 进入body
    emit('OPR', 0, 0)  # 过程调用结束后,返回调用点并退栈


# <const> → <id>:=<integer>
def const():
    token = get_token()
    variable = token['value']
    if token['attribute'] == 'identifier':  # 这里有变量 需要记录在符号表中
        token = get_token()
        if token['value'] == ':=':
            token = get_token()
            if token['attribute'] == 'number':
                record_table(variable, 'CONSTANT', value=token['value'], level=level)
            else:
                error(token['line_num'], ':=后面需要跟整数')
        else:
            error(token['line_num'], '缺少:=')
    else:
        error(token['line_num'], '缺少标识符')


# <body> → begin <statement>{;<statement>}end
def body():
    global token_index
    token = get_token()
    if token['value'] != 'begin':
        error(token['line_num'], '缺少begin')
        token_index -= 1
    statement()
    token = get_token()
    while token['value'] == ';':  # 循环statement
        statement()
        token = get_token()
    if token['value'] != 'end':
        error(token['line_num'], '缺少end')
        token_index -= 1


# <statement> → <id> := <exp>
#                |if <lexp> then <statement>[else <statement>]
#                |while <lexp> do <statement>
#                |call <id>([<exp>{,<exp>}])
#                |<body>
#                |read (<id>{,<id>})
#                |write (<exp>{,<exp>})
def statement():
    global token_index
    global level
    token = get_token()
    if token['value'] == 'end':  # 这一步是因为如果最后有人多写了一个; 会继续进入statement,但实际上会退出
        error(token['line_num'], ';是多余的')
        token_index -= 1
        return
    if token['attribute'] == 'identifier':  # <id> := <exp>
        index = find_table(token['value'])
        if index == -1:
            error(token['line_num'], token['value'] + '未定义')
        elif table_list[index]['kind'] != 'VARIABLE':
            error(token['line_num'], table_list[index]['name'] + '不是一个变量')
        token = get_token()
        if token['value'] != ':=':
            error(token['line_num'], '缺少:=')
            token_index -= 1  # 需要回退一个
        expression()
        if index != -1:  # 合法变量产生一个sto指令 从数据栈中取数据赋值给变量 关于为什么使用层差和地址会在解析指令地方解释
            emit('STO', level - table_list[index]['level'], table_list[index]['address'])
    elif token['value'] == 'if':  # if <lexp> then <statement>[else <statement>]
        lexp()
        token = get_token()
        if token['value'] != 'then':
            error(token['line_num'], '缺少关键字then')
            token_index -= 1
        cx2 = len(mid_code)  # cx2表示jpc指令需要回填在指令集中的位置
        emit('JPC', 0, 0)  # if语句先做jpc跳转到else的地方 后面回填
        statement()
        # 这里回填if语句不满足执行else的地址
        ins = dict()
        ins['F'] = 'JPC'
        ins['L'] = 0
        ins['A'] = len(mid_code)  # 跳转的地址就是if语句不满足的地方
        mid_code[cx2] = ins
        token = get_token()
        if token['value'] == 'else':  # 判断是否还有
            cx1 = len(mid_code)
            emit('JMP', 0, 0)
            statement()
            # 这里回填if语句结束的地址
            ins = dict()
            ins['F'] = 'JMP'
            ins['L'] = 0
            ins['A'] = len(mid_code)  # 跳转的地址就是if语句不满足的地方
            mid_code[cx1] = ins
        else:
            token_index -= 1  # 没有则回退
        # 这里回填if语句结束的地址
    elif token['value'] == 'while':  # while <lexp> do <statement>
        jmp_addr = len(mid_code)  # 这里保存while循环开始的语句 因为循环如果条件满足需要继续执行
        lexp()
        token = get_token()
        # 地址指令回头填写
        if token['value'] != 'do':
            error(token['line_num'], '缺少do关键字')
            token_index -= 1
        cx2 = len(mid_code)  # cx2表示jpc指令需要回填在指令集中的位置
        emit('JPC', 0, 0)  # if语句先做jpc跳转到else的地方 后面回填
        statement()
        # 插入一条jmp指令继续执行循环
        emit('JMP', 0, jmp_addr)
        # 回填jpc指令
        ins = dict()
        ins['F'] = 'JPC'
        ins['L'] = 0
        ins['A'] = len(mid_code)  # 跳转的地址就是while语句结束的地方
        mid_code[cx2] = ins
    elif token['value'] == 'call':  # call <id>([<exp>{,<exp>}])
        token = get_token()
        if token['attribute'] != 'identifier':
            error(token['line_num'], '函数名必须是标识符')
        else:
            index = find_table(token['value'])
            if index == -1:
                error(token['line_num'], token['value'] + '未定义')
            elif table_list[index]['kind'] == 'PROCEDURE':
                emit('CAL', level - table_list[index]['level'], table_list[index]['address'])
            else:
                error(token['line_num'], token['value'] + '不是函数名')
    else:  # body
        token_index -= 1
        body()


#  <exp> → [+|-]<term>{<aop><term>}
def expression():
    global token_index
    token = get_token()
    if token['value'] == '+' or token['value'] == '-':
        term()
        if token['value'] == '-':  # -号需要取反操作
            emit('OPR', 0, 1)
    else:
        token_index -= 1  # 回退
        term()
    token = get_token()
    while token['value'] == '+' or token['value'] == '-':
        term()
        if token['value'] == '+':
            emit('OPR', 0, 2)
        elif token['value'] == '-':
            emit('OPR', 0, 3)
        token = get_token()
    token_index -= 1


#  <term> → <factor>{<mop><factor>}
def term():
    global token_index
    factor()
    token = get_token()  # 处理乘除
    while token['value'] == '*' or token['value'] == '/':
        factor()
        if token['value'] == '*':
            emit('OPR', 0, 4)
        elif token['value'] == '/':
            emit('OPR', 0, 5)
        token = get_token()
    token_index -= 1  # 需要回退一个单词


#  <id>|<integer>|(<exp>)
def factor():
    global token_index
    token = get_token()
    if token['attribute'] == 'identifier':  # 标识符要查符号表
        index = find_table(token['value'])
        if index == -1:  # 未找到 报错
            error(token['line_num'], token['value'] + '未定义')
        else:
            if table_list[index]['kind'] == 'CONSTANT':  # 常量
                emit('LIT', 0, table_list[index]['value'])  # 把常量放入栈顶
            elif table_list[index]['kind'] == 'VARIABLE':
                emit('LOD', level - table_list[index]['level'], table_list[index]['address'])  # 把变量放入栈顶
            elif table_list[index]['kind'] == 'PROCEDURE':
                error(token['line_num'], table_list[index]['name'] + '为过程名, 出错')
    elif token['attribute'] == 'number':  # 遇到数字
        emit('LIT', 0, token['value'])
    elif token['attribute'] == '(':  # 遇到左括号要进入表达式
        expression()
        token = get_token()
        if token['attribute'] != ')':  # 没有右括号报错
            error(token['line_num'], '缺少右括号')
            token_index -= 1  # 要回退一个


# <lexp> → <exp> <lop> <exp>|odd <exp>
def lexp():
    global token_index
    token = get_token()
    if token['value'] == 'odd':
        expression()
        emit('OPR', 0, 6)  # 奇偶判断
    else:
        token_index -= 1  # 要先回退才能进入表达式
        expression()
        token = get_token()
        if token['value'] != '=' and token['value'] != '<>' and token['value'] != '<' and token['value'] != '<=' \
                and token['value'] != '>' and token['value'] != '>=':
            error(token['line_num'], '缺少比较运算符')
            token_index -= 1
        expression()
        if token['value'] == '=':
            emit('OPR', 0, 8)
        elif token['value'] == '<>':
            emit('OPR', 0, 9)
        elif token['value'] == '<':
            emit('OPR', 0, 10)
        elif token['value'] == '>=':
            emit('OPR', 0, 11)
        elif token['value'] == '>':
            emit('OPR', 0, 12)
        elif token['value'] == '<=':
            emit('OPR', 0, 13)

猜你喜欢

转载自blog.csdn.net/wh_computers/article/details/105746706