☰
东南大学编译原理实验:从零手搓编译器全流程实战
2026/10/1 10:55:40 网站建设 项目流程

简介:这份资源是东南大学软件学院编译原理课程实验项目的完整实现,面向正在学习编译原理、需要动手实践编译器各阶段的高校学生与自学者。它围绕词法分析、语法分析、语义分析、中间代码生成、目标代码生成与代码优化等完整流程,构建了一个从源代码到可执行代码的编译器模拟系统,帮助读者把课堂理论落到可运行的工程代码上。压缩包共28个文件,以12个java源文件和12个class编译产物为主体,另含2个txt说明、1个iml工程配置与1个md文档,整体约20KB,体量轻便,便于快速导入IDE阅读与调试。目录中可见词法分析器、语法分析器等模块划分,结构清晰,适合按阶段对照学习。目前已有63人学习下载。读者可借此理解各编译阶段的衔接方式、AST构建思路与代码优化切入点,并参考工程组织方式完成自己的课程实验,是编译原理实践环节一份可直接复用的参考实现。

1. 从零手搓编译器:东南大学软件学院编译原理实验到底在练什么

很多人第一次看到“编译原理课程实验”这几个字,脑子里浮现的是龙书里那些晦涩的自动机推导和课后习题答案。但东南大学软件学院这套实验项目的真正价值,在于它逼着你把一个完整的编译器从前到后走一遍——从词法分析、语法分析、语义分析,到中间代码生成、目标代码优化,一个环节都不能少。你写的不再是零散的算法题,而是一个能跑通“源代码进、可执行代码出”的模拟系统。这件事对新手来说门槛不低,对熟手来说却是一次难得的体系化梳理。如果你正在搜“编译原理实验怎么做”“java+编译原理怎么结合”,或者被课后习题答案折磨得头大,那这篇笔记就是给你写的。我会按实际动手的顺序,把每个阶段的选型理由、核心代码、参数设置和踩坑记录讲清楚,让你能照着复现,也能看到边界在哪。

2. 词法分析与语法分析:从字符流到语法树的落地路径

2.1 词法分析器为什么建议手写而不是直接上 Lex

很多同学第一反应是用 Flex/Lex 自动生成词法分析器,觉得省事。但东南大学这套实验的评分点往往落在“你是否理解正则表达式到 DFA 的转换过程”,直接调库反而拿不到过程分。我一般会建议手写一个基于状态转移的词法分析器,核心逻辑不超过 200 行,但能把标识符、关键字、运算符、界符、常量全部覆盖。

先定义 Token 类型和 Token 结构:

# token_types.py # 定义所有词法单元类型,方便后续语法分析引用 TOKEN_TYPES = { 'KEYWORD': 'KEYWORD', # int, if, while, return 等 'IDENTIFIER': 'IDENTIFIER', # 变量名、函数名 'NUMBER': 'NUMBER', # 整数、浮点数 'OPERATOR': 'OPERATOR', # + - * / = == != < > <= >= 'DELIMITER': 'DELIMITER', # ; , ( ) { } 'EOF': 'EOF' # 文件结束标记 } class Token: def __init__(self, type_, value, line, col): self.type = type_ # Token 类型 self.value = value # 原始字符串值 self.line = line # 所在行号,报错用 self.col = col # 所在列号,报错用 def __repr__(self): return f'Token({self.type}, {self.value!r}, line={self.line})'

逻辑说明:Token 类里保留 line 和 col 是为了后续语法分析报错时能精确定位。很多同学只存 value,结果语法错误时只能报“第某行附近有错”,调试成本翻倍。

参数说明:TOKEN_TYPES 用字典而不是枚举,是为了后续扩展方便,比如加注释类型、字符串类型时直接加键值对即可。

接下来是词法分析器主体,采用逐字符扫描加状态机的方式:

# lexer.py from token_types import Token, TOKEN_TYPES KEYWORDS = {'int', 'float', 'if', 'else', 'while', 'return', 'void'} OPERATORS = {'+', '-', '*', '/', '=', '==', '!=', '<', '>', '<=', '>='} DELIMITERS = {';', ',', '(', ')', '{', '}'} class Lexer: def __init__(self, source): self.source = source self.pos = 0 # 当前字符位置 self.line = 1 # 当前行号 self.col = 1 # 当前列号 self.tokens = [] # 结果 Token 列表 def tokenize(self): while self.pos < len(self.source): ch = self.source[self.pos] if ch in ' \t\r': self._advance() elif ch == '\n': self.line += 1 self.col = 1 self.pos += 1 elif ch.isalpha() or ch == '_': self._read_identifier() elif ch.isdigit(): self._read_number() elif ch in OPERATORS or ch in DELIMITERS: self._read_operator_or_delimiter() else: raise SyntaxError(f'非法字符 {ch!r} 在 {self.line}:{self.col}') self.tokens.append(Token('EOF', '', self.line, self.col)) return self.tokens def _advance(self): self.pos += 1 self.col += 1 def _read_identifier(self): start = self.pos while self.pos < len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] == '_'): self._advance() word = self.source[start:self.pos] token_type = 'KEYWORD' if word in KEYWORDS else 'IDENTIFIER' self.tokens.append(Token(token_type, word, self.line, self.col - len(word))) def _read_number(self): start = self.pos while self.pos < len(self.source) and self.source[self.pos].isdigit(): self._advance() # 处理小数 if self.pos < len(self.source) and self.source[self.pos] == '.': self._advance() while self.pos < len(self.source) and self.source[self.pos].isdigit(): self._advance() self.tokens.append(Token('NUMBER', self.source[start:self.pos], self.line, self.col)) def _read_operator_or_delimiter(self): ch = self.source[self.pos] # 优先匹配双字符运算符 if self.pos + 1 < len(self.source): two = self.source[self.pos:self.pos+2] if two in OPERATORS: self.tokens.append(Token('OPERATOR', two, self.line, self.col)) self.pos += 2 self.col += 2 return token_type = 'OPERATOR' if ch in OPERATORS else 'DELIMITER' self.tokens.append(Token(token_type, ch, self.line, self.col)) self._advance()

逻辑说明:_read_operator_or_delimiter里先检查双字符运算符(如==、<=),再回退到单字符,这是手写词法分析器最容易翻车的地方——如果先匹配单字符,==会被拆成两个=,语法分析直接崩。

参数说明:self.line和self.col在换行时重置,保证报错位置准确。KEYWORDS 集合建议按实验要求补全,常见的是 C 语言子集。

2.2 递归下降语法分析:用 LL(1) 把 Token 流变成 AST

语法分析阶段,东南大学实验通常要求实现递归下降分析器或 LL(1) 分析表。我建议手写递归下降,因为代码结构直接对应文法产生式,调试时能一眼看出哪条规则出了问题。先定义 AST 节点:

# ast_nodes.py class ASTNode: pass class Program(ASTNode): def __init__(self, declarations): self.declarations = declarations # 顶层声明列表 class FunctionDecl(ASTNode): def __init__(self, return_type, name, params, body): self.return_type = return_type self.name = name self.params = params self.body = body class BinaryOp(ASTNode): def __init__(self, op, left, right): self.op = op self.left = left self.right = right class NumberLiteral(ASTNode): def __init__(self, value): self.value = value class Identifier(ASTNode): def __init__(self, name): self.name = name

逻辑说明:AST 节点只存结构信息,不存 Token 位置,位置信息在语义分析阶段通过符号表关联。这样 AST 更干净,后续遍历也更快。

参数说明:FunctionDecl里的params是参数列表,每个参数包含类型和名字,建议用元组(type, name)存储。

递归下降分析器核心代码:

# parser.py from ast_nodes import * from token_types import Token class Parser: def __init__(self, tokens): self.tokens = tokens self.pos = 0 def peek(self): return self.tokens[self.pos] def consume(self, expected_type=None, expected_value=None): token = self.tokens[self.pos] if expected_type and token.type != expected_type: raise SyntaxError(f'期望 {expected_type},实际 {token.type} 在 {token.line}:{token.col}') if expected_value and token.value != expected_value: raise SyntaxError(f'期望 {expected_value!r},实际 {token.value!r} 在 {token.line}:{token.col}') self.pos += 1 return token def parse_program(self): declarations = [] while self.peek().type != 'EOF': declarations.append(self.parse_declaration()) return Program(declarations) def parse_declaration(self): # 简化处理:只支持函数声明 return_type = self.consume('KEYWORD').value name = self.consume('IDENTIFIER').value self.consume('DELIMITER', '(') params = [] while self.peek().value != ')': ptype = self.consume('KEYWORD').value pname = self.consume('IDENTIFIER').value params.append((ptype, pname)) if self.peek().value == ',': self.consume('DELIMITER', ',') self.consume('DELIMITER', ')') body = self.parse_block() return FunctionDecl(return_type, name, params, body) def parse_block(self): self.consume('DELIMITER', '{') stmts = [] while self.peek().value != '}': stmts.append(self.parse_statement()) self.consume('DELIMITER', '}') return stmts def parse_statement(self): token = self.peek() if token.value == 'return': self.consume('KEYWORD', 'return') expr = self.parse_expression() self.consume('DELIMITER', ';') return ('return', expr) elif token.type == 'IDENTIFIER': name = self.consume('IDENTIFIER').value self.consume('OPERATOR', '=') expr = self.parse_expression() self.consume('DELIMITER', ';') return ('assign', name, expr) else: raise SyntaxError(f'无法识别的语句在 {token.line}:{token.col}') def parse_expression(self): left = self.parse_term() while self.peek().value in ('+', '-'): op = self.consume('OPERATOR').value right = self.parse_term() left = BinaryOp(op, left, right) return left def parse_term(self): left = self.parse_factor() while self.peek().value in ('*', '/'): op = self.consume('OPERATOR').value right = self.parse_factor() left = BinaryOp(op, left, right) return left def parse_factor(self): token = self.peek() if token.type == 'NUMBER': self.consume('NUMBER') return NumberLiteral(token.value) elif token.type == 'IDENTIFIER': self.consume('IDENTIFIER') return Identifier(token.value) elif token.value == '(': self.consume('DELIMITER', '(') expr = self.parse_expression() self.consume('DELIMITER', ')') return expr else: raise SyntaxError(f'无法识别的表达式在 {token.line}:{token.col}')

逻辑说明:parse_expression和parse_term分层处理加减和乘除,天然实现了运算符优先级。parse_factor处理括号和原子表达式,递归下降的结构一目了然。

参数说明:consume方法支持同时校验类型和值,比如consume('KEYWORD', 'return')确保当前 Token 既是关键字又是return。这种双重校验在调试时能快速定位是词法错了还是语法错了。

3. 语义分析与中间代码生成:符号表、类型检查和四元式输出

3.1 符号表怎么设计才能同时支持作用域和类型检查

语义分析阶段最容易踩的坑是符号表设计得太简单,导致嵌套作用域里变量重名时直接覆盖。我一般用栈式符号表,每个作用域一层,进入块时压栈,退出时弹栈。

# symbol_table.py class Symbol: def __init__(self, name, type_, kind, scope_level): self.name = name self.type = type_ # int, float, void self.kind = kind # variable, function, parameter self.scope_level = scope_level class SymbolTable: def __init__(self): self.scopes = [{}] # 栈式作用域,初始全局作用域 self.current_level = 0 def enter_scope(self): self.scopes.append({}) self.current_level += 1 def exit_scope(self): if self.current_level == 0: raise RuntimeError('无法退出全局作用域') self.scopes.pop() self.current_level -= 1 def declare(self, name, type_, kind): if name in self.scopes[-1]: raise SemanticError(f'变量 {name} 在当前作用域重复声明') self.scopes[-1][name] = Symbol(name, type_, kind, self.current_level) def lookup(self, name): # 从当前作用域向外逐层查找 for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(f'未声明的标识符 {name}') def lookup_current(self, name): return self.scopes[-1].get(name)

逻辑说明:lookup从最内层作用域向外查找,符合大多数语言的名称解析规则。declare只检查当前作用域是否重复,允许内层遮蔽外层同名变量。

参数说明:scope_level记录声明时的作用域层级,后续做类型检查时可以用来判断变量是否在合法作用域内被引用。

语义分析器主体:

# semantic.py from symbol_table import SymbolTable, SemanticError from ast_nodes import * class SemanticAnalyzer: def __init__(self): self.symtab = SymbolTable() self.errors = [] def analyze(self, node): method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node): raise SemanticError(f'没有为 {node.__class__.__name__} 定义 visit 方法') def visit_Program(self, node): for decl in node.declarations: self.analyze(decl) def visit_FunctionDecl(self, node): # 函数名加入全局作用域 self.symtab.declare(node.name, node.return_type, 'function') self.symtab.enter_scope() for ptype, pname in node.params: self.symtab.declare(pname, ptype, 'parameter') for stmt in node.body: self.analyze(stmt) self.symtab.exit_scope() def visit_BinaryOp(self, node): left_type = self.analyze(node.left) right_type = self.analyze(node.right) if left_type != right_type: raise SemanticError(f'类型不匹配:{left_type} 和 {right_type} 在运算符 {node.op}') return left_type def visit_NumberLiteral(self, node): return 'int' if '.' not in node.value else 'float' def visit_Identifier(self, node): symbol = self.symtab.lookup(node.name) return symbol.type

逻辑说明:用visit_前缀加类名的方式做分发,新增 AST 节点时只需加对应方法,不用改主流程。visit_BinaryOp里做类型检查,左右类型不一致直接报错。

参数说明:errors列表用于收集多个错误后统一输出,而不是遇到第一个错误就退出,这样用户能一次性看到所有问题。

3.2 四元式中间代码生成:从 AST 到线性指令序列

中间代码生成阶段,东南大学实验通常要求输出四元式。四元式结构是(op, arg1, arg2, result),比如(+, a, b, t1)表示t1 = a + b。

# ir_generator.py from ast_nodes import * class Quadruple: def __init__(self, op, arg1, arg2, result): self.op = op self.arg1 = arg1 self.arg2 = arg2 self.result = result def __repr__(self): return f'({self.op}, {self.arg1}, {self.arg2}, {self.result})' class IRGenerator: def __init__(self): self.quads = [] self.temp_count = 0 def new_temp(self): self.temp_count += 1 return f't{self.temp_count}' def generate(self, node): method = 'gen_' + node.__class__.__name__ visitor = getattr(self, method, self.generic_gen) return visitor(node) def generic_gen(self, node): raise RuntimeError(f'没有为 {node.__class__.__name__} 定义 gen 方法') def gen_Program(self, node): for decl in node.declarations: self.generate(decl) return self.quads def gen_FunctionDecl(self, node): self.quads.append(Quadruple('func', node.name, len(node.params), None)) for stmt in node.body: self.generate(stmt) self.quads.append(Quadruple('endfunc', node.name, None, None)) def gen_BinaryOp(self, node): left = self.generate(node.left) right = self.generate(node.right) temp = self.new_temp() self.quads.append(Quadruple(node.op, left, right, temp)) return temp def gen_NumberLiteral(self, node): return node.value def gen_Identifier(self, node): return node.name

逻辑说明:gen_BinaryOp递归生成左右子表达式的代码,然后分配临时变量存放结果。临时变量命名用t1、t2递增,保证唯一性。

参数说明:temp_count是全局计数器,每次new_temp递增。如果实验要求临时变量按作用域重置,可以在gen_FunctionDecl里保存和恢复temp_count。

对于赋值语句和返回语句,需要单独处理:

def gen_assign(self, name, expr): # 赋值语句:('assign', name, expr) result = self.generate(expr) self.quads.append(Quadruple('=', result, None, name)) def gen_return(self, expr): result = self.generate(expr) self.quads.append(Quadruple('return', result, None, None))

逻辑说明:赋值语句把表达式结果直接写入变量名,返回语句生成return四元式。这两个方法在gen_FunctionDecl遍历语句时根据语句类型调用。

参数说明:四元式的result字段在赋值时是变量名,在返回时是None,后续目标代码生成阶段根据op区分处理。

4. 目标代码优化与生成:从四元式到可执行模拟指令

4.1 常量折叠与公共子表达式消除:两个必做的优化 pass

目标代码优化阶段,最基础也最容易被实验评分点覆盖的是常量折叠和公共子表达式消除。常量折叠在四元式层面做,遍历所有四元式,如果arg1和arg2都是数字,直接计算结果并替换。

# optimizer.py from ir_generator import Quadruple class Optimizer: def __init__(self, quads): self.quads = quads def constant_folding(self): new_quads = [] for quad in self.quads: if quad.op in ('+', '-', '*', '/') and self._is_number(quad.arg1) and self._is_number(quad.arg2): # 两个操作数都是常量,直接计算 result = self._compute(quad.op, quad.arg1, quad.arg2) new_quads.append(Quadruple('=', result, None, quad.result)) else: new_quads.append(quad) self.quads = new_quads return self.quads def _is_number(self, s): try: float(s) return True except (ValueError, TypeError): return False def _compute(self, op, a, b): a, b = float(a), float(b) if op == '+': return str(int(a + b)) if a + b == int(a + b) else str(a + b) if op == '-': return str(int(a - b)) if a - b == int(a - b) else str(a - b) if op == '*': return str(int(a * b)) if a * b == int(a * b) else str(a * b) if op == '/': return str(int(a / b)) if a / b == int(a / b) else str(a / b)

逻辑说明:constant_folding遍历四元式列表,遇到可折叠的表达式直接计算结果,生成一条赋值四元式。_is_number用异常捕获判断字符串是否为数字,比正则更简洁。

参数说明:_compute里对结果做整数判断,如果结果是整数值就输出整数形式,避免t1 = 3.0这种不美观的输出。

公共子表达式消除需要先做可用表达式分析,简化版可以用字典记录已计算的表达式:

def common_subexpression_elimination(self): expr_map = {} # (op, arg1, arg2) -> result_temp new_quads = [] for quad in self.quads: if quad.op in ('+', '-', '*', '/'): key = (quad.op, quad.arg1, quad.arg2) if key in expr_map: # 已经计算过,直接用之前的临时变量 new_quads.append(Quadruple('=', expr_map[key], None, quad.result)) else: expr_map[key] = quad.result new_quads.append(quad) else: new_quads.append(quad) self.quads = new_quads return self.quads

逻辑说明:expr_map以(op, arg1, arg2)为键,记录第一次计算时的结果临时变量。后续遇到相同表达式时,直接生成赋值四元式,避免重复计算。

参数说明:这个简化版没有考虑变量被重新赋值后表达式失效的情况,适合实验级别的优化。如果要更严谨,需要在赋值语句处清空expr_map中涉及该变量的条目。

4.2 目标代码生成:把四元式翻译成栈式模拟指令

目标代码生成阶段,常见做法是生成一种简单的栈式虚拟机指令,每条指令对应一个操作。指令集可以设计为PUSH、LOAD、STORE、ADD、SUB、MUL、DIV、RET、CALL。

# codegen.py class CodeGenerator: def __init__(self, quads): self.quads = quads self.instructions = [] self.var_offset = {} # 变量名 -> 栈偏移 self.next_offset = 0 def get_offset(self, name): if name not in self.var_offset: self.var_offset[name] = self.next_offset self.next_offset += 1 return self.var_offset[name] def generate(self): for quad in self.quads: self.gen_quad(quad) return self.instructions def gen_quad(self, quad): op = quad.op if op == 'func': self.instructions.append(f'FUNC {quad.arg1} {quad.arg2}') elif op == 'endfunc': self.instructions.append(f'ENDFUNC {quad.arg1}') elif op == '=': # 赋值:把 arg1 的值存入 result if self._is_number(quad.arg1): self.instructions.append(f'PUSH {quad.arg1}') else: self.instructions.append(f'LOAD {self.get_offset(quad.arg1)}') self.instructions.append(f'STORE {self.get_offset(quad.result)}') elif op in ('+', '-', '*', '/'): # 二元运算:先压左操作数,再压右操作数,然后运算 if self._is_number(quad.arg1): self.instructions.append(f'PUSH {quad.arg1}') else: self.instructions.append(f'LOAD {self.get_offset(quad.arg1)}') if self._is_number(quad.arg2): self.instructions.append(f'PUSH {quad.arg2}') else: self.instructions.append(f'LOAD {self.get_offset(quad.arg2)}') instr = {'+': 'ADD', '-': 'SUB', '*': 'MUL', '/': 'DIV'}[op] self.instructions.append(instr) self.instructions.append(f'STORE {self.get_offset(quad.result)}') elif op == 'return': if self._is_number(quad.arg1): self.instructions.append(f'PUSH {quad.arg1}') else: self.instructions.append(f'LOAD {self.get_offset(quad.arg1)}') self.instructions.append('RET') def _is_number(self, s): try: float(s) return True except (ValueError, TypeError): return False

逻辑说明:gen_quad根据四元式类型生成对应指令。二元运算先生成两个LOAD或PUSH,再生成运算指令,最后STORE结果。get_offset为每个变量分配唯一的栈偏移。

参数说明:var_offset字典在函数级别应该重置,如果实验要求支持多函数,需要在FUNC指令处保存和恢复偏移映射。

5. 避坑与排查:编译原理实验里最容易翻车的五个地方

5.1 词法分析阶段:双字符运算符被拆成两个单字符

现象:输入a == b,词法分析输出Token(OPERATOR, '=')、Token(OPERATOR, '='),语法分析报“意外的运算符”。

原因:_read_operator_or_delimiter里先匹配了单字符,没有优先检查双字符组合。

解决:在匹配单字符之前,先检查self.source[self.pos:self.pos+2]是否在 OPERATORS 集合里。如果是,消费两个字符并生成一个 Token。

5.2 语法分析阶段:左递归导致无限递归

现象:解析表达式时程序卡死或栈溢出。

原因:文法写成E -> E + T | T,递归下降分析器直接调用parse_expression会无限递归。

解决:消除左递归,改写成E -> T E',E' -> + T E' | ε。或者用循环代替递归,如parse_expression里用while处理运算符。

5.3 语义分析阶段:符号表作用域没有正确弹栈

现象:内层块声明的变量在外层块被错误引用,或者内层块退出后变量仍然可见。

原因:enter_scope和exit_scope没有成对调用,或者exit_scope在异常路径上被跳过。

解决:用try/finally确保exit_scope一定执行。或者在 AST 遍历时,进入块节点时压栈,离开时弹栈,不要依赖异常处理。

5.4 中间代码生成阶段:临时变量命名冲突

现象:两个不同的表达式生成了同名的临时变量,导致后续优化和代码生成出错。

原因:temp_count在递归生成时被重置,或者多个函数共享同一个计数器但没有隔离。

解决:temp_count作为实例变量全局递增,不要在任何地方重置。如果实验要求按函数隔离,在gen_FunctionDecl入口保存当前值,出口恢复。

5.5 目标代码生成阶段:变量偏移分配不一致

现象:同一个变量在LOAD和STORE时使用了不同的偏移,运行时取到错误的值。

原因:get_offset在两次调用之间被重置,或者变量名大小写不一致导致字典键不匹配。

解决:var_offset字典在代码生成器实例化时创建,整个生成过程不重置。变量名统一转小写或保持原样,不要混用。

6. 进阶技巧:用解释器验证目标代码的正确性

写完代码生成器后,最直接的验证方式是写一个栈式虚拟机解释器,逐条执行生成的指令,看结果是否和预期一致。这个解释器不需要太复杂,能处理PUSH、LOAD、STORE、ADD、SUB、MUL、DIV、RET就够了。

# vm.py class StackVM: def __init__(self): self.stack = [] self.vars = {} # 变量偏移 -> 值 self.pc = 0 # 程序计数器 self.instructions = [] self.return_value = None def load(self, instructions): self.instructions = instructions self.pc = 0 def run(self): while self.pc < len(self.instructions): instr = self.instructions[self.pc] parts = instr.split() op = parts[0] if op == 'PUSH': self.stack.append(float(parts[1])) elif op == 'LOAD': self.stack.append(self.vars.get(int(parts[1]), 0.0)) elif op == 'STORE': self.vars[int(parts[1])] = self.stack.pop() elif op == 'ADD': b, a = self.stack.pop(), self.stack.pop() self.stack.append(a + b) elif op == 'SUB': b, a = self.stack.pop(), self.stack.pop() self.stack.append(a - b) elif op == 'MUL': b, a = self.stack.pop(), self.stack.pop() self.stack.append(a * b) elif op == 'DIV': b, a = self.stack.pop(), self.stack.pop() self.stack.append(a / b) elif op == 'RET': self.return_value = self.stack.pop() break self.pc += 1 return self.return_value

逻辑说明:StackVM维护一个操作数栈和一个变量表。PUSH把常量压栈,LOAD从变量表取值压栈,STORE弹栈存入变量表。二元运算弹两个操作数,计算后压回结果。

参数说明:vars字典的键是变量偏移(整数),值是浮点数。如果实验要求支持整数,可以在PUSH和STORE时做类型转换。

验证流程可以写成一个端到端的测试脚本:

# test_end_to_end.py from lexer import Lexer from parser import Parser from semantic import SemanticAnalyzer from ir_generator import IRGenerator from optimizer import Optimizer from codegen import CodeGenerator from vm import StackVM source = ''' int main() { int a = 3 + 4 * 2; return a; } ''' # 1. 词法分析 tokens = Lexer(source).tokenize() print('Token 流:', tokens) # 2. 语法分析 ast = Parser(tokens).parse_program() # 3. 语义分析 SemanticAnalyzer().analyze(ast) # 4. 中间代码生成 quads = IRGenerator().generate(ast) print('四元式:', quads) # 5. 优化 optimizer = Optimizer(quads) quads = optimizer.constant_folding() quads = optimizer.common_subexpression_elimination() print('优化后四元式:', quads) # 6. 目标代码生成 instructions = CodeGenerator(quads).generate() print('目标指令:', instructions) # 7. 虚拟机执行 vm = StackVM() vm.load(instructions) result = vm.run() print('执行结果:', result) # 预期输出 11.0

逻辑说明:这个脚本把七个阶段串起来,每一步的输出都打印出来,方便定位问题。如果最终结果不对,可以从后往前逐阶段检查。

参数说明:source里的表达式3 + 4 * 2预期结果是 11,如果输出不是 11,说明优先级处理或常量折叠有问题。

我自己的习惯是每写完一个阶段就先跑这个阶段的单元测试,不要等全部写完再联调。编译原理实验的调试成本很高,一个词法错误可能导致语法分析报一堆莫名其妙的错,逐阶段验证能省下大量时间。另外,符号表和临时变量的命名规则最好在动手前就定好,中途改命名规则会让所有阶段的代码都要跟着改。希望帮到你。

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

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

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

立即咨询