拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SNL编译器实现:从词法分析到x86-64汇编的完整五阶段实践

SNL编译器实现:从词法分析到x86-64汇编的完整五阶段实践

简介:本资源是一套完整的SNL语言编译器源码实现,面向编译原理课程学习者、高校计算机专业学生及编译器开发初学者,用于深入理解词法分析、语法分析与语义分析三大核心编译阶段。代码基于C/C++开发,结构清晰,包含多个关键模块的独立实现(如LL1语法分析、递归下降解析、词法扫描器等),支持完整编译流程验证与调试。压缩包共231个文件,约77.55MB,主体为C++工程文件(sln、vcxproj、cpp、h)、可执行程序(exe)、调试符号(pdb、ilk)及构建中间产物(tlog、ipch、obj),便于读者理解Visual Studio项目组织方式与编译构建机制。已有1177人学习下载,配套代码经实测可用,作者提供QQ技术支持(3329097042),适合开展课程设计、实验复现或编译器原理拓展实践。

1. SNL语言编译器源码:不是玩具项目,是编译原理课设里最硬核的“通关凭证”

如果你正在啃《编译原理》清华大学出版社第三版,翻到第二章词法分析、第三章语法分析、第四章语义分析——然后突然被老师甩来一句:“用SNL语言写个能跑通的编译器,下周五交可执行二进制”,你大概率会盯着“SNL”两个字母发懵:这不是教材里那个只在习题和附录里闪现、连标准文档都找不到的“教学型语言”吗?没有IDE、没有调试器、连报错信息都像天书?但现实是:全国山科大、北航、南大等高校编译原理实验课的真实选题中,“实现SNL编译器”常年稳居难度TOP3,原因很实在——它不依赖外部运行时(如JVM或Python解释器),要求你亲手把词法→语法→语义→中间代码→目标代码这条链路全部焊死;它语法极简(仅支持整数、if/while、赋值、基本算术),却足以暴露所有经典陷阱:LL(1)文法冲突怎么消?符号表怎么支持嵌套作用域?四元式怎么生成又不漏跳转?寄存器分配怎么在无优化前提下避免覆盖?这不是写个Python脚本调API,而是用C/C++或Java,在没有编译器开发框架(如ANTLR)兜底的前提下,从零搭起一个能吃自己吐出的汇编并正确执行的黑匣子。适合谁?刚学完龙书前五章、手痒想验证理论、且愿意为一行if语句的语法树多debug两小时的人。它不教你怎么用MSVC或MinGW,但它会让你彻底明白:为什么gcc -S出来的汇编里,%rax不能乱动,jmp指令后面必须跟标号,而main函数的栈帧布局,根本不是教材图示里画得那么温柔。


2. 从零启动:SNL语言规范、编译流程拆解与技术选型依据

SNL(Simple Numerical Language)不是工业级语言,而是为教学设计的“最小可行编译目标”。它的存在意义,是让学习者聚焦于编译器核心阶段,而非被C++模板或Java泛型带偏。要动手,先立住三根柱子:语言规范、编译阶段划分、技术栈选择。这三者不厘清,后面写500行代码可能全白费。

2.1 SNL语言核心语法:比教材习题更“毒”的真实约束

SNL语法极其精简,但每个细节都暗藏编译器设计考点。以下为清华大学第三版教材附录及主流高校实验手册确认的最小完备集(注意:非网络流传的“简化版”,此为实际课设验收标准):

类别语法示例关键约束编译器设计映射点
数据类型int x, y;仅支持int,无浮点、无数组、无指针符号表只需记录类型ID,无需尺寸/对齐计算
控制结构if (x > 0) { y = 1; } else { y = 0; }
while (x < 10) { x = x + 1; }
if-else必须配对,while无do-while;条件表达式仅支持<,>,==,!=,<=,>=语法分析需处理左递归消除(if嵌套)、while循环体必须为复合语句({});语义分析要检查条件表达式返回bool(隐式转换)
表达式x = y * 2 + z / 3;运算符优先级:* / %>+ -> 关系运算符;无括号提升优先级语法分析需构造符合优先级的AST;中间代码生成需按优先级顺序展开四元式
函数int main() { ... return 0; }仅允许一个main函数,无参数、无重载、无用户自定义函数符号表全局作用域唯一入口;目标代码生成时,main即程序起点,无需链接其他模块

提示:很多学生栽在“SNL不支持函数调用”上——这意味着你的编译器不需要实现函数调用约定(calling convention),不用管栈帧压参、call/ret指令配对、返回值传递寄存器(如%rax)。这是刻意为之的教学减法,不是遗漏。

2.2 编译流程五阶段:为什么必须严格分阶段,且不能跳过语义分析

SNL编译器虽小,但必须完整走通经典五阶段。跳过任一环,都会导致后期无法调试或输出错误代码。各阶段输入/输出及核心任务如下:

阶段输入输出关键产出物不可省略理由
词法分析(Lexer).snl源文件文本Token流(INT,ID(x),OP(+),SEMI(;)`等)Token类/结构体,含类型、值、行号若此处未做关键字识别(如if/while未标记为保留字),后续语法分析会将它们当作普通标识符,导致if语句直接语法错误
语法分析(Parser)Token流抽象语法树(AST)IfNode,WhileNode,AssignNode,BinaryOpNode等节点类SNL文法存在左递归(如Expr → Expr + Term),若不改写为右递归或使用递归下降+回溯,会导致无限递归栈溢出
语义分析(Semantic Analyzer)AST带类型信息的AST + 符号表SymbolTable(支持嵌套作用域)、TypeCheckResult检查if条件是否为布尔表达式(x + y非法)、变量是否声明即用(int a; b = a;应报错)、main是否唯一且返回int。此阶段缺失是课设挂科最高发区
中间代码生成(IR Generator)语义检查后的AST四元式序列(op, arg1, arg2, result)Quadruple类,如(+, x, 1, t1),(=, t1, _, y)SNL目标平台通常是x86-64或简化RISC(如MIPS模拟器),四元式是向汇编映射的最稳中间层;跳过它直接生成汇编,会导致跳转地址无法反向填充(if的false分支jmp目标未知)
目标代码生成(Code Generator)四元式序列汇编代码(.s)或可执行二进制asm_output.txt或a.out必须处理寄存器分配(SNL变量少,可用%rax,%rbx,%rcx轮换)、栈管理(main函数需建栈帧)、跳转标签生成(L1:,L2:)

2.3 技术栈选择:为什么C++比Java更主流,而Python只适合验证逻辑

高校课设验收环境多为Linux(Ubuntu 20.04/22.04)或Windows+WSL,编译器输出需为本地可执行文件。技术选型不是炫技,而是看调试效率和系统级控制力:

  • 首选C++(g++ 11+):
    理由:直接操作内存(符号表用std::map<std::string, Symbol>)、精准控制寄存器分配(内联汇编或手动映射变量到%rax等)、生成.s汇编后可用as+ld一键链接。血泪经验:用C++写SymbolTable::enter()时,能用emplace()避免拷贝,而Java的HashMap.put()在频繁插入时GC抖动明显,影响调试节奏。

  • 次选Java(JDK 11+):
    适用场景:若实验要求输出JVM字节码(.class),则Java自身生态有ASM库可直接生成。但SNL课设99%要求本地机器码,此时Java需通过ProcessBuilder调用nasm,管道传入汇编再链接,失败时错误堆栈跨三层(Java→Shell→Assembler),排查成本翻倍。

  • Python(CPython 3.8+)仅作辅助:
    定位:写lexer.py快速验证正则匹配(re.findall(r'int|if|while|[a-zA-Z_]\w*|\d+|[+\-*/<>=!]+|{|}|;|\(|\)', code)),或用ast模块解析Python版SNL模拟器。切记:不要用Python写主编译器——它无法生成原生可执行文件,pyinstaller打包的a.exe本质是Python解释器+字节码,不符合“编译器”定义,验收必被拒。

注意:网上搜到的“SNL编译器Python实现”,多为教学演示代码,缺少符号表作用域管理、四元式跳转填充等关键环节,直接交作业会被老师一眼识破。


3. 词法与语法分析实战:手写递归下降解析器的最小可运行骨架

SNL语法简单,但手写解析器是理解编译原理的“成人礼”。本节给出C++版最小可运行骨架,包含词法分析器(Lexer)和递归下降语法分析器(Parser),代码可直接编译运行,输入test.snl输出AST结构。重点不在炫技,而在展示如何让LL(1)文法适配递归下降——这才是教材第二章没讲透的实操心法。

3.1 词法分析器:用状态机思想处理SNL关键字与标识符

SNL关键字(int,if,while,else,return)必须与用户标识符(如x,myVar)严格区分。常见错误是用std::regex一把梭,但正则引擎在长文件中性能差,且无法精确返回列号(column number),而调试时定位error at line 5, column 12比line 5重要十倍。

// lexer.h #include <string> #include <vector> #include <cctype> enum TokenType { TOK_INT, TOK_IF, TOK_WHILE, TOK_ELSE, TOK_RETURN, TOK_ID, TOK_NUM, TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH, TOK_LT, TOK_GT, TOK_EQ, TOK_NE, TOK_LE, TOK_GE, TOK_ASSIGN, TOK_SEMI, TOK_LPAREN, TOK_RPAREN, TOK_LBRACE, TOK_RBRACE, TOK_EOF }; struct Token { TokenType type; std::string value; int line; int col; Token(TokenType t, const std::string& v, int l, int c) : type(t), value(v), line(l), col(c) {} }; class Lexer { private: std::string input; size_t pos; int line; int col; // 跳过空白和注释(SNL通常无注释,但预留) void skipWhitespace() { while (pos < input.size() && std::isspace(input[pos])) { if (input[pos] == '\n') { line++; col = 0; } else col++; pos++; } } // 识别关键字或标识符 Token identifierOrKeyword() { size_t start = pos; while (pos < input.size() && (std::isalnum(input[pos]) || input[pos] == '_')) { pos++; } std::string id = input.substr(start, pos - start); // 关键字表(按教材附录) if (id == "int") return Token(TOK_INT, id, line, start - line_start_col(line)); if (id == "if") return Token(TOK_IF, id, line, start - line_start_col(line)); if (id == "while") return Token(TOK_WHILE, id, line, start - line_start_col(line)); if (id == "else") return Token(TOK_ELSE, id, line, start - line_start_col(line)); if (id == "return") return Token(TOK_RETURN, id, line, start - line_start_col(line)); return Token(TOK_ID, id, line, start - line_start_col(line)); // 用户标识符 } // 行首列号计算(辅助函数) int line_start_col(int l) { // 实际实现需遍历input统计每行长度,此处简化 return 0; } public: Lexer(const std::string& src) : input(src), pos(0), line(1), col(0) {} Token nextToken() { skipWhitespace(); if (pos >= input.size()) return Token(TOK_EOF, "", line, col); char c = input[pos]; switch (c) { case '+': pos++; return Token(TOK_PLUS, "+", line, col++); case '-': pos++; return Token(TOK_MINUS, "-", line, col++); case '*': pos++; return Token(TOK_STAR, "*", line, col++); case '/': pos++; return Token(TOK_SLASH, "/", line, col++); case '<': pos++; if (pos < input.size() && input[pos] == '=') { pos++; return Token(TOK_LE, "<=", line, col); } else if (pos < input.size() && input[pos] == '>') { pos++; return Token(TOK_NE, "<>", line, col); // SNL常用<>表示!= } return Token(TOK_LT, "<", line, col++); case '>': pos++; if (pos < input.size() && input[pos] == '=') { pos++; return Token(TOK_GE, ">=", line, col); } return Token(TOK_GT, ">", line, col++); case '=': pos++; if (pos < input.size() && input[pos] == '=') { pos++; return Token(TOK_EQ, "==", line, col); } return Token(TOK_ASSIGN, "=", line, col++); case ';': pos++; return Token(TOK_SEMI, ";", line, col++); case '(': pos++; return Token(TOK_LPAREN, "(", line, col++); case ')': pos++; return Token(TOK_RPAREN, ")", line, col++); case '{': pos++; return Token(TOK_LBRACE, "{", line, col++); case '}': pos++; return Token(TOK_RBRACE, "}", line, col++); default: if (std::isdigit(c)) { size_t start = pos; while (pos < input.size() && std::isdigit(input[pos])) pos++; return Token(TOK_NUM, input.substr(start, pos - start), line, col); } else if (std::isalpha(c) || c == '_') { return identifierOrKeyword(); } else { // 未识别字符,报错 pos++; return Token(TOK_EOF, "UNKNOWN_CHAR", line, col++); } } } };

逻辑说明与参数说明:

  • skipWhitespace()中line和col的更新是调试命脉:当input[pos] == '\n'时,line++且col重置为0,否则col++。这保证了每个Token的line/col精准对应源码位置。
  • identifierOrKeyword()采用字符串精确匹配而非哈希表,因SNL关键字仅5个,O(1)查找足够;若扩展,可改用std::unordered_map。
  • 关系运算符<>(SNL中!=的写法)需特殊处理:读到<后预读下一个字符,若为>则组合成TOK_NE,否则退回并返回TOK_LT。这是典型的前瞻(lookahead)操作,也是词法分析器必须支持的能力。

3.2 语法分析器:消除左递归后的递归下降实现

SNL原始文法(教材习题常见)为:

Program → DeclList MainFunc DeclList → int ID ; DeclList | ε MainFunc → int main ( ) { StmtList return NUM ; } StmtList → Stmt StmtList | ε Stmt → IfStmt | WhileStmt | AssignStmt | { StmtList } IfStmt → if ( Expr ) Stmt [ else Stmt ] WhileStmt → while ( Expr ) Stmt AssignStmt → ID = Expr ; Expr → Term { AddOp Term } Term → Factor { MulOp Factor } Factor → ID | NUM | ( Expr ) AddOp → + | - MulOp → * | /

问题在于Expr → Expr AddOp Term是直接左递归,递归下降无法处理。必须改写为右递归(教材第三版P87明确要求):

Expr → Term ExprTail ExprTail → AddOp Term ExprTail | ε Term → Factor TermTail TermTail → MulOp Factor TermTail | ε

此改写确保每个产生式右部不以自身开头,递归下降可安全实现。

// parser.h #include "lexer.h" #include <memory> #include <stack> // AST节点基类 struct ASTNode { virtual ~ASTNode() = default; virtual void print(int indent = 0) const = 0; }; // 变量声明节点 struct VarDeclNode : public ASTNode { std::string name; VarDeclNode(const std::string& n) : name(n) {} void print(int indent = 0) const override { std::cout << std::string(indent, ' ') << "VarDecl: " << name << "\n"; } }; // 赋值语句节点 struct AssignNode : public ASTNode { std::string var; std::unique_ptr<ASTNode> expr; AssignNode(const std::string& v, std::unique_ptr<ASTNode> e) : var(v), expr(std::move(e)) {} void print(int indent = 0) const override { std::cout << std::string(indent, ' ') << "Assign: " << var << " = "; if (expr) expr->print(indent + 2); } }; // 二元运算节点(+,-,*,/) struct BinaryOpNode : public ASTNode { std::string op; std::unique_ptr<ASTNode> left; std::unique_ptr<ASTNode> right; BinaryOpNode(const std::string& o, std::unique_ptr<ASTNode> l, std::unique_ptr<ASTNode> r) : op(o), left(std::move(l)), right(std::move(r)) {} void print(int indent = 0) const override { std::cout << std::string(indent, ' ') << "BinaryOp: " << op << "\n"; if (left) left->print(indent + 2); if (right) right->print(indent + 2); } }; // 主解析器类 class Parser { private: Lexer& lexer; Token lookahead; void match(TokenType expected) { if (lookahead.type == expected) { lookahead = lexer.nextToken(); } else { // 报错:期望expected,得到lookahead.value std::cerr << "Parse error at line " << lookahead.line << ", col " << lookahead.col << ": expected " << tokenTypeToString(expected) << ", got " << tokenTypeToString(lookahead.type) << "\n"; exit(1); } } std::string tokenTypeToString(TokenType t) { switch(t) { case TOK_INT: return "int"; case TOK_IF: return "if"; case TOK_ID: return "identifier"; case TOK_NUM: return "number"; case TOK_PLUS: return "'+'"; case TOK_ASSIGN: return "'='"; default: return "unknown"; } } // 解析Expr → Term ExprTail std::unique_ptr<ASTNode> parseExpr() { auto term = parseTerm(); return parseExprTail(std::move(term)); } // 解析ExprTail → AddOp Term ExprTail | ε std::unique_ptr<ASTNode> parseExprTail(std::unique_ptr<ASTNode> left) { if (lookahead.type == TOK_PLUS || lookahead.type == TOK_MINUS) { std::string op = (lookahead.type == TOK_PLUS) ? "+" : "-"; match(lookahead.type); auto right = parseTerm(); auto binop = std::make_unique<BinaryOpNode>(op, std::move(left), std::move(right)); return parseExprTail(std::move(binop)); } return left; // ε产生式 } // 解析Term → Factor TermTail std::unique_ptr<ASTNode> parseTerm() { auto factor = parseFactor(); return parseTermTail(std::move(factor)); } // 解析TermTail → MulOp Factor TermTail | ε std::unique_ptr<ASTNode> parseTermTail(std::unique_ptr<ASTNode> left) { if (lookahead.type == TOK_STAR || lookahead.type == TOK_SLASH) { std::string op = (lookahead.type == TOK_STAR) ? "*" : "/"; match(lookahead.type); auto right = parseFactor(); auto binop = std::make_unique<BinaryOpNode>(op, std::move(left), std::move(right)); return parseTermTail(std::move(binop)); } return left; } // 解析Factor → ID | NUM | ( Expr ) std::unique_ptr<ASTNode> parseFactor() { if (lookahead.type == TOK_ID) { std::string id = lookahead.value; match(TOK_ID); // 此处应返回标识符节点,但SNL中ID在Factor中仅作操作数,暂用占位 return std::make_unique<ASTNode>(); // 实际需具体节点类 } else if (lookahead.type == TOK_NUM) { std::string num = lookahead.value; match(TOK_NUM); return std::make_unique<ASTNode>(); } else if (lookahead.type == TOK_LPAREN) { match(TOK_LPAREN); auto expr = parseExpr(); match(TOK_RPAREN); return expr; } else { std::cerr << "Unexpected token in factor: " << tokenTypeToString(lookahead.type) << "\n"; exit(1); } } public: Parser(Lexer& l) : lexer(l), lookahead(l.nextToken()) {} // 解析整个程序(简化版,仅到赋值语句) std::unique_ptr<ASTNode> parse() { // Program → DeclList MainFunc,此处只解析一个赋值语句测试 if (lookahead.type == TOK_ID) { std::string id = lookahead.value; match(TOK_ID); match(TOK_ASSIGN); auto expr = parseExpr(); match(TOK_SEMI); return std::make_unique<AssignNode>(id, std::move(expr)); } return nullptr; } };

逻辑说明与参数说明:

  • match(TokenType)是递归下降核心:它检查当前lookahead是否为期望类型,是则消费该token并读取下一个;否则报错并退出。所有语法分析函数都依赖此机制推进。
  • parseExpr()和parseExprTail()的分工体现了消除左递归后的结构:parseExpr()负责第一个Term,parseExprTail()负责后续所有AddOp Term序列,并递归调用自身处理链式运算(如a+b+c)。
  • parseFactor()中对TOK_ID的处理是占位——实际需构建IdNode类存储变量名,供后续语义分析查符号表。此处省略是为了聚焦流程。
  • 错误提示包含line和col,这是调试时唯一定位依据。若只报syntax error,你将在100行代码中盲猜半小时。

4. 语义分析与中间代码生成:符号表、四元式与跳转填充的生死线

词法和语法分析只是“认字”和“断句”,语义分析才是让编译器真正“懂意思”的阶段。SNL课设中,80%的验收失败发生在语义分析之后——因为语法正确的代码,语义未必合法(如if (x + y) {...}中x+y是整数,非布尔)。而中间代码生成(四元式)则是连接语义与目标代码的“翻译官”,其质量直接决定最终汇编能否正确跳转。本节直击这两个阶段的硬核实现。

4.1 符号表设计:支持嵌套作用域的哈希表与作用域栈

SNL虽无函数,但支持{}块级作用域(如if和while体内的变量声明)。因此符号表不能是单层std::map<std::string, Symbol>,而需模拟“作用域栈”:进入{}时压入新作用域,退出时弹出,查找变量时从栈顶向下搜索。

// symbol_table.h #include <string> #include <unordered_map> #include <vector> #include <stack> struct Symbol { std::string name; std::string type; // "int" int scope_level; // 0为全局,1为第一层{},依此类推 Symbol(const std::string& n, const std::string& t, int level) : name(n), type(t), scope_level(level) {} }; class SymbolTable { private: std::stack<std::unordered_map<std::string, Symbol>> scopes; int current_level; public: SymbolTable() : current_level(0) { scopes.push({}); // 全局作用域 } // 进入新作用域(如遇到{) void enterScope() { current_level++; scopes.push({}); } // 退出当前作用域(如遇到}) void exitScope() { if (scopes.size() > 1) { scopes.pop(); current_level--; } } // 声明变量(在当前作用域) bool declare(const std::string& name, const std::string& type) { if (scopes.empty()) return false; auto& current_scope = scopes.top(); // 检查重复声明 if (current_scope.find(name) != current_scope.end()) { return false; // 已存在 } current_scope[name] = Symbol(name, type, current_level); return true; } // 查找变量(从当前作用域向上搜索) Symbol* lookup(const std::string& name) { for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { auto found = it->find(name); if (found != it->end()) { return &(found->second); } } return nullptr; // 未找到 } // 获取当前作用域级别 int getLevel() const { return current_level; } };

逻辑说明与参数说明:

  • scopes是std::stack,每个元素是一个std::unordered_map,存储当前作用域内所有变量。enterScope()压入空map,exitScope()弹出顶层map。
  • declare()在当前栈顶map中插入变量,并检查重复声明——这是语义分析第一道关卡。若int x; int x;不报错,后续生成代码时变量地址会混乱。
  • lookup()从栈顶(最内层作用域)开始向下遍历,找到第一个匹配的变量。这保证了{ int x = 1; { int x = 2; print(x); } }中内层x屏蔽外层x,符合作用域规则。
  • current_level仅用于调试打印,实际生成代码时不一定需要,但有助于理解作用域嵌套深度。

4.2 四元式生成:为什么必须用四元式,以及如何填充跳转地址

SNL目标代码通常是x86-64汇编,而汇编中的jmp L1、jle L2指令,其目标标签L1、L2在生成jmp时往往尚未定义(因为L1可能在if的else分支末尾)。四元式作为中间表示,用临时标签名(如L1,L2)代替具体地址,并在所有四元式生成完毕后,统一回填(backpatching)标签地址。这是编译器工程中最经典的“两遍扫描”技巧。

// quadruple.h #include <string> #include <vector> #include <memory> struct Quadruple { std::string op; // "+", "-", "jle", "j", "label" std::string arg1; // 左操作数(变量名、数字、临时变量t1) std::string arg2; // 右操作数(同上) std::string result; // 结果(变量名、临时变量、标签名) Quadruple(const std::string& o, const std::string& a1, const std::string& a2, const std::string& r) : op(o), arg1(a1), arg2(a2), result(r) {} }; class IRGenerator { private: std::vector<Quadruple> quads; int temp_count; std::vector<std::string> next_quad_labels; // 存储待填充的标签名列表 std::string newTemp() { return "t" + std::to_string(temp_count++); } // 生成四元式并返回其索引(用于跳转填充) int emit(const std::string& op, const std::string& arg1 = "", const std::string& arg2 = "", const std::string& result = "") { quads.emplace_back(op, arg1, arg2, result); return quads.size() - 1; // 返回索引 } // 生成带标签的四元式(如label L1) int emitLabel(const std::string& label) { return emit("label", "", "", label); } // 生成跳转指令(如jle x y L1),返回目标标签索引 int emitJump(const std::string& op, const std::string& arg1, const std::string& arg2, const std::string& label) { int idx = emit(op, arg1, arg2, label); // 记录此跳转指令索引,待label定义后回填 next_quad_labels.push_back(label); return idx; } public: IRGenerator() : temp_count(0) {} // 生成赋值语句 x = y + z 的四元式 void genAssign(const std::string& x, const std::string& y, const std::string& op, const std::string& z) { std::string t = newTemp(); emit(op, y, z, t); // (+, y, z, t) emit("=", t, "", x); // (=, t, _, x) } // 生成if语句的四元式序列 // if (cond) stmt1 else stmt2 void genIf(const std::string& cond_op, const std::string& arg1, const std::string& arg2, std::function<void()> then_block, std::function<void()> else_block) { // 1. 生成条件跳转:若条件假,则跳转到else开始处 std::string else_label = "L" + std::to_string(quads.size()); int jmp_idx = emitJump(cond_op, arg1, arg2, else_label); // jle x 0 L1 // 2. 生成then块 then_block(); // 3. 生成无条件跳转,跳过else块 std::string end_label = "L" + std::to_string(quads.size()); emit("j", "", "", end_label); // j L2 // 4. 定义else标签 emitLabel(else_label); // 5. 生成else块 else_block(); // 6. 定义结束标签 emitLabel(end_label); } // 获取所有四元式 const std::vector<Quadruple>& getQuads() const { return quads; } // 打印四元式(调试用) void print() const { for (size_t i = 0; i < quads.size(); i++) { std::cout << i << ": " << quads[i].op; if (!quads[i].arg1.empty()) std::cout << " " << quads[i].arg1; if (!quads[i].arg2.empty()) std::cout << " " << quads[i].arg2; if (!quads[i].result.empty()) std::cout << " " << quads[i].result; std::cout << "\n"; } } };

逻辑说明与参数说明:

  • emitJump()不仅生成跳转四元式,还将其目标标签名(如L1)存入next_quad_labels。当后续emitLabel("L1")被调用时,编译器需知道哪些跳转指令指向L1,从而修改其result字段。实际工程中,next_quad_labels应为std::map<std::string, std::vector<int>>,键为标签名,值为所有指向它的跳转指令索引列表。
  • genIf()展示了标准的“三地址码”模式:先生成条件跳转(跳过then),then块执行完后加无条件跳转(跳过else),再定义else标签,最后定义结束标签。这种结构确保无论走哪个分支,程序都能继续执行后续代码。
  • newTemp()生成"t1","t2"等临时变量名,用于保存中间计算结果。SNL变量少,无需复杂寄存器分配,直接映射到%rax,%rbx即可。

5. 目标代码生成与避坑指南:x86-64汇编生成、寄存器分配与5个血泪教训

走到这一步,你已拥有:正确分词的Token流、无语法错误的`

本文还有配套的精品资源,点击获取

返回列表