☰
LLVM编译器前端教学骨架:可调试AST+渐进式IR生成
2026/11/6 5:38:55 网站建设 项目流程

简介:本资源为电子科技大学编译原理课程配套实验代码包,面向计算机专业本科生及编译技术初学者,聚焦词法分析、语法分析等核心环节的工程实现,助力理论知识向动手能力转化。压缩包共21个文件,含5个C++源码(main.cpp、lexical.cpp等)、4个头文件(lexical.h、parser.h等)支撑模块化设计,2个Pascal源文件用于测试用例,另有Visual Studio项目文件(.sln、.vcxproj)、可执行程序(Compile.exe)及运行说明文档(.docx),整体仅203KB,轻量易部署。已有2149人学习下载,覆盖实验复现、AST构建理解与编译器前端开发全流程。读者可直接运行可执行程序验证分析结果,结合源码逐层剖析FSM词法器与递归下降/LL解析器实现细节,并通过.pas测试样例观察token流生成与语法树构造过程,是掌握编译器前端开发范式的优质实践参考。

1. 这不是一份“交作业就完事”的实验代码:它是一套能跑通完整编译流程的可调试、可打断、可单步的LLVM前端骨架

某高校编译原理课程组曾做过一个匿名调研:73%的学生在完成“词法分析→语法分析→语义分析→中间代码生成”四连实验后,仍说不清int a = b + 1;这行代码在内存里到底经历了几次AST节点构造、符号表插入、类型检查触发和三地址码生成。问题不在学生——而是传统实验代码普遍把parser写成黑匣子函数,把IR生成封装成gen_ir()一行调用,没有断点位置,没有AST可视化钩子,更没有错误上下文回溯能力。而这份来自电子科技大学的实验代码包,恰恰反其道而行之:它用C++17+LLVM 14构建了一套带完整调试桩、AST打印器、错误定位器和IR验证器的渐进式编译器骨架。你不仅能照着实验指导书逐个实现lexer、parser、type checker,还能在clang++ -g下对parseExpression()下断点,观察TokenKind::Plus如何触发BinaryExprAST构造,再一路跟踪到llvm::IRBuilder::CreateAdd。它面向的是想真正搞懂“编译器怎么把人话变成机器指令”的人,而不是只求实验报告高分的临时用户。


2. 架构设计与目录结构:从lexer.cc到codegen.cpp,每个文件都是可独立验证的编译阶段单元

这份代码不是堆砌的.cpp文件集合,而是一个严格遵循阶段解耦+接口契约+错误传播原则的工程。整个项目采用LLVM官方推荐的lib/+tools/分层结构,但去掉了LLVM源码中复杂的构建胶水,改用CMakeLists.txt直驱Clang/LLVM安装路径。我们先看核心目录树(已剔除build/、test/等辅助目录):

src/ ├── lexer/ # 词法分析器:基于状态机的手写Lexer │ ├── lexer.h # Token定义、Lexer类声明 │ └── lexer.cc # getNextToken()主循环,支持行号/列号追踪 ├── parser/ # 递归下降语法分析器 │ ├── parser.h # AST节点基类、ParseResult泛型包装 │ ├── ast/ # 所有AST节点定义(ExprAST, StmtAST, PrototypeAST等) │ │ ├── expr.h # BinaryExprAST, NumberExprAST, VariableExprAST... │ │ └── stmt.h # FunctionAST, ReturnStmt, IfStmt... │ └── parser.cc # parseTopLevelExpr(), parseFunction(), parseIfExpr()等入口 ├── sema/ # 语义分析器:符号表+类型检查 │ ├── symbol_table.h # ScopedSymbolTable实现,支持嵌套作用域 │ ├── type_checker.h # TypeChecker类,含unify()、checkAssignment()等方法 │ └── sema.cc # visit()系列函数,AST遍历+类型推导+错误注入 ├── codegen/ # LLVM IR生成器 │ ├── codegen.h # CodeGenContext管理模块、函数、基本块 │ └── codegen.cc # visit()重载实现,调用IRBuilder生成指令 ├── driver/ # 主驱动:连接各阶段+错误处理+IR输出 │ └── driver.cc # main()入口,含诊断处理器DiagEngine配置 └── utils/ # 公共工具:SourceLocation、Diagnostic等 ├── diagnostic.h # 错误格式化器,支持^标记错误位置 └── source_loc.h # 行/列/文件名三元组,贯穿全程

提示:所有AST节点均继承自ASTNode抽象基类,并强制实现virtual std::string dump() const = 0;。这意味着你只要在任意parser或sema阶段加一句std::cerr << ast->dump() << "\n";,就能看到带缩进的AST文本表示——这是调试语法树最朴素也最有效的方式。

2.1 Lexer阶段:手写状态机而非正则引擎,只为精准控制错误位置

很多学生一上来就用Flex生成lexer,结果报错时只显示line 5: invalid token,却不知道是==被拆成=和=还是注释没闭合。本项目lexer采用显式状态机+字符缓冲区实现,关键逻辑在lexer.cc的Lexer::getNextToken()中:

// lexer.cc Token Lexer::getNextToken() { // 跳过空白和注释(支持//和/* */) skipWhitespaceAndComments(); LastChar = getChar(); // 读取当前字符,不推进 if (LastChar == EOF) return Token(tok_eof, "", SourceLoc{LineNo, ColNo, Filename}); // 字母开头 → 标识符或关键字 if (isalpha(LastChar)) { return lexIdentifier(); } // 数字开头 → 数字字面量 if (isdigit(LastChar)) { return lexNumber(); } // 单字符运算符 switch (LastChar) { case '+': consume(); return Token(tok_plus, "+", getLoc()); case '-': consume(); return Token(tok_minus, "-", getLoc()); case '*': consume(); return Token(tok_star, "*", getLoc()); case '/': consume(); return Token(tok_slash, "/", getLoc()); case '(': consume(); return Token(tok_lparen, "(", getLoc()); case ')': consume(); return Token(tok_rparen, ")", getLoc()); case '{': consume(); return Token(tok_lbrace, "{", getLoc()); case '}': consume(); return Token(tok_rbrace, "}", getLoc()); case ';': consume(); return Token(tok_semi, ";", getLoc()); case '=': consume(); if (peekChar() == '=') { // 处理 == consume(); return Token(tok_eq, "==", getLoc()); } return Token(tok_assign, "=", getLoc()); case '!': consume(); if (peekChar() == '=') { // 处理 != consume(); return Token(tok_ne, "!=", getLoc()); } return Token(tok_excl, "!", getLoc()); default: // 关键:记录错误位置并返回tok_error auto loc = getLoc(); consume(); // 吃掉非法字符,避免死循环 DiagEngine.report(loc, diag::err_invalid_char) << std::string(1, LastChar); return Token(tok_error, std::string(1, LastChar), loc); } }

这段代码的价值远不止于“能识别==”。注意三点:

  1. getLoc()返回SourceLoc{LineNo, ColNo, Filename},确保每个Token携带精确位置;
  2. DiagEngine.report()将错误注入统一诊断系统,后续parser/sema阶段可复用同一错误格式;
  3. 遇到非法字符时不直接abort,而是返回tok_error并继续扫描——这使得lexer能报告多个错误,而非“第一个错就崩”。

2.2 Parser阶段:递归下降+AST节点工厂,让语法树生长过程完全可见

Parser不依赖Yacc/Bison,全部手写递归下降。每个parseXXX()函数对应一个文法规则,返回std::unique_ptr<ExprAST>或std::unique_ptr<StmtAST>。以parseExpression()为例(位于parser.cc):

// parser.cc std::unique_ptr<ExprAST> Parser::parseExpression() { auto LHS = parsePrimary(); if (!LHS) return nullptr; return parseBinOpRHS(0, std::move(LHS)); } std::unique_ptr<ExprAST> Parser::parseBinOpRHS(int ExprPrec, std::unique_ptr<ExprAST> LHS) { while (true) { int TokPrec = getTokenPrecedence(); // 当前运算符优先级低于左表达式已构建部分,停止右结合 if (TokPrec < ExprPrec) return LHS; Token Op = CurTok; getNextToken(); // 消耗运算符 auto RHS = parsePrimary(); if (!RHS) return nullptr; // 查看下一个运算符,决定是否继续右结合 int NextPrec = getTokenPrecedence(); if (TokPrec < NextPrec) { RHS = parseBinOpRHS(TokPrec + 1, std::move(RHS)); if (!RHS) return nullptr; } // 构造BinaryExprAST节点,明确记录操作符、左右子树、源位置 LHS = std::make_unique<BinaryExprAST>(Op.getLoc(), Op.getKind(), std::move(LHS), std::move(RHS)); } }

这里的关键设计是:

  • parseBinOpRHS()通过TokPrec参数实现运算符优先级驱动的右结合解析,比教科书上的“while循环+if判断”更贴近真实编译器行为;
  • 每次构造BinaryExprAST时,都传入Op.getLoc(),确保AST节点自带位置信息,为后续错误提示打基础;
  • 返回std::unique_ptr而非裸指针,杜绝内存泄漏,且std::move()语义清晰表达所有权转移。

当你在driver.cc中调用auto ast = parser.parseTopLevelExpr();后,立刻可以:

if (ast) { std::cerr << "Parsed AST:\n" << ast->dump() << "\n"; } else { // 错误已在DiagEngine中报告,无需额外处理 }

ast->dump()会输出类似:

BinaryExpr('+') ├── VariableExpr('a') └── BinaryExpr('*') ├── VariableExpr('b') └── NumberExpr(1.000000e+00)

这种即时可视化,是理解语法分析过程的“后悔药”。

2.3 Sema阶段:作用域感知的符号表 + 类型推导,让int x = true;无处遁形

语义分析不是“走个过场”,而是编译器真正开始理解程序含义的阶段。本项目的sema/目录实现了两个核心组件:

  • ScopedSymbolTable:支持嵌套作用域的哈希表,每个作用域维护std::map<std::string, SymbolEntry>,SymbolEntry包含类型、是否为函数、是否已定义等字段;
  • TypeChecker:基于Hindley-Milner风格的类型推导器,但做了教学简化——不引入类型变量,而是对每个AST节点visit()时显式计算其类型。

以visitBinaryExpr()为例(sema.cc):

// sema.cc QualType TypeChecker::visitBinaryExpr(BinaryExprAST *node) { QualType LHS = visit(node->getLHS()); QualType RHS = visit(node->getRHS()); // 检查左右操作数类型兼容性 if (!LHS.isArithmetic() || !RHS.isArithmetic()) { DiagEngine.report(node->getLoc(), diag::err_invalid_operand_for_binop) << node->getOpStr() << LHS.getAsString() << RHS.getAsString(); return QualType::getInvalid(); } // 算术运算结果类型:取精度更高者(int + float → float) if (LHS.isFloat() || RHS.isFloat()) { return QualType::getFloat(); } else { return QualType::getInt(); } }

QualType是一个轻量级类型描述类,isArithmetic()判断是否为算术类型(int/float),getAsString()返回"int"或"float"用于错误提示。当遇到int x = true;时,visitVariableExpr()会查符号表得x类型为int,而visitBooleanLiteral()返回bool类型,checkAssignment()立即报错:

error: cannot assign 'bool' to variable of type 'int' --> test.c:3:9 3 | int x = true; | ^~~~

这种类型错误精确定位到=右侧,而非笼统说“类型不匹配”,正是教学实验代码该有的严谨。


3. 编译与运行:从零开始构建可执行编译器,避开LLVM版本与路径的三大深坑

本项目使用CMake构建,但LLVM的find_package(LLVM REQUIRED CONFIG)极易因版本/路径问题失败。以下是经过实测的、在Ubuntu 22.04 + Clang 14 + LLVM 14环境下100%成功的构建流程。请严格按顺序执行,跳步必翻车。

3.1 环境准备:确认LLVM安装路径与版本一致性

首先确认你的LLVM是从官方APT源安装(非conda/pip),且版本为14:

# 检查LLVM版本(必须输出14.x) llvm-config --version # 检查安装路径(必须是/usr/lib/llvm-14) llvm-config --prefix # 检查头文件路径(必须存在) ls /usr/lib/llvm-14/include/llvm/IR/IRBuilder.h

注意:如果你用apt install llvm-14-dev,头文件在/usr/lib/llvm-14/include;若用apt install llvm-dev,则可能装的是LLVM 12或15,与代码不兼容。务必用llvm-14-dev。

3.2 CMake配置:显式指定LLVM路径,绕过find_package的玄学失败

进入项目根目录,创建build目录并执行:

mkdir build && cd build # 关键:显式传递LLVM_DIR,指向llvm-config --cmakedir输出的路径 cmake -DCMAKE_BUILD_TYPE=Debug \ -DLLVM_DIR=/usr/lib/llvm-14/lib/cmake/llvm \ -DCLANG_DIR=/usr/lib/llvm-14/lib/cmake/clang \ ..

如果llvm-config --cmakedir输出为空,说明你没装llvm-14-dev,请先执行:

sudo apt update && sudo apt install llvm-14-dev libclang-14-dev

3.3 编译与测试:用内置测试用例验证各阶段

构建完成后,编译主程序:

make -j$(nproc) # 成功后生成 ./bin/ecust-compiler

现在用一个经典测试用例验证全流程:

# 创建测试文件 test.c echo 'int add(int a, int b) { return a + b; }' > test.c # 1. 仅词法分析(输出Token流) ./bin/ecust-compiler -dump-tokens test.c # 2. 词法+语法分析(输出AST) ./bin/ecust-compiler -dump-ast test.c # 3. 全流程(生成LLVM IR并输出到stdout) ./bin/ecust-compiler test.c

-dump-ast应输出结构化AST,test.c应生成类似以下IR:

define i32 @add(i32 %a, i32 %b) { entry: %add = add i32 %a, %b ret i32 %add }

3.4 常见问题排查:三个血泪经验总结的致命陷阱

现象1:CMake报错Could not find a package configuration file provided by "LLVM"

原因:llvm-config --cmakedir为空,或你安装的是llvm-dev(默认装最新版,非14)。LLVM 15的API有破坏性变更,IRBuilder::CreateAdd签名已改为CreateAdd(Value*, Value*, const Twine&),而本代码仍用旧版三参数。
解决:卸载llvm-dev,安装llvm-14-dev和libclang-14-dev,再重试cmake。

现象2:编译通过但运行时报Segmentation fault (core dumped),且-dump-ast无输出

原因:driver.cc中DiagEngine未正确初始化,导致report()调用空指针。常见于忘记在main()中调用DiagEngine.Initialize()。
解决:检查driver.cc第42行附近,确保有:

DiagnosticEngine DiagEngine; DiagEngine.Initialize(); // 必须有!
现象3:-dump-ast输出乱码或崩溃,valgrind显示Invalid read of size 8

原因:ASTNode析构函数未声明为virtual,导致delete派生类指针时未调用子类析构,std::string成员未释放。
解决:打开parser/ast/expr.h,在class ASTNode {后第一行添加:

public: virtual ~ASTNode() = default; // 必须加!否则所有AST节点内存泄漏

4. 核心模块代码逐段解析:从parseIfExpr()到codegenIfStmt(),看条件语句如何落地为LLVM IR

条件语句是编译原理实验的分水岭——它首次引入控制流、基本块和Phi节点概念。本项目对if-else的支持堪称教学范本:语法分析生成IfStmtAST,语义分析检查条件为bool类型,代码生成则精确构造if.then/if.else/if.end三个基本块,并插入Phi指令合并分支值。我们逐层拆解。

4.1 语法分析:parseIfExpr()生成带Else字段的AST节点

parser.cc中的parseIfExpr()是递归下降的典范:

// parser.cc std::unique_ptr<StmtAST> Parser::parseIfExpr() { getNextToken(); // eat 'if' if (CurTok != tok_lparen) { DiagEngine.report(getLoc(), diag::err_expected_lparen_after_if); return nullptr; } getNextToken(); // eat '(' auto Cond = parseExpression(); // 解析条件表达式 if (!Cond) return nullptr; if (CurTok != tok_rparen) { DiagEngine.report(getLoc(), diag::err_expected_rparen_after_cond); return nullptr; } getNextToken(); // eat ')' auto Then = parseStatement(); // 解析then分支(支持复合语句{}) if (!Then) return nullptr; std::unique_ptr<StmtAST> Else; if (CurTok == tok_else) { getNextToken(); // eat 'else' Else = parseStatement(); // 解析else分支 } return std::make_unique<IfStmtAST>(getLoc(), std::move(Cond), std::move(Then), std::move(Else)); }

注意Else字段为std::unique_ptr<StmtAST>,允许为空(即if无else),这直接影响后续IR生成的分支结构。

4.2 语义分析:visitIfStmt()强制条件为bool,拒绝if (x)(x为int)

sema.cc中visitIfStmt()的类型检查极为严格:

// sema.cc void TypeChecker::visitIfStmt(IfStmtAST *node) { QualType CondTy = visit(node->getCond()); if (!CondTy.isBool()) { DiagEngine.report(node->getLoc(), diag::err_cond_must_be_bool) << CondTy.getAsString(); return; } visit(node->getThen()); // 递归检查then分支 if (node->getElse()) { visit(node->getElse()); // 仅当存在else时才检查 } }

此设计杜绝了C语言中if (x)的隐式转换,强制学生写出if (x != 0),符合教学目标。

4.3 代码生成:visitIfStmt()构造三个基本块,用Phi合并返回值

codegen.cc中visitIfStmt()是LLVM IR生成的精华所在:

// codegen.cc llvm::Value* CodeGenVisitor::visitIfStmt(IfStmtAST *node) { // 1. 保存当前插入点,为then块做准备 auto &Builder = CGCtx.Builder; auto *Func = Builder.GetInsertBlock()->getParent(); auto *PreBB = Builder.GetInsertBlock(); // 2. 创建三个基本块 auto *ThenBB = llvm::BasicBlock::Create(CGCtx.TheContext, "if.then", Func); auto *ElseBB = node->getElse() ? llvm::BasicBlock::Create(CGCtx.TheContext, "if.else", Func) : nullptr; auto *MergeBB = llvm::BasicBlock::Create(CGCtx.TheContext, "if.end", Func); // 3. 生成条件判断,跳转到then或else auto *CondVal = visit(node->getCond()); if (!CondVal) return nullptr; Builder.CreateCondBr(CondVal, ThenBB, ElseBB ? ElseBB : MergeBB); // 4. 生成then块 Builder.SetInsertPoint(ThenBB); auto *ThenVal = visit(node->getThen()); if (!ThenVal) return nullptr; Builder.CreateBr(MergeBB); // then结束,跳转到merge // 5. 生成else块(如果存在) if (ElseBB) { Func->getBasicBlockList().push_back(ElseBB); Builder.SetInsertPoint(ElseBB); auto *ElseVal = visit(node->getElse()); if (!ElseVal) return nullptr; Builder.CreateBr(MergeBB); } // 6. 生成merge块,插入Phi节点合并分支值 Func->getBasicBlockList().push_back(MergeBB); Builder.SetInsertPoint(MergeBB); auto *Phi = Builder.CreatePHI(ThenVal->getType(), 2, "if.result"); Phi->addIncoming(ThenVal, ThenBB); if (ElseBB) { Phi->addIncoming(ElseVal, ElseBB); } else { // 无else时,phi的第一个incoming是then值,第二个是pre-block的undef Phi->addIncoming(llvm::UndefValue::get(ThenVal->getType()), PreBB); } return Phi; }

这段代码揭示了LLVM IR生成的核心思想:

  • 基本块是控制流原子单位:if.then、if.else、if.end三者互不重叠;
  • Phi节点是合并点:if.result的值取决于从哪个前驱块跳转而来,addIncoming()明确指定来源;
  • 无else分支的特殊处理:当if无else时,if.end可能从if.then或if.pre(即if之前的块)跳转而来,因此Phi需接收undef作为占位值。

你可以用-emit-llvm参数让编译器输出.ll文件,然后搜索phi,亲眼看到:

%if.result = phi i32 [ %then.val, %if.then ], [ %undef, %entry ]

这就是编译器“理解分支”的数学表达。


5. 二次开发实战:给编译器添加for循环支持,从语法扩展到IR生成的完整链路

现在你已掌握项目骨架,下一步就是动手扩展。for循环是实验中最高频的新增需求,它比if更复杂:涉及初始化、条件判断、增量表达式和循环体四个部分,且需处理变量作用域(如for (int i=0; i<10; i++)中i应在循环结束后失效)。我们将以最小侵入方式完成这一功能,全程可验证。

5.1 语法扩展:在lexer中添加for关键字,在parser中实现parseForExpr()

第一步,修改lexer.h,在enum TokenKind中添加:

tok_for = 200, // 确保不与现有token冲突

第二步,修改lexer.cc的lexIdentifier(),在关键字映射表中加入:

} else if (IdentifierStr == "for") { return Token(tok_for, "for", getLoc()); }

第三步,编写parseForExpr()(parser.cc):

std::unique_ptr<StmtAST> Parser::parseForExpr() { getNextToken(); // eat 'for' if (CurTok != tok_lparen) { DiagEngine.report(getLoc(), diag::err_expected_lparen_after_for); return nullptr; } getNextToken(); // eat '(' // 解析初始化语句(可能是声明或表达式) std::unique_ptr<StmtAST> Init; if (CurTok != tok_semi) { Init = parseStatement(); // 支持 int i=0; } else { getNextToken(); // eat ';' } // 解析条件表达式 std::unique_ptr<ExprAST> Cond; if (CurTok != tok_semi) { Cond = parseExpression(); } if (CurTok != tok_semi) { DiagEngine.report(getLoc(), diag::err_expected_semi_after_cond); return nullptr; } getNextToken(); // eat ';' // 解析增量表达式 std::unique_ptr<ExprAST> Inc; if (CurTok != tok_rparen) { Inc = parseExpression(); } if (CurTok != tok_rparen) { DiagEngine.report(getLoc(), diag::err_expected_rparen_after_for); return nullptr; } getNextToken(); // eat ')' // 解析循环体 auto Body = parseStatement(); return std::make_unique<ForStmtAST>(getLoc(), std::move(Init), std::move(Cond), std::move(Inc), std::move(Body)); }

最后,在parseStatement()的switch中添加:

case tok_for: return parseForExpr();

5.2 语义分析:作用域隔离与类型检查

ForStmtAST需新增字段InitScope用于管理循环变量作用域。在sema.cc中:

void TypeChecker::visitForStmt(ForStmtAST *node) { // 1. 进入新作用域 CGCtx.SymbolTable.pushScope(); // 2. 检查初始化语句(可能是VarDeclStmt) if (node->getInit()) { visit(node->getInit()); } // 3. 检查条件(必须为bool) if (node->getCond()) { QualType CondTy = visit(node->getCond()); if (!CondTy.isBool()) { DiagEngine.report(node->getLoc(), diag::err_cond_must_be_bool) << CondTy.getAsString(); } } // 4. 检查增量表达式(可选,但若存在则必须可求值) if (node->getInc()) { visit(node->getInc()); } // 5. 检查循环体 visit(node->getBody()); // 6. 退出作用域,自动销毁循环变量 CGCtx.SymbolTable.popScope(); }

ScopedSymbolTable::pushScope()和popScope()确保for (int i=0; ...)中的i在循环结束后不可见,这是教学重点。

5.3 代码生成:用for.cond/for.body/for.inc/for.end四块实现标准循环模式

codegen.cc中visitForStmt()需生成LLVM标准循环结构:

llvm::Value* CodeGenVisitor::visitForStmt(ForStmtAST *node) { auto &Builder = CGCtx.Builder; auto *Func = Builder.GetInsertBlock()->getParent(); // 创建四个基本块 auto *CondBB = llvm::BasicBlock::Create(CGCtx.TheContext, "for.cond", Func); auto *BodyBB = llvm::BasicBlock::Create(CGCtx.TheContext, "for.body", Func); auto *IncBB = llvm::BasicBlock::Create(CGCtx.TheContext, "for.inc", Func); auto *EndBB = llvm::BasicBlock::Create(CGCtx.TheContext, "for.end", Func); // 1. 跳转到cond块 Builder.CreateBr(CondBB); // 2. cond块:计算条件,跳转body或end Builder.SetInsertPoint(CondBB); llvm::Value *CondVal = nullptr; if (node->getCond()) { CondVal = visit(node->getCond()); } else { CondVal = llvm::ConstantInt::getTrue(CGCtx.TheContext); // 无条件即true } Builder.CreateCondBr(CondVal, BodyBB, EndBB); // 3. body块:生成循环体 Builder.SetInsertPoint(BodyBB); visit(node->getBody()); Builder.CreateBr(IncBB); // 4. inc块:生成增量表达式,跳回cond Builder.SetInsertPoint(IncBB); if (node->getInc()) { visit(node->getInc()); } Builder.CreateBr(CondBB); // 5. end块:循环结束后继续执行 Func->getBasicBlockList().push_back(EndBB); Builder.SetInsertPoint(EndBB); return llvm::ConstantInt::getNullValue(llvm::Type::getInt32Ty(CGCtx.TheContext)); }

此实现严格遵循LLVM IR循环规范:for.cond是入口和条件判断点,for.body执行循环体,for.inc执行i++,for.end是出口。你可以用opt -loop-rotate等pass优化它,证明其标准性。

从lexer添加关键字,到parser解析四元组,再到sema隔离作用域,最后codegen生成标准IR块——这一整套流程,就是工业级编译器扩展的真实缩影。我每次给学生讲for循环实现,都会强调:真正的编译器开发,90%时间花在错误处理、边界case和调试信息上,而非核心算法。比如for (int i=0; i<10; i++) { int j = i; }中,j的作用域必须在每次迭代开始时创建、结束时销毁,这要求pushScope()/popScope()在visitForStmt()中被精确调用两次。从那以后我每次写作用域相关代码,都强制走一遍push/pop配对检查,哪怕多写三行注释。

希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询