
简介本资源是一个面向计算机专业本科生的编译原理课程设计实践项目聚焦用Java实现C语言子集的LL(1)文法编译器帮助学习者深入理解词法分析、语法解析、语义检查与代码生成四大核心阶段。压缩包共67个文件含22个Java源码文件实现Parser、Lexer、AST等关键模块、33个编译生成的class文件、4个txt文档含文法定义grammer.txt及README说明、1个C语言测试样例a.c以及项目配置文件.project、.classpath、许可证与背景图等整体仅210KB轻量易部署。已有143人下载学习适合课堂实践、课程设计参考或编译器原理入门复现。读者可直接运行调试完整编译流程获取带错误定位的LL(1)解析器实现细节、AST构建逻辑、Java版编译器工程结构组织方式以及针对C子集变量声明、if/for/while控制流、函数定义的语法支持边界说明。1. 为什么用 Java 写 C 编译器不是“玩具项目”而是工程级语言工具链的可控入口你可能刚看到标题就皱眉C 编译器用 C 写不是天经地义吗Java 做编译器是不是又一个课堂作业级别的 parserlexer 演示不。这个【基于 Java 的 C 语言编译器】编号 100013298是真实存在于工业级教学与嵌入式工具链演进中的落地项目——它不生成 x86 机器码也不对标 GCC 或 Clang而是聚焦于C89/C90 子集的完整前端 中间表示IR生成 可验证目标代码输出如 JVM 字节码、LLVM IR 或自定义汇编核心价值在于用 Java 的强类型、垃圾回收、JVM 跨平台性和成熟调试生态把编译器开发从“黑匣子系统编程”拉回到可单步调试、可热重载、可单元测试的软件工程轨道上。它适合三类人高校编译原理课需交付可运行、可评分、可扩展的课程设计避免学生卡在 lex/yacc 语法冲突或内存泄漏上嵌入式团队想为私有指令集快速构建轻量 C 前端比如给 RISC-V 自定义扩展加语法支持以及 Java 工程师想穿透“写业务逻辑”表层真正吃透词法/语法/语义/优化四层流水线——这里没有玄学每行 AST 构建、每个符号表插入、每次类型检查都能在 IntelliJ 里打断点、看变量、改逻辑、重跑测试。它不是替代 GCC而是让你第一次看清“int a b 1;”背后到底发生了多少次哈希查找、多少次作用域跳转、多少次隐式类型转换。2. 从词法分析到抽象语法树用 Java 实现 C89 前端的最小可行路径C 语言编译器前端的核心任务是把字符流变成结构化的程序表示。Java 生态里没有 lex/yacc 那种“写规则自动生成代码”的传统路径但反而更利于理解每一步——我们不用黑盒生成器而用手写状态机 递归下降解析器全程可控、可测、可 debug。这不是倒退而是回归本质当你亲手写出skipWhitespace()、readIdentifier()、parsePrimaryExpression()时才真正明白为什么ab是语法错误而a b是合法的。2.1 手写词法分析器状态机驱动的 Token 流生成C89 关键字少仅 32 个、运算符固定、注释规则明确/* */但不支持//这让我们能用纯 Java 实现一个健壮的Lexer类无需依赖 ANTLR 或 JavaCC。核心是维护pos指针和currentChar按字符类型切换状态public class Lexer { private final String input; private int pos 0; private char currentChar; public Lexer(String input) { this.input input; advance(); // 初始化读取第一个字符 } private void advance() { if (pos input.length()) { currentChar input.charAt(pos); } else { currentChar \0; } } public Token getNextToken() { while (Character.isWhitespace(currentChar)) advance(); if (currentChar \0) return new Token(TokenType.EOF, ); if (Character.isLetter(currentChar) || currentChar _) { return readIdentifier(); } if (Character.isDigit(currentChar)) { return readNumber(); } if (currentChar / peek() *) { advance(); advance(); // 跳过 /* return readBlockComment(); } // 其他单字符 token - * / % ! | ^ ~ [ ] { } ( ) ; , . switch (currentChar) { case : advance(); return new Token(TokenType.PLUS, ); case -: advance(); return new Token(TokenType.MINUS, -); case *: advance(); return new Token(TokenType.STAR, *); case /: advance(); return new Token(TokenType.SLASH, /); case %: advance(); return new Token(TokenType.PERCENT, %); case : advance(); return (currentChar ) ? (advance(), new Token(TokenType.EQ, )) : new Token(TokenType.ASSIGN, ); // ... 其他 case 省略实际需覆盖全部 C89 运算符 } throw new RuntimeException(Unexpected character: currentChar); } private char peek() { return (pos input.length()) ? input.charAt(pos) : \0; } private Token readIdentifier() { StringBuilder sb new StringBuilder(); while (Character.isLetterOrDigit(currentChar) || currentChar _) { sb.append(currentChar); advance(); } String value sb.toString(); if (KEYWORDS.contains(value)) { return new Token(TokenType.KEYWORD, value); } return new Token(TokenType.IDENTIFIER, value); } }关键参数说明KEYWORDS是Set.of(auto, break, case, ..., while)静态常量Token类必须包含type枚举、value原始字符串、line/column用于报错定位peek()方法是处理、!、等双字符运算符的基础设施——没有它你就得在getNextToken()里反复advance()回退极易出错。2.2 递归下降解析器按 C89 语法规则逐层构建 ASTC89 语法相对简单无函数重载、无模板、无复杂声明语法适合手写递归下降。我们不追求 100% 覆盖比如忽略_Pragma和__attribute__但必须保证int main() { return 0; }和struct { int x; } s;这类典型代码能正确解析。核心是Parser类每个parseXXX()方法对应一条语法规则public class Parser { private final Lexer lexer; private Token currentToken; public Parser(Lexer lexer) { this.lexer lexer; this.currentToken lexer.getNextToken(); } public Program parseProgram() { ListDeclaration declarations new ArrayList(); while (currentToken.getType() ! TokenType.EOF) { declarations.add(parseDeclaration()); } return new Program(declarations); } private Declaration parseDeclaration() { // C89 声明int a; 或 int f() { ... } 或 struct S { ... }; TokenType first currentToken.getType(); if (first TokenType.KEYWORD isTypeKeyword(currentToken.getValue())) { return parseTypeDeclaration(); } else if (first TokenType.KEYWORD struct.equals(currentToken.getValue())) { return parseStructDeclaration(); } else { throw new RuntimeException(Expected declaration at currentToken); } } private Declaration parseTypeDeclaration() { String type currentToken.getValue(); advance(); // consume type keyword String name expect(TokenType.IDENTIFIER).getValue(); advance(); if (accept(TokenType.SEMICOLON)) { return new VariableDeclaration(type, name); // int a; } else if (accept(TokenType.LPAREN)) { return parseFunctionDefinition(type, name); // int f() { ... } } else { throw new RuntimeException(Expected ; or ( after identifier); } } private Token expect(TokenType expected) { if (currentToken.getType() expected) { Token t currentToken; advance(); return t; } throw new RuntimeException(Expected expected , got currentToken.getType()); } private boolean accept(TokenType type) { if (currentToken.getType() type) { advance(); return true; } return false; } private void advance() { currentToken lexer.getNextToken(); } }为什么选递归下降而非 Pratt 解析C89 运算符优先级明确15 级且无左递归expr: expr term在 C89 中不存在递归下降逻辑清晰、调试友好。expect()和accept()是两个关键辅助方法前者强制匹配并报错后者尝试匹配返回布尔值——这是处理if (cond) stmt else stmt中else可选性的基础。isTypeKeyword()必须包含int,char,void,struct等注意signed/unsigned是修饰符需在parseTypeSpecifiers()中组合处理。2.3 AST 设计让语义分析有据可依而非字符串拼接很多初学者的“编译器”止步于打印 AST 结构但真正的工程价值在于AST 节点必须携带类型信息、作用域引用、源码位置才能支撑后续的语义检查和 IR 生成。我们定义Node抽象基类所有具体节点BinaryOp,FunctionCall,VariableRef继承它并添加type字段Type.INT,Type.POINTER_TO(Type.CHAR)和scope字段指向SymbolTable实例abstract class Node { protected final int line, column; protected Type type; // 语义分析后填充 protected SymbolTable scope; // 该节点所在作用域 Node(int line, int column) { this.line line; this.column column; } } class BinaryOp extends Node { final Node left, right; final String op; // , -, etc. BinaryOp(int line, int column, Node left, String op, Node right) { super(line, column); this.left left; this.right right; this.op op; } } class FunctionCall extends Node { final String functionName; final ListNode arguments; FunctionCall(int line, int column, String functionName, ListNode arguments) { super(line, column); this.functionName functionName; this.arguments arguments; } }关键设计原则line/column必须在Lexer生成Token时就记录new Token(type, value, line, col)并在Parser构造Node时传入——这是实现精准报错如error: x undeclared (first use in this function)的唯一途径。type字段初始为null由SemanticAnalyzer填充scope字段在进入{}块时由Parser创建新SymbolTable并传递给子节点。这种设计让 AST 不再是语法树而是带上下文的语义载体。3. 符号表与类型系统C89 作用域规则的 Java 实现C89 的作用域规则看似简单文件作用域、函数作用域、块作用域但细节致命int a; { int a 1; printf(%d, a); }中内层a隐藏外层a但函数参数int f(int a)的a属于函数作用域与外层同名变量不冲突。用 Java 实现这套规则核心是SymbolTable的嵌套与查找链以及Type类的不可变性与等价判断。3.1 嵌套符号表用 Stack 模拟作用域栈我们不使用 HashMap 做全局表而是为每个作用域创建独立SymbolTable实例通过parent引用形成链表。Parser在遇到{时pushScope()遇到}时popScope()public class SymbolTable { private final MapString, Symbol symbols new HashMap(); private final SymbolTable parent; private final int depth; public SymbolTable(SymbolTable parent) { this.parent parent; this.depth (parent null) ? 0 : parent.depth 1; } public void define(String name, Symbol symbol) { symbols.put(name, symbol); } public Symbol resolve(String name) { Symbol sym symbols.get(name); if (sym ! null) return sym; if (parent ! null) return parent.resolve(name); return null; // not found } public boolean hasLocal(String name) { return symbols.containsKey(name); } }为什么不用 ThreadLocal 或静态单例因为编译器必须支持多文件、多函数并发解析比如 IDE 实时检查每个Parser实例需独立作用域栈。depth字段用于调试时打印作用域层级depth0: global, depth1: function, depth2: block。hasLocal()是关键——语义分析时若resolve()找到符号但hasLocal()为 false说明是外层变量被隐藏此时应警告“variable x shadows a previous declaration”。3.2 C89 类型系统用枚举组合实现指针/数组/结构体C89 类型只有基本类型int,char,void、指针、数组、结构体、函数类型。Java 中用Type枚举 组合类实现abstract class Type { public static final Type INT new PrimitiveType(int); public static final Type CHAR new PrimitiveType(char); public static final Type VOID new PrimitiveType(void); public abstract boolean isCompatibleWith(Type other); // 类型兼容性判断 public abstract boolean isArithmetic(); // 是否可参与 - * / public abstract boolean isPointer(); // 是否为指针 } final class PrimitiveType extends Type { private final String name; PrimitiveType(String name) { this.name name; } Override public boolean isCompatibleWith(Type other) { return this other || (other instanceof PrimitiveType (int.equals(name) char.equals(((PrimitiveType) other).name))); } Override public boolean isArithmetic() { return true; } Override public boolean isPointer() { return false; } } final class PointerType extends Type { private final Type baseType; PointerType(Type baseType) { this.baseType baseType; } Override public boolean isCompatibleWith(Type other) { if (!(other instanceof PointerType)) return false; return this.baseType.isCompatibleWith(((PointerType) other).baseType); } Override public boolean isArithmetic() { return false; } Override public boolean isPointer() { return true; } }关键细节isCompatibleWith()必须支持int*与char*的赋值C89 允许尽管不安全但禁止int*与int直接比较。PointerType的baseType可以是PrimitiveType、另一个PointerTypeint**甚至StructTypestruct S*。StructType需要MapString, Field存储成员Field包含name和type并实现equals()以支持struct A { int x; }与struct B { int x; }的类型等价判断C89 中结构体标签不同即为不同类型但字段相同可赋值。3.3 语义分析器遍历 AST填充类型报告错误SemanticAnalyzer是连接语法与语义的桥梁。它接收Program根节点和全局SymbolTable深度优先遍历对每个节点执行变量声明define()到当前作用域检查重复定义变量引用resolve()查找若为null报 “undeclared identifier”表达式递归分析子节点调用inferType()计算结果类型检查操作数类型兼容性函数调用resolve()函数符号比对参数数量与类型。public class SemanticAnalyzer { private SymbolTable currentScope; public void analyze(Program program) { currentScope new SymbolTable(null); // global scope for (Declaration decl : program.getDeclarations()) { analyzeDeclaration(decl); } } private void analyzeDeclaration(Declaration decl) { if (decl instanceof VariableDeclaration v) { if (currentScope.hasLocal(v.getName())) { throw new SemanticError(Redeclaration of v.getName() , v.getLine(), v.getColumn()); } currentScope.define(v.getName(), new Symbol(v.getType(), v.getName())); } else if (decl instanceof FunctionDefinition f) { // 函数声明进入 global scope currentScope.define(f.getName(), new Symbol(f.getReturnType(), f.getName())); // 进入函数作用域分析函数体 SymbolTable funcScope new SymbolTable(currentScope); currentScope funcScope; analyzeStatement(f.getBody()); currentScope currentScope.getParent(); // 回退到 global } } private void analyzeStatement(Statement stmt) { if (stmt instanceof BlockStatement b) { SymbolTable blockScope new SymbolTable(currentScope); currentScope blockScope; for (Statement s : b.getStatements()) { analyzeStatement(s); } currentScope currentScope.getParent(); } else if (stmt instanceof ExpressionStatement e) { analyzeExpression(e.getExpression()); } } private Type analyzeExpression(Expression expr) { if (expr instanceof BinaryOp b) { Type leftType analyzeExpression(b.getLeft()); Type rightType analyzeExpression(b.getRight()); if (!leftType.isCompatibleWith(rightType)) { throw new SemanticError(Incompatible types in binary operation: leftType and rightType, b.getLine(), b.getColumn()); } b.setType(leftType); // 简化假设 - * / 结果类型同操作数 return leftType; } else if (expr instanceof VariableRef v) { Symbol sym currentScope.resolve(v.getName()); if (sym null) { throw new SemanticError(Undeclared identifier v.getName() , v.getLine(), v.getColumn()); } v.setType(sym.getType()); return sym.getType(); } return null; } }血泪经验analyzeExpression()必须返回Type因为父节点如BinaryOp需要它来检查兼容性。VariableRef的setType()是填充 AST 的关键一步——后续 IR 生成器直接读取v.getType()无需再次查表。错误信息必须包含line/column这是用户能快速定位问题的唯一依据。4. 生成中间表示IR从 AST 到三地址码的确定性翻译有了带类型和作用域的 AST下一步是生成平台无关的中间表示。C89 的控制流简单if,while,for,return数据流清晰无闭包、无异常最适合生成三地址码Three-Address Code, TAC——每条指令最多三个操作数x y op z或goto L1便于后续优化和目标代码生成。Java 实现的关键是用StringBuilder累积指令用StackString管理标签用MapNode, String缓存表达式计算结果。4.1 TAC 指令集设计覆盖 C89 所有操作我们定义最小完备指令集共 12 条足够表达 C89assign x y变量赋值binop x y op z二元运算,-,*,/,,!,,,,unary x op y一元运算-,!,*解引用,取地址call x f(a, b, c)函数调用return x返回值goto L1无条件跳转if x goto L1条件跳转label L1:标签定义param x函数参数传递alloc x size栈分配用于局部变量load x y[i]数组加载store y[i] x数组存储为什么不用 LLVM IR 或 JVM 字节码LLVM 太重需 JNI 或绑定JVM 字节码对 C 的指针模型支持差。TAC 是编译器教材标准且完全可控每条指令对应一行文本可直接System.out.println()输出也可序列化为 JSON 供后续模块消费。4.2 表达式翻译递归生成 TAC管理临时变量TACGenerator遍历 AST为每个表达式生成 TAC 序列并返回其计算结果所在的临时变量名如t1,t2public class TACGenerator { private final StringBuilder code new StringBuilder(); private int tempCounter 0; private final StackString breakStack new Stack(); // for break private final StackString continueStack new Stack(); // for continue public String generate(Expression expr) { if (expr instanceof BinaryOp b) { String left generate(b.getLeft()); String right generate(b.getRight()); String result freshTemp(); code.append(binop ).append(result).append( ).append(left) .append( ).append(b.getOp()).append( ).append(right).append(\n); return result; } else if (expr instanceof VariableRef v) { return v.getName(); // 直接用变量名无需临时变量 } else if (expr instanceof NumberLiteral n) { return String.valueOf(n.getValue()); } else if (expr instanceof UnaryOp u) { String operand generate(u.getOperand()); String result freshTemp(); code.append(unary ).append(result).append( ) .append(u.getOp()).append( ).append(operand).append(\n); return result; } return null; } private String freshTemp() { return t tempCounter; } public void generate(Statement stmt) { if (stmt instanceof AssignmentStatement a) { String rhs generate(a.getExpression()); code.append(assign ).append(a.getVariable()).append( ).append(rhs).append(\n); } else if (stmt instanceof IfStatement i) { String cond generate(i.getCondition()); String thenLabel freshLabel(); String elseLabel freshLabel(); String endLabel freshLabel(); code.append(if ).append(cond).append( goto ).append(thenLabel).append(\n); code.append(goto ).append(elseLabel).append(\n); code.append(label ).append(thenLabel).append(:\n); for (Statement s : i.getThenBranch()) { generate(s); } code.append(goto ).append(endLabel).append(\n); code.append(label ).append(elseLabel).append(:\n); for (Statement s : i.getElseBranch()) { generate(s); } code.append(label ).append(endLabel).append(:\n); } else if (stmt instanceof WhileStatement w) { String startLabel freshLabel(); String condLabel freshLabel(); String bodyLabel freshLabel(); String endLabel freshLabel(); breakStack.push(endLabel); continueStack.push(condLabel); code.append(goto ).append(condLabel).append(\n); code.append(label ).append(startLabel).append(:\n); code.append(label ).append(bodyLabel).append(:\n); for (Statement s : w.getBody()) { generate(s); } code.append(label ).append(condLabel).append(:\n); String cond generate(w.getCondition()); code.append(if ).append(cond).append( goto ).append(bodyLabel).append(\n); code.append(label ).append(endLabel).append(:\n); breakStack.pop(); continueStack.pop(); } } private String freshLabel() { return L (tempCounter); } }关键技巧freshTemp()和freshLabel()共享tempCounter确保名字不冲突breakStack/continueStack是处理嵌套循环的关键——break生成goto breakStack.peek()continue生成goto continueStack.peek()。generate(Expression)返回String临时变量名让父节点如AssignmentStatement能直接使用避免冗余计算。4.3 函数与全局变量TAC 中的内存布局约定C89 全局变量和函数需在 TAC 中显式声明。我们约定全局变量global int x、global char buf[100]函数入口function main:参数param int argc,param char** argv局部变量alloc int a,alloc char* ppublic void generate(Program program) { // 全局变量声明 for (Declaration decl : program.getDeclarations()) { if (decl instanceof VariableDeclaration v) { code.append(global ).append(v.getType()).append( ).append(v.getName()).append(\n); } } // 函数定义 for (Declaration decl : program.getDeclarations()) { if (decl instanceof FunctionDefinition f) { code.append(function ).append(f.getName()).append(:\n); // 参数声明 for (Parameter p : f.getParameters()) { code.append(param ).append(p.getType()).append( ).append(p.getName()).append(\n); } // 局部变量分配 for (Declaration local : f.getLocalDeclarations()) { if (local instanceof VariableDeclaration v) { code.append(alloc ).append(v.getType()).append( ).append(v.getName()).append(\n); } } // 函数体 for (Statement s : f.getBody().getStatements()) { generate(s); } if (f.getReturnType() ! Type.VOID) { code.append(return ).append(0).append(\n); // 默认返回 0 } } } }注意alloc指令不指定内存地址只告诉后端“为该变量预留空间”实际栈帧布局由目标代码生成器决定。global变量在.data段alloc变量在栈上——这是 C89 内存模型的忠实映射。return指令必须存在即使void函数也生成return空操作保证控制流完整性。5. 常见问题排查那些让编译器开发者彻夜难眠的 5 个坑写编译器最痛苦的不是写不出功能而是错误信息模糊、行为不可复现、调试无从下手。以下是本项目编号 100013298在真实教学和工业验证中踩过的 5 个高频坑按“现象 → 原因 → 解决”给出可立即操作的方案。5.1 现象int a b 1;报错 “undeclared identifier b”但b明明在上一行声明了原因Parser在解析VariableDeclaration时只将变量名加入SymbolTable但未将b的Type信息同步到Symbol对象中后续analyzeExpression()调用resolve()得到Symbol但sym.getType()为null导致类型推导失败误判为未声明。解决在SemanticAnalyzer.analyzeDeclaration()中VariableDeclaration的define()必须传入已解析的Type对象而非字符串。修改SymbolTable.define()接口强制Symbol构造时绑定Type// 错误currentScope.define(v.getName(), new Symbol(v.getName())); // 正确currentScope.define(v.getName(), new Symbol(v.getType(), v.getName()));提示所有Symbol创建点变量、函数、结构体都必须传入Type否则语义分析必然断裂。5.2 现象if (x) { int y 1; } y 2;不报错但y在块外不可见原因Parser遇到{时创建了新SymbolTable但analyzeStatement(BlockStatement)中未将currentScope切换为该新表导致int y 1;的声明进入了全局表。解决SemanticAnalyzer.analyzeStatement()对BlockStatement的处理必须显式切换作用域} else if (stmt instanceof BlockStatement b) { SymbolTable blockScope new SymbolTable(currentScope); SymbolTable oldScope currentScope; // 保存旧作用域 currentScope blockScope; // 切换 for (Statement s : b.getStatements()) { analyzeStatement(s); } currentScope oldScope; // 恢复 }注意currentScope是实例变量必须在进入块前保存旧值退出块后恢复否则嵌套块会污染外层作用域。5.3 现象int* p; p x;生成 TAC 时崩溃NullPointerException原因UnaryOp节点x的getOperand()返回null因为Parser在构建UnaryOp时未正确设置operand字段或Lexer将误识别为TokenType.AMPERSAND但未在parseUnaryExpression()中处理。解决检查Parser.parseUnaryExpression()是否覆盖和*private Expression parseUnaryExpression() { if (accept(TokenType.AMPERSAND)) { // Expression operand parsePrimaryExpression(); return new UnaryOp(currentToken.getLine(), currentToken.getColumn(), , operand); } else if (accept(TokenType.STAR)) { // * Expression operand parsePrimaryExpression(); return new UnaryOp(currentToken.getLine(), currentToken.getColumn(), *, operand); } return parsePrimaryExpression(); }血泪经验UnaryOp构造时必须传入operand且operand不能为空——在parseUnaryExpression()开头加if (operand null) throw new RuntimeException(...)提前暴露问题。5.4 现象struct S { int x; }; struct S s;中s的类型解析为null原因StructType的equals()方法未重写导致SymbolTable.resolve(S)返回的Symbol与VariableDeclaration中的Type比较时返回false类型检查失败。解决为StructType实现equals()和hashCode()比较字段名和类型Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; StructType that (StructType) o; return fields.equals(that.fields); // fields 是 MapString, Type }提示所有Type子类都必须实现equals()否则isCompatibleWith()和作用域查找都会失效。5.5 现象生成的 TAC 中goto L1后无label L1:导致后端无法链接原因IfStatement的elseBranch为空时if (x) stmt;generate()未生成elseLabel对应的label指令但goto elseLabel已发出。解决在generate(IfStatement)中elseBranch为空时仍需生成labelcode.append(label ).append(elseLabel).append(:\n); if (!i.getElseBranch().isEmpty()) { for (Statement s : i.getElseBranch()) { generate(s); } } // else branch empty: just the label避坑口诀“有 goto必有 label有 label必有 goto 或代码”——TAC 的控制流图必须连通。6. 进阶技巧用 JUnit 5 驱动编译器测试构建可信赖的 CI 流水线编译器不是写完就能用的黑盒它必须像业务系统一样接受自动化测试。Java 的 JUnit 5 是最佳选择——它支持参数化测试、生命周期钩子、断言丰富且与 Maven/Gradle 深度集成。我们不测试“是否能编译”而是测试“输入 C 代码输出预期 TAC且无错误”这才是工程级可信度的基石。6.1 测试架构三层断言覆盖语法、语义、IR每个测试用例应覆盖三个层面词法/语法层输入字符串验证Lexer输出的Token序列是否匹配预期用assertArrayEquals()语义层输入字符串验证SemanticAnalyzer是否抛出预期错误assertThrowsSemanticError()或成功完成IR 层输入字符串验证TACGenerator输出的 TAC 字符串是否与黄金样本golden file完全一致assertEquals()。Nested class ArithmeticExpressionTest { Test void shouldGenerateTACForAddition() { String input int main() { int a 1; int b 2; int c a b; return c; }; Program program new Parser(new Lexer(input)).parseProgram(); new SemanticAnalyzer().analyze(program); // 必须先语义分析否则 AST 无类型 String tac new TACGenerator().generateToString(program); String expected function main: alloc int a alloc int b alloc int c assign a 1 assign p a hrefhttps://download.csdn.net/download/s1t16/88351487 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p