Project Demo

← 返回项目列表

LittleC 编译器前端可视化工具

基于 Qt 开发的桌面端编译器前端工具,面向简化版 C 语言(LittleC)实现完整编译核心流程,涵盖词法分析、递归下降语法分析、语义检查与 8086 汇编代码生成, 支持编译过程可视化展示与源码错误定位。通过 Qt 多面板交互界面,实时查看 Token 序列、AST 语法树、符号表与生成的汇编代码。

C++ Qt 编译原理 递归下降分析 AST 8086 Assembly

词法分析器(Lexer)

词法分析是编译的第一阶段,负责将源代码的字符流转换为等长二元组形式的 Token 序列。 分析器从左到右逐字符扫描源程序,根据构词规则识别出有意义的最小语法单元,同时记录行号用于后续错误定位。


① Token 分类与编码方案

LittleC 中所有合法词素被分为 5 大类,每个 Token 用 (类型编码, 词素字符串) 的等长二元组表示:

分类编码范围包含内容示例
关键字1 ~ 11int, bool, if, then, else, while, do, read, write, true, false(3, "if")
标识符20字母/下划线开头,≤8 字符,不区分大小写(20, "count")
整常数21纯数字串,≤8 位(21, "42")
运算符30 ~ 44+ - * / > >= < <= == != || && ! = :=(35, ">=")
分隔符50 ~ 55{ } ( ) ; ,(54, ";")

② 核心扫描流程

词法分析器采用单遍从左到右扫描策略,维护当前位置指针 pos 和行号计数器 line,通过字符分类跳转到不同的识别分支:

词法分析器核心流程
Pseudocode
函数 tokenize(源代码字符串 src) → Token列表:
    pos = 0, line = 1, tokens = []


    while pos < src.length:
        跳过所有空白字符(遇 '\n' 则 line++)


        c = src[pos]    // 取当前字符


        // ── 跳过注释 ──
        if c == '/' 且 next == '/':     // 单行注释
            跳到行末, continue
        if c == '/' 且 next == '*':     // 多行注释
            向前扫描直到找到 "*/"(未找到则报 E04 错误), continue


        // ── 识别整常数 ──
        if c 是数字 [0-9]:
            贪心读取连续数字 → word
            if word.length > 8: 报 E03 错误
            tokens.添加( (21, word, line) )
            continue


        // ── 识别标识符 / 关键字 ──
        if c 是字母 或 '_':
            贪心读取 [a-zA-Z0-9_] → word
            if word.length > 8: 报 E02 错误
            word = 统一转小写      // 不区分大小写
            if word 在 关键字哈希表 中:
                tokens.添加( (关键字编码, word, line) )
            else:
                tokens.添加( (20, word, line) )    // 标识符
            continue


        // ── 识别双字符运算符(优先匹配长串) ──
        if c+next ∈ { ">=", "<=", "==", "!=", "||", "&&", ":=" }:
            tokens.添加( (对应编码, c+next, line) )
            pos += 2, continue


        // ── 识别单字符运算符 & 分隔符 ──
        pos++
        switch c:
            '+','-','*','/','>','<','!','=': 添加对应运算符Token
            '{','}','(',')',';',',':         添加对应分隔符Token
            default: 报 E01 错误(非法字符)


    return tokens
③ 关键设计细节
  • 最长匹配原则:双字符运算符(如 >=)优先于单字符(如 > + =),通过向前看一个字符(peek)实现。
  • 关键字与标识符的区分:先按标识符规则贪心读取整个单词,再到关键字哈希表中查找;命中则是关键字,否则是标识符。
  • 大小写不敏感:所有标识符和关键字统一转小写后再比对,INTIntint 均识别为关键字。
  • 注释处理:单行注释 // 跳到行末;多行注释 /* */ 跨行扫描,未闭合则报错。
  • 模块化设计:词法分析封装为 Lexer 类,对外仅暴露 tokenize() 接口,后续语法分析模块直接调用,无需重复读文件。

语法分析器(Parser)— 递归下降 + AST 构建

语法分析是编译的第二阶段,接收词法分析器输出的 Token 序列,按照文法产生式从左到右扫描一遍, 检查是否符合 LittleC 的语法规则。如果合法,则构建一棵抽象语法树(AST);如果不合法,则报告语法错误并终止。


① 递归下降方法

本项目采用递归下降分析(Recursive Descent Parsing):为文法中的每个非终结符编写一个同名的解析函数, 函数之间通过相互调用形成递归关系,天然对应文法的产生式结构。例如:

文法 → 函数的对应关系
Grammar
// LittleC 文法(简化)
PROG  → { DECLS STMTS }
DECLS → (DECL)*
DECL  → int NAMES ; | bool NAMES ;
STMTS → (STMT)+
STMT  → id = EXPR ;            // 整型赋值
      | id := BOOL ;           // 布尔赋值
      | if id then STMT [else STMT]
      | while id do STMT
      | { STMTS }              // 复合语句
      | read id ; | write id ;
EXPR  → TERM ((+|-) TERM)*     // 算术表达式
TERM  → NEGA ((*|/) NEGA)*
NEGA  → -FACTOR | FACTOR
FACTOR→ (EXPR) | id | number
BOOL  → JOIN (|| JOIN)*        // 布尔表达式
JOIN  → NOT (&& NOT)*
NOT   → !REL | REL | true | false
REL   → EXPR ROP EXPR


// ↕ 每条产生式对应一个 C++ 函数 ↕


ASTPtr parseProgram()    // ← 对应 PROG
ASTPtr parseDeclList()   // ← 对应 DECLS
ASTPtr parseStmt()       // ← 对应 STMT
ASTPtr parseExpr()       // ← 对应 EXPR
ASTPtr parseBoolExpr()   // ← 对应 BOOL
...                      // 以此类推

② 算术表达式的优先级处理

通过函数调用层级天然实现运算符优先级:调用越深的函数绑定越紧。以 a + b * c 为例:

表达式解析 — 优先级递归
Pseudocode
// parseExpr 处理 + -(最低优先级)
函数 parseExpr():
    left = parseTerm()                  // 先解析更高优先级
    while 当前Token是 '+' 或 '-':
        op = 当前运算符, 前进
        right = parseTerm()
        left = 创建 BinOp(op, left, right)  // 构建二元运算节点
    return left


// parseTerm 处理 * /(更高优先级)
函数 parseTerm():
    left = parseNega()
    while 当前Token是 '*' 或 '/':
        op = 当前运算符, 前进
        right = parseNega()
        left = 创建 BinOp(op, left, right)
    return left


// parseFactor 处理叶子节点(最高优先级)
函数 parseFactor():
    if 当前是 '(':  前进, expr = parseExpr(), 期望 ')', return expr
    if 当前是 数字:  return 创建 Number(值)
    if 当前是 标识符: return 创建 Ident(名)

解析 a + b * c 时,parseExpr 先调用 parseTermparseTerm 内部识别到 b * c, 构建为一棵子树后返回给 parseExpr 作为 + 的右孩子。函数调用深度 = 优先级高低,乘法在更深层被处理,自然先于加法绑定。


③ AST 节点结构与输出格式

每个 AST 节点包含:节点类型(Program / VarDecl / BinOp / Ident 等)、value(运算符 / 变量名 / 数值)、行号子节点列表。 最终以树形缩进文本输出到文件,使用 ├──└── 连接线展示父子关系:

AST 输出示例:c = a + b * 2
Output
Program  [line 1]
├── DeclList  [line 1]
│   └── VarDecl (int)  [line 2]
│       ├── Name: a  [line 2]
│       ├── Name: b  [line 2]
│       └── Name: c  [line 2]
└── StmtList  [line 3]
    └── IntAssign -> c  [line 5]
        └── BinOp (+)  [line 5]          ← 加法是根
            ├── Ident: a  [line 5]       ← 左操作数
            └── BinOp (*)  [line 5]      ← 乘法是子树(优先级更高)
                ├── Ident: b  [line 5]
                └── Number: 2  [line 5]
④ 关键设计细节
  • 向前看(Lookahead):通过 peek() 查看下一个 Token 来决定当前语句类型。例如 id 后面是 = 则进入整型赋值,是 := 则进入布尔赋值。
  • if-else 悬空问题:采用贪心策略,else 总是与最近的 if 匹配(在递归下降中自然成立)。
  • 错误恢复:遇到非法 Token 时记录错误并跳过,尽量继续解析后续语句,一次报告尽可能多的语法错误。
  • 布尔表达式与算术表达式分离= 右侧调用 parseExpr():= 右侧调用 parseBoolExpr(),从语法层面区分两种类型系统。

语义分析 + 8086 汇编代码生成

语义分析是编译的第三阶段,在语法正确的前提下进一步检查程序的类型安全性和作用域规则。 如果全部检查通过,则遍历 AST 生成目标平台的 8086 汇编代码。


① 符号表构建与类型检查

语义分析器从 AST 的 DeclList 子树中提取所有变量声明,构建符号表(Symbol Table):一个从变量名到类型(int/bool)+声明行号的映射。 随后递归遍历 StmtList 子树,对每条语句和每个表达式进行类型推导和检查。

语义分析核心流程
Pseudocode
函数 analyze(AST根节点):
    // ── 第1步:遍历 DeclList,构建符号表 ──
    for each VarDecl(type, names) in DeclList:
        for each name in names:
            if name 已在符号表中:
                报错 E06: 变量重复声明
            else:
                符号表[name] = { type, 声明行号 }


    // ── 第2步:遍历 StmtList,递归检查每条语句 ──
    for each stmt in StmtList:
        checkStmt(stmt)


函数 checkStmt(stmt):
    case IntAssign(name, expr):
        if name 不在符号表:    报错 E07 (未声明)
        if 符号表[name] ≠ int: 报错 E08 (= 左侧非int)
        if infer(expr) ≠ int:  报错 E08 (= 右侧非int)


    case BoolAssign(name, boolExpr):
        if name 不在符号表:    报错 E07
        if 符号表[name] ≠ bool:报错 E09 (:= 左侧非bool)
        if infer(boolExpr) ≠ bool: 报错 E09


    case IfStmt(condVar, thenBranch, elseBranch):
        if 符号表[condVar] ≠ bool: 报错 E13 (条件非bool)
        checkStmt(thenBranch)
        checkStmt(elseBranch)   // 如有 else


    // while / read / write 类似 ...


// ── 类型推导函数:自底向上推导表达式类型 ──
函数 infer(expr) → "int" | "bool" | "error":
    case Number:     return "int"
    case BoolLit:    return "bool"
    case Ident(name):return 符号表[name].type
    case BinOp("+"/"-"/"*"/"/", L, R):
        检查 infer(L)==int 且 infer(R)==int → return "int"
    case BinOp(">"/">="/"<"/"<="/"=="/"!=", L, R):
        检查 infer(L)==int 且 infer(R)==int → return "bool"
    case BinOp("||"/"&&", L, R):
        检查 infer(L)==bool 且 infer(R)==bool → return "bool"
    case UnaryOp("-", child):
        检查 infer(child)==int → return "int"
    case UnaryOp("!", child):
        检查 infer(child)==bool → return "bool"

② 完整错误类型表(15 种)
编号阶段错误类型触发条件示例
E01词法非法字符@#$
E02词法标识符过长(>8)longVarName9
E03词法整常数过长(>8)123456789
E04词法多行注释未闭合/* ... 缺少 */
E05语法语法结构不匹配缺少 {;then
E06语义变量重复声明int a; int a;
E07语义变量未声明就使用未写 int x; 就用 x=1;
E08语义整型赋值类型错误= 左边是 bool 变量
E09语义布尔赋值类型错误:= 左边是 int 变量
E10语义算术运算操作数非 intflag + 1(flag 为 bool)
E11语义逻辑运算操作数非 boola || b(a 为 int)
E12语义关系运算操作数非 int比较两个 bool 变量
E13语义if/while 条件非 boolif a then ...(a 为 int)
E14语义read 目标非 intread flag;(bool)
E15语义write 目标非 intwrite flag;(bool)

③ 8086 汇编代码生成

当三个阶段均无错误时,代码生成器再次递归遍历 AST,将每个节点翻译为对应的 MASM 格式 8086 汇编指令。 核心策略:表达式求值统一使用 AX 寄存器 + 栈

表达式代码生成策略
Pseudocode
// 表达式求值:结果统一存入 AX
函数 genExpr(node):
    case Number(val):
        emit "MOV AX, val"


    case Ident(name):
        emit "MOV AX, name"        // 从内存变量读到 AX


    case BinOp(op, left, right):
        genExpr(left)               // 左子树 → AX
        emit "PUSH AX"             // 保存到栈
        genExpr(right)              // 右子树 → AX
        emit "POP BX"              // BX = 左值, AX = 右值
        emit "XCHG AX, BX"         // AX = 左值, BX = 右值
        switch op:
            '+': emit "ADD AX, BX"
            '-': emit "SUB AX, BX"
            '*': emit "IMUL BX"     // 结果在 AX
            '/': emit "CWD"         // 符号扩展
                 emit "IDIV BX"     // 商在 AX


    case 关系运算(op, left, right):
        // 同上求出 AX=left, BX=right
        emit "CMP AX, BX"
        emit "Jcc L_true"          // 条件跳转(JG/JGE/JL/JLE/JE/JNE)
        emit "MOV AX, 0"           // false
        emit "JMP L_end"
        emit "L_true: MOV AX, 1"   // true
        emit "L_end:"

c = a + b 为例,生成的 8086 汇编及其执行过程:

生成结果:c = a + b
x86asm
    ; c = a + b
    MOV AX, a        ; AX = a           (左操作数)
    PUSH AX          ; 栈: [a]          (保存左值)
    MOV AX, b        ; AX = b           (右操作数)
    POP BX           ; BX = a, AX = b   (弹出左值)
    XCHG AX, BX      ; AX = a, BX = b   (交换)
    ADD AX, BX        ; AX = a + b       (运算)
    MOV c, AX         ; c = AX           (写回内存)
④ 控制流翻译
  • if-else:生成条件变量的 CMP AX, 0 + JE 跳转到 else 标签,then 分支结束后 JMP 到 end 标签。
  • while:循环开头放置标签 L_start,检查条件变量,为 0 则 JE L_end 跳出,循环体结束后 JMP L_start 回到开头。
  • read/write:生成 CALL ReadInt / CALL WriteInt,子程序通过 DOS 中断 INT 21H 实现键盘输入和屏幕输出。

运行演示

测试源程序(source.txt)
source.txt
LittleC
//program 1: add two numbers.
{
    int a, b, c  ;
    a = 1;
    b = 2;
    c = a + b ;
}

① 词法分析运行结果 词法分析运行截图 词法分析运行截图

词法分析器输出 Token 二元组序列(lex_out.txt)


② 语法分析运行结果 语法分析运行截图

语法分析器输出 AST 树形结构(parse_out.txt)


③ 语义分析 + 代码生成运行结果 语义分析运行截图

语义检查通过后输出 8086 汇编代码(asm_out.asm)


④ QT窗口程序 QT窗口程序截图 QT窗口程序截图

QT窗口程序界面