本项目基于L25文法,利用C++实现了一个编译器,包含词法分析、语法分析、出错处理、代码生成和解释程序。L25文法是简单的编程语言文法,本项目在其基础定义上扩展了字符串以及指针的实现。具体见下面给出的扩展后的L25文法的定义:
no references
referenced by:
referenced by:
referenced by:
stmt ::= declare_stmt
| if_stmt
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
referenced by:
letter ::= 'a'
| 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'i'
| 'j'
| 'k'
| 'l'
| 'm'
| 'n'
| 'o'
| 'p'
| 'q'
| 'r'
| 's'
| 't'
| 'u'
| 'v'
| 'w'
| 'x'
| 'y'
| 'z'
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'W'
| 'X'
| 'Y'
| 'Z'
referenced by:
digit ::= '0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
referenced by:
referenced by:
referenced by:
str_char ::= '除了\和"之外的任何 ASCII 字符'
referenced by:
::= '\' ( '"' | '\' | 'n' | 't' )
referenced by:
关于L25文法有一些注意事项如下:
- func 体里最后必须有 return;
- main或普通语句块里不能使用 return;
- 每个stmt_list 至少有一条语句。
示例代码(求最小公倍数):
program LCMpro{
func gcd(a, b) {
let result;
if (b == 0) {
result = a;
} else {
result = gcd(b, a - (a/b)*b);
};
return result;
}
func lcm(a, b) {
let g = gcd(a, b);
let result = (a * b) / g;
return result;
}
main {
let a;
let b;
output("Number1\n");
input(a);
output("Number2\n");
input(b);
if (a > 0) {
if(b>0){
let result = lcm(a, b);
output("LCM:\n");
output(result);
};
} else {
output("Integer only!");
};
}
}
关于保留字的说明:
根据上面L25文法的EBNF和铁路图描述,L25语言包含11个保留字,分别是program, func, main, return, let, if, else, while, input, output, str;支持支持字符串连接运算以及字符串和整型之间的运算,例如str a = "hello world";let b = 3;str c = a + b;str d = a*b; str e = a + d;
关于字符串的内存管理的说明:
对于字符串的内存管理,此编译器采用C++的数组虚拟一个数据栈,数据栈的大小为5000,其中0到999为常规数据栈,1000到1999为字符串常量的字面量值,2000到4999为运行时字符串栈,保存运行时程序产生的字符串字面量的值。字符串变量保存在常规数据栈中,存储的值实际是字符串所在的地址。基于这种内存管理机制,可以实现指针的支持。
关于指针的说明:
此编译器扩展了指针,指针声明需要在声明符号后面加 @,例如let@ p = 1;
,同时允许取地址值赋值给指针,取地址符号为 &,例如p = &a;
。允许指针的加减运算,例如p = p + 1;
。此时p
指向的是下一个元素所在的地址,通过@p
,可以取出对应的值。
代码结构:
该编译器仿照java版本的P/L0编译器,按照词法分析、语法分析、代码生成和执行的编译流程,主要由以下几个核心模块组成:
1. 编译器整合核心 ( Compiler.hpp , Compiler.cpp ) 编译器的中央协调器,用于实现编译器的实例,集成并管理其他各个模块。在构造函数中初始化词法分析器 ( Scanner )、符号表 ( Table )、语法分析器 ( Parser ) 和解释器 ( Interpreter )并调用其中编译时需要用到的函数或方法。
2. 词法分析器 ( Scanner.hpp , Scanner.cpp ) 读取L25源程序文本,将其分解成一系列的词法单元(tokens/symbols)。
3. 符号表 ( table.hpp , table.cpp ) 存储和管理在源程序中声明的标识符(如变量、函数、字符串等)的信息。
4. 语法分析器 ( Parser.hpp , Parser.cpp ) 根据L25语言的语法规则,分析词法分析器提供的词法单元序列,检查语法正确性,并在此过程中生成中间代码(P-code)。采用递归下降的解析策略。包含多个 parse<NonTerminal>()
方法,对应语法规则中的非终结符(例如 parseProgram , parseStatementList , parseExpression
)。使用 SymSet.hpp 中定义的 SymSet 类来辅助进行语法分析,例如用于错误恢复或预测分析中的FIRST/FOLLOW集。
5. 解释器 ( Interpreter.hpp , Interpreter.cpp )
执行由语法分析器生成的P-code指令。定义P-code指令的操作码(如 LIT , OPR , LOD , STO , CAL , JMP , JPC , WRT , RED , HLT 等),后文会对指令做详细说明。
6. 辅助模块
Symbol.h : 定义了 Symbol 枚举类,列出了L25语言所有的词法单元类型。
SymSet.hpp : 提供 SymSet 类,用于表示符号的集合,主要被语法分析器用于语法制导翻译和错误恢复。
编译器使用的P-code指令及其含义:
指令 | 格式 | 含义 |
LIT | LIT 0 a | 将常量值 a 推入栈顶,用于加载整数常量和字符串地址。 |
LOD | LOD l a | 将变量的值推入栈顶。该变量位于层差 l,地址为 a。用于读取变量的值。 |
STO | STO l a | 将栈顶的值存入变量中并出栈。该变量存储于层差 l,地址为 a。 |
CAL | CAL l a | 调用函数。a 是函数入口地址,l 是层差。 |
INT | INT 0 a | 在栈上为函数分配/释放空间。a 为正数时分配,为负数时释放。 |
JMP | JMP 0 a | 无条件跳转到地址 a。 |
JPC | JPC 0 a | 条件跳转。当栈顶值为 0 (false) 时,跳转到地址 a 并出栈。 |
LDA | LDA l a | 加载地址。将变量的地址推入栈顶,用于 &v 操作。 |
LDI | LDI 0 0 | 间接加载。将栈顶值作为地址,取出该地址处的值放入栈顶,用于 @p 取值。 |
STI | STI 0 0 | 间接存储。从栈顶弹出一个值和地址,将值存入该地址,用于 @p = … 赋值。 |
RED | RED 0 0 | 读取输入 ( input )。从输入流读取一个值,并将其推入栈顶。 |
WRT | WRT 0 0 | 写输出 ( output )。弹出栈顶值并将其输出到输出流。 |
HLT | HLT 0 0 | 停机。程序正常执行结束。 |
OPR (操作) 指令:
OPR 指令用于执行算术、逻辑和特定的字符串运算。
a值 | 含义 |
0 | 函数返回 ( return )。 |
1 | 对栈顶值取负 (一元负号 – )。 |
2 | 对栈顶的两个元素进行加法 ( + )并将结果存入栈顶。 |
3 | 弹出栈顶两个元素,用前一个被压入的元素减去后一个被压入的元素,并将结果存入栈顶。 ( – )。 |
4 | 对栈顶的两个元素进行乘法 ( * )并存入栈顶。 |
5 | 前一个元素被压入栈顶的元素除以( / )后一个被压入的元素,将结果存入栈顶。包含除零检查。 |
8 | 等于 ( == ),判断两个栈顶元素是否相等。 |
9 | 不等于 ( != ),判断两个栈顶元素是否不等。 |
10 | 小于 ( < ),判断前一个入栈的元素是否小于后一个入栈的元素。 |
11 | 大于等于 ( >= ),判断前一个入栈的元素是否大于等于后一个入栈的元素。 |
12 | 大于 ( > ),判断前一个入栈的元素是否大于后一个入栈的元素。 |
13 | 小于等于 ( <= ),判断前一个入栈的元素是否小于等于后一个入栈的元素。 |
14 | 带换行的输出,与 WRT 功能相同,但在输出后会自动添加一个换行符。(保留原P/L0指令设计,未使用) |
15 | 输出换行,输出一个换行符。(保留原P/L0指令设计,未使用) |
16 | 读取:与 RED 功能完全相同,从标准输入读取一个整数并压栈。(保留原P/L0指令设计,未使用) |
17 | 字符串重复 ( str * num ),将字符串重复 num 次并返回结果字符串。 |
18 | 字符串连接 ( str1 + str2 ),连接两个字符串。 |
19 | 字符串与数字连接 ( str + num ),连接字符串和数字。 |
编译器错误号及含义说明:
错误号 | 错误含义 | 触发条件 | 出现位置 |
1 | 嵌套层级过深 | 函数或块的嵌套层级超过最大限制(3) | parseProgram() |
4 | 期望标识符 | 在需要标识符的位置遇到其他符号 | 程序名、函数名、参数、变量声明等 |
5 | 期望分号 | 语句结束时缺少分号 | 语句列表、函数返回语句 |
11 | 未声明的标识符 | 使用了未在符号表中声明的标识符 | 变量引用、函数调用等 |
12 | 不能对该标识符赋值 | 试图对常量或函数赋值 | 赋值语句、输入语句 |
13 | 期望 ‘}’ | 代码块未正确关闭 | 程序体、函数体、if/while语句 |
16 | 期望关系操作符 | 条件表达式中缺少比较运算符 | parseCondition() |
17 | 期望 ‘{‘ | 需要左大括号开始代码块 | 各种代码块开始处 |
18 | 期望 ‘program’ 关键字 | 程序开始时没有program关键字 | parse() 函数开始 |
19 | 期望 ‘main’ | 函数定义后没有找到main函数 | parseProgram() |
20 | 期望 ‘return’ | 函数体结束时没有return语句 | parseFunctionDef() |
21 | 期望 ‘(‘ | 函数定义/调用或条件语句缺少左括号 | 函数、if/while、输入输出语句 |
22 | 期望 ‘)’ | 缺少右括号匹配左括号 | 函数、条件语句、表达式等 |
23 | 非法语句 | 遇到不支持的语句类型 | parseStatement() 、 parseStatementList() |
24 | 非法的因子 | 表达式因子不合法 | parseFactor() |
25 | 不是函数 | 试图调用非函数类型的标识符 | 函数调用解析 |
26 | 期望字符串 | 字符串表达式中遇到非字符串类型 | parseStringExpression() |
27 | 期望 ‘=’ | 赋值语句中缺少赋值操作符 | 变量赋值、字符串赋值 |
28 | 不能把地址赋给非指针变量 | 将取地址结果赋给普通变量 | 变量/字符串声明初始化 |
29 | 试图解引用非指针变量 | 对非指针类型使用解引用操作符@ | parseAssignment() |
出错处理机制:
错误计数: 使用静态变量 errorCount
统计错误数量
错误限制: 超过 maxerr
限制时终止编译
错误定位: 输出格式为 **空格^错误号 ,精确定位错误位置
错误恢复: 遇到错误首先会报告错误,然后进入一个循环,不断地调用 nextSym()
来丢弃(跳过) 当前及后续的符号。这个跳过过程会一直持续,直到找到一个安全的“同步符号”为止。