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)

 

  • 7
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
pl0语法分析器,编译原理实验; 用C写的。 /语法分析过程中 /利用词法分析的结果进行分析 /严格按照PL0程序定义来编写 / /<程序> ::= <程序首部> <分程序>. /<程序首部> ::= PROGRAM <标识符>; /<分程序> ::= [<常量说明部分>][<变量说明部分>][<过程说明部分>]<语句部分> /<常量说明部分> ::= CONST <常量定义>{,<常量定义>}; /<常量定义> ::= <标识符>=<无符号整数> /<变量说明部分> ::= VAR <标识符>{,<标识符>}; /<过程说明部分> ::= <过程首部>;<分程序>;【原课件中没有最后的分号,经分析应该有分号】 /<过程首部> ::= PROCEDURE <标识符> /<语句部分> ::= <语句>|<复合语句> /<复合语句> ::= BEGIN <语句>{;<语句>} END【符合语句应该注意的是,END前距离END最近的那条语句一定没有分号,其他语句必须有分号】 /<语句>::= <赋值语句>|<条件语句>|<当型 循环语句>|<过程调用语句>|<读语句>|<写语句>|<复合语句> /<赋值语句> ::= <标识符>:=<表达式> /<读语句> ::= READ(<标识符>{,<标识符>}) /<写语句> ::= WRITE(<表达式>{,<表达式>}) /<过程调用语句> ::= CALL <标识符>【原课件中有分号,实际不应该有】 /<条件语句> ::= IF <条件> THEN <语句> /<当型循环语句> ::= WHILE <条件> DO <语句> /<因子> ::= <标识符>|<常量>|(<表达式>) /<项> ::= <因子>{<乘法运算符><因子>} /<乘法运算符> ::= *|/ /<表达式> ::= [+|-]<项>{<加法运算符><项>} /<加法运算符> ::= +|- /<条件> ::= <表达式><关系运算符><表达式>|ODD <表达式> /<关系运算符> ::= #|=|>|>=|<|<=
编译原理实验是通过对PL/0语言进行语义分析的实践。PL/0是一种简单的过程式语言,语法规则相对简单,适合用来学习编译原理中的语义分析语义分析是编译过程中的一个重要环节,其主要任务是对源程序中的语言结构进行分析,生成相应的中间代码,为后续的优化和代码生成做准备。在PL/0语义分析实验中,我们需要实现对PL/0语言的语义规则进行分析,包括语法树的构建、类型检查和作用域分析等内容。 首先,需要构建语法树,将源程序转换为一棵树状结构,便于后续的语义分析中间代码生成。在构建语法树的过程中,需要对语法规则进行递归下降分析,识别各种语法结构,并生成对应的语法树节点。 其次,需要进行类型检查,对源程序中的标识符和表达式进行类型推导,保证类型的一致性和正确性。这包括对变量的声明和使用、运算符的操作数类型等方面的检查。 最后,需要进行作用域分析,确定各个变量和过程的作用域,并进行变量的定义和引用的合法性检查。在PL/0语义分析实验中,我们需要实现对静态作用域的支持,确保程序在运行过程中能够正确地获取和修改变量的值。 通过实验的学习和实践,我们能够更深入地理解编译原理中的语义分析过程,掌握PL/0语言的语义规则和处理方法,为之后的编译器设计和实现打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值