news 2026/10/23 18:01:51

编译原理课程设计:从词法分析到四元式解释执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理课程设计:从词法分析到四元式解释执行

简介:《北邮编译原理课程设计》面向学习编译器构造的高校学生与自学者,以 Pascal 到目标代码的完整实现为主线,覆盖词法分析、语法分析、语义分析、符号表管理、错误处理、中间代码与代码生成等核心环节。压缩包共 83 个文件,约 123KB,包含 21 个 h 头文件、19 个 cpp 源文件、8 个 pas 示例、8 个 asm 汇编及 bin、rez、rc、vcxproj 等工程与资源文件,便于对照阅读编译流程和工程组织。已有 1195 人学习下载,适合用作课程设计参考与编译原理实践补强。读者可从中梳理 Pascal 编译器各阶段的实现思路,观察虚拟机、汇编编辑器与测试用例的配合方式,并借鉴符号表、错误报告、三地址码及优化相关设计,提升系统级编程与问题拆解能力。

1. 从字符流到四元式:一份编译原理课程设计真正要交付的流水线

某高校的编译原理课程设计,评分表上写着词法、语法、语义、中间代码四栏,但真正决定能不能按时交差的,是最后一步:源程序喂进去,四元式跑出来,屏幕上打印出正确结果。我见过太多人 FIRST/FOLLOW 算得飞快,一动手就卡在“分析表怎么变成代码”;文法写得出来,却在递归下降里被左递归撑爆调用栈。这条流水线每一段都有自己的玄学:词法的最长匹配、语法的冲突消解、语义的作用域进出、代码生成的跳转回填,任何一个环节松动一点,前面全白做。下面按我实际交付的顺序,从字符流一路拆到解释执行。

2. 词法分析:把正则压成一张可回退的转移表

词法是整条流水线里性价比最高的一段:写好了后面全顺,写歪了后面天天在调“为什么这个 token 少了半个字符”。课程设计一般不允许直接调 lex/flex 生成,要求你手写扫描器并且能讲清楚状态转移,所以选型上没什么可犹豫的——手写,但要用表驱动而不是一堆 if-else 堆出来,否则改一个运算符就要动五处代码。

2.1 表驱动和 if-else 堆砌,差别在后期改文法的时候

常见做法有两种。第一种是纯手写分支:遇到数字就一直吃数字,遇到字母就一直吃字母,遇到运算符就查一张两字符的表。这种写法前期最快,二十分钟能出活,但一旦语言规范里加了浮点数、十六进制、字符串转义,分支就会互相打架,最长匹配很容易被破坏。

第二种是状态转移表:把“当前状态 + 字符类”映射到“下一个状态”,另外维护一张“哪些状态是接受态”的表。扫描时一路走,记录最后一次落在接受态的位置,走不动了就回退到那个位置切出一个 token。这就是最长匹配(maximal munch)的标准实现,也是教材里 DFA 识别标识符那套东西的工程版本。

我一般选第二种,理由很实在:表是数据,改语言只要改表;回退逻辑只写一次,不会因为新增一个运算符就漏掉;而且答辩时能直接把表画出来,和 NFA 到 DFA 的子集构造对上。代价是表要设计得克制,别为每个字符建一列,先做字符类归并。

下面这张表是一个类 C 小语言的最小可用版本,支持整数、浮点、标识符、单字符运算符和= < > !开头的双字符运算符。

# lexer.py —— 表驱动扫描器:最长匹配 + 单步回退 from dataclasses import dataclass @dataclass class Token: kind: str # NUM / ID / OP / KW / EOF value: str line: int col: int def char_class(ch): """把 50 多个字符归并成 6 个类,状态表才不会爆炸""" if ch.isdigit(): return 'd' if ch.isalpha() or ch == '_': return 'a' if ch in ' \t\r\n': return 's' if ch in '=<>!': return 'm' # 可能组成双字符运算符的头 if ch == '.': return 'p' return 'o' # 其它单字符符号 # 状态 0 为初态;ACCEPT 记录"走到这个状态时,已识别出的 token 类型" TRANS = { 0: {'d': 1, 'a': 2, 'm': 3, 's': 0}, 1: {'d': 1, 'p': 4}, # 整数部分,遇到小数点转 4 2: {'d': 2, 'a': 2}, # 标识符 3: {'m': 33}, # 双字符运算符的第二位 4: {'d': 4}, # 小数部分 } ACCEPT = {1: 'NUM', 2: 'ID', 3: 'OP', 33: 'OP', 4: 'NUM'} # 状态 3 单独接受时只有 < > = 合法,!= 和 == 靠切片后查表兜底 LEGAL_OP = {'=', '==', '!=', '<', '<=', '>', '>='}

表建好之后,主循环只做三件事:走表、记接受点、回退切词。这里的33是刻意留的双位状态,表示“已经多吃了一个字符”,回退时不会把它当成单字符运算符。LEGAL_OP是第二道闸门,因为<<这种组合在表里也会走到 33,但它不在语言规范里,必须在切词之后拦下来,否则错误会一路传到语法分析器,报出来的位置完全对不上。

def scan(src): toks, i, line, col = [], 0, 1, 1 n = len(src) while i < n: ch = src[i] if ch in ' \t\r\n': line, col = (line + 1, 1) if ch == '\n' else (line, col + 1) i += 1 continue start, start_col = i, col state, last = 0, None while i < n: nxt = TRANS.get(state, {}).get(char_class(src[i])) if nxt is None: break state, i, col = nxt, i + 1, col + 1 if state in ACCEPT: last = (i, state) # 只记最后一个接受点 if last is None: raise SyntaxError(f'{line}:{col} 无法识别的字符 {ch!r}') i, state = last # 回退到最后一个接受位置 col = start_col + (i - start) # 列号跟着回退量修正 lexeme = src[start:i] kind = ACCEPT[state] if kind == 'OP' and lexeme not in LEGAL_OP: raise SyntaxError(f'{line}:{start_col} 非法运算符 {lexeme!r}') toks.append(Token(kind, lexeme, line, start_col)) toks.append(Token('EOF', '', line, col)) return toks

这段代码里唯一容易被忽略的参数是last。如果只保留“当前状态”,一旦走进死胡同就没法退回,123abc这种输入会直接吞成一个大 token。改成记录最后一个接受位置之后,123会在状态 1 被接受,abc从回退点重新开始扫,错误定位也能精到列。

2.2 关键字、注释和 Token 的位置信息

关键字最常见的错误做法是给每个关键字单独建一条分支。正确做法是让它们先按标识符识别,再查一张保留字哈希表:if、while、int、float这些在kind == 'ID'之后一次性替换成KW。好处是标识符状态机只有一条路径,加关键字只是往表里加一行,扫描性能不受影响。

注释则有两种处理思路。一是预处理阶段整段剥离,代码量最小,但行号会错乱,后面语法报错定位全飘;二是在状态机里加两个状态,/后面看一位,是/就吃到行尾,是*就吃到*/。我一般用第二种,因为课设的报错定位在评分里权重不低,行号飘了会被扣。

位置信息别只存行号,列号也存。原因很现实:单行里的a = b + ;这类错误,只报行号等于没报。Token里带上line和col,语法分析器报错时直接把两个都打出来,调试时间能省一大半。

2.3 错误恢复:一次报五个错比报一个错有用

新手最容易写成“遇到第一个非法字符就sys.exit()”。这在评测用例里很吃亏,因为输入文件里往往埋了好几处错误,只报一个等于只拿一处分。常见做法是:遇到无法识别的字符,跳过它,记录一条错误,继续扫下一个 token,最后统一输出错误列表。

跳过时要注意别陷入死循环——跳过之后必须让i前进至少一位。另外,错误恢复出来的 token 流可以带上一个ERR类型,语法分析器遇到ERR时直接同步到下一个分号再继续,这样一次运行就能把整份代码的问题全暴露出来,省去反复改反复跑的来回。

3. 语法分析:LL(1) 预测表与 SLR(1) 项目集,两套怎么选

语法分析是课设里最花时间的一章,也是答辩最爱问的一章。很多人卡在这里不是因为不会算 FIRST/FOLLOW,而是不知道该用哪套方法,写到一半发现文法不支持,推倒重来。

3.1 先看你的文法长什么样,再决定用 LL 还是 LR

判断标准其实很粗暴:如果语言的表达式文法是分层写好的(E → T E'、T → F T'),没有左递归、没有左公因子,那 LL(1) 递归下降是最省事的,代码短、调试直观、出错位置好定位。如果文法里到处都是左递归(E → E + T | T),或者语句部分有大量公共前缀,那 SLR(1) 更合适,它能直接吃左递归,不用改写文法,少了一层“改写之后语义对不上”的风险。

课程设计里两种都会被要求,我的建议是先写 LL(1) 把流程跑通,再补一份 SLR(1) 做对比。LL(1) 的递归下降可以直接在递归函数里顺手建 AST 和做语义动作,改起来快;SLR(1) 的价值在于让你真正理解“项目集、闭包、GOTO”这套东西,答辩时能讲清楚移进-归约冲突从哪来。

要注意一个坑:把左递归文法改写掉之后,运算符的结合性和优先级必须靠文法层次重新体现。E → T E'、E' → + T E' | ε这一套写出来,+是左结合的,但如果你把E'写成E' → T + E',就变成右结合了,1-2-3会算成1-(2-3)。这个错误在测试用例里不一定暴露,但一算长表达式就翻车。

3.2 FIRST / FOLLOW 到预测分析表,一次算对的做法

FIRST 集的迭代算法没什么好讲的,关键是两个细节:空产生式ε要单独处理,序列的 FIRST 要能“穿透”——只有当前符号可空时,才继续看下一个符号。FOLLOW 集里 90% 的错误都出在同一条规则上:对A → αBβ,如果β可空,那么FOLLOW(A)必须并入FOLLOW(B)。漏了这一步,预测表就会出现空洞,运行时抛KeyError。

# ll1.py —— FIRST / FOLLOW / 预测分析表 EPS = 'ε' G = { # 已消除左递归、提完左公因子的表达式文法 'S': [('id', '=', 'E', ';')], 'E': [('T', "E'")], "E'": [('+', 'T', "E'"), (EPS,)], 'T': [('F', "T'")], "T'": [('*', 'F', "T'"), (EPS,)], 'F': [('id',), ('num',), ('(', 'E', ')')], } NT = set(G) TERM = {s for ps in G.values() for p in ps for s in p if s not in NT and s != EPS} def first_of(seq, FIRST): """求一个符号串的 FIRST;遇到可空符号继续往后看""" out = set() for sym in seq: if sym == EPS: return out | {EPS} out |= FIRST[sym] - {EPS} if EPS not in FIRST[sym]: return out return out | {EPS} FIRST = {A: set() for A in NT} for A in NT: for p in G[A]: FIRST[A].add(p[0]) # 终结符或 ε 直接入集 while True: # 迭代到不动点 changed = False for A in NT: for p in G[A]: add = first_of(p, FIRST) - FIRST[A] if add: FIRST[A] |= add changed = True if not changed: break FOLLOW = {A: set() for A in NT} FOLLOW['S'].add('$') # 起始符的 FOLLOW 放结束符 while True: changed = False for A in NT: for p in G[A]: for k, B in enumerate(p): if B not in NT: continue rest = p[k + 1:] fs = first_of(rest, FIRST) add = fs - {EPS} if EPS in fs: # β 可空,FOLLOW(A) 并入 FOLLOW(B) add |= FOLLOW[A] if add - FOLLOW[B]: FOLLOW[B] |= add changed = True if not changed: break # 填表,冲突时立刻断言炸出来,别等运行时 M = {A: {} for A in NT} for A in NT: for p in G[A]: fs = first_of(p, FIRST) for a in fs - {EPS}: assert a not in M[A], f'LL(1) 冲突:M[{A}][{a}]' M[A][a] = p if EPS in fs: for b in FOLLOW[A]: assert b not in M[A], f'LL(1) 冲突:M[{A}][{b}]' M[A][b] = p

这段代码里first_of是复用的核心,FIRST表和FOLLOW表都靠它。参数seq传入的是产生式右部的剩余部分,空元组时返回{EPS},正好对应“到末尾了,整串可空”。断言放在建表阶段而不是分析阶段,是为了让冲突在初始化时就暴露——运行时才炸,你根本不知道是哪个非终结符出的问题。

如果断言真的炸了,先检查两件事:是不是漏了消除左递归,是不是漏了提取左公因子。LL(1) 冲突几乎只有这两个来源,剩下的属于文法本身不适合 LL(1),得换 LR。

3.3 SLR(1):闭包、GOTO 和冲突检测

SLR(1) 的代码量比 LL(1) 大,但结构非常规整:项目用(产生式编号, 点的位置)表示,闭包函数把点后面是非终结符的项目继续展开,GOTO 函数先移点再求闭包。核心就是把项目集规范族做出来,然后按 FOLLOW 集填归约项。

# slr.py —— 项目集规范族 + ACTION/GOTO(G0 为未改写的左递归文法) prods = [(A, p) for A in G0 for p in G0[A]] # 扁平化,下标即产生式编号 index = {pr: i for i, pr in enumerate(prods)} def closure(items): """点后面是非终结符,就把该非终结符的所有产生式加进来""" out, stack = set(items), list(items) while stack: pi, dot = stack.pop() _, rhs = prods[pi] if dot < len(rhs) and rhs[dot] in G0: for j, (lhs, _) in enumerate(prods): if lhs == rhs[dot] and (j, 0) not in out: out.add((j, 0)) stack.append((j, 0)) return frozenset(out) def goto(items, sym): return closure({(pi, dot + 1) for pi, dot in items if dot < len(prods[pi][1]) and prods[pi][1][dot] == sym}) states, queue = [closure({(0, 0)})], [0] trans = {} while queue: # BFS 展开规范族 s = queue.pop() syms = {prods[pi][1][dot] for pi, dot in states[s] if dot < len(prods[pi][1])} for sym in syms: t = goto(states[s], sym) if t not in states: states.append(t) queue.append(len(states) - 1) trans[(s, sym)] = states.index(t) ACTION, GOTO = {}, {} for s, st in enumerate(states): for pi, dot in st: A, rhs = prods[pi] if dot < len(rhs): # 移进项 sym = rhs[dot] if sym in G0: GOTO[(s, sym)] = trans[(s, sym)] else: ACTION[(s, sym)] = ('shift', trans[(s, sym)]) else: # 归约项 if A == 'S': ACTION[(s, '$')] = ('accept',) else: for a in FOLLOW[A]: key = (s, a) assert key not in ACTION, f'冲突 {key}' ACTION[key] = ('reduce', pi)

trans存的是状态编号,用states.index(t)查会退化成线性扫描,规模小的时候无所谓,真要做大就换成字典按冻结集合查。assert key not in ACTION是移进-归约冲突的探针,一旦触发,说明文法有二义性或者 FOLLOW 集算错了,别急着改代码,先回去看文法。

3.4 分析过程里的错误恢复

不管用哪种方法,分析过程都要能报错、能继续。LL(1) 的做法是查表失败时跳到 FOLLOW 集里的同步符号,通常选;或},丢弃当前 token 直到能对上;SLR(1) 的做法是从状态栈里弹出若干层,直到某个状态对当前 token 有合法动作。

这两种恢复都会产生“错误级联”,一次输入报出十几个错。实用技巧是设一个错误计数,报错后强制跳过至少一个 token 再继续,避免在同一个位置反复报。答辩时这属于加分项,因为能说明你真的跑过坏输入。

4. 语义与四元式:符号表、类型提升和回填链

语法树建出来只是骨架,真正让程序“能算”的是语义分析加中间代码。这一段代码量不大,但逻辑最绕,尤其是布尔表达式的回填,几乎每个人都要在这上面栽一次。

4.1 符号表:作用域链比哈希更重要

符号表有两种做法:全局一张表加作用域标记,或者每个作用域一张表、用父指针串成链。课程设计规模小,我推荐后者,因为进出作用域就是push和pop,配一个栈结构,代码短且不容易漏。

class Scope: def __init__(self, parent=None): self.parent = parent self.syms = {} # name -> (type, offset) def declare(self, name, typ): if name in self.syms: # 只查本层,允许内层遮蔽外层 raise NameError(f'重复定义:{name}') self.syms[name] = (typ, len(self.syms)) def lookup(self, name): s = self while s: # 沿作用域链一路往上找 if name in s.syms: return s.syms[name] s = s.parent raise NameError(f'未声明:{name}')

declare只查本层是刻意的:{ int a; { float a; } }在大多数语言里合法,内层遮蔽外层。如果连外层一起查,就会误报重复定义,这是新手最常写错的一行。

4.2 类型检查:把提升规则做成表,别写成 if 嵌套

表达式的类型检查本质是查一张二维表。整数和浮点混合运算往浮点提升,布尔不能参与算术,赋值两边要兼容。写成if left == 'int' and right == 'float'这种嵌套,三五个类型还行,一加数组和指针就崩了。

左操作数右操作数结果类型说明
intintint整数运算,除法截断
intfloatfloat右操作数提升
floatintfloat左操作数提升
floatfloatfloat直接运算
bool任意报错布尔不参与算术
任意void报错函数调用返回值检查

表驱动的好处是错误信息可以统一生成:cannot apply '+' to (int, void),比一堆分支里各写一句要一致得多。除法那一格要单独标注,整数除零和浮点除零的语义不同,前者在解释执行阶段应该直接抛错,后者按语言规范处理。

4.3 四元式生成与布尔表达式的回填

四元式就是(op, arg1, arg2, result)。算术和赋值直接发射,麻烦的是跳转——生成if和while的时候,条件成立要跳到哪儿,这个目标地址在生成那一刻还不知道,得等后面的代码发完再补上,这就是回填。

quads = [] def emit(op, a=None, b=None, r=None): quads.append([op, a, b, r]) return len(quads) - 1 # 返回四元式下标,作为回填句柄 def backpatch(lst, target): """把 lst 里所有待填的四元式目标地址改成 target""" for q in lst: quads[q][3] = target

emit返回下标这一点很关键:回填的时候你手里拿的不是地址,是一串下标,等目标确定了一次性刷进去。backpatch的参数target通常是len(quads),也就是“下一条要生成的指令”,这样跳转就落在正确位置。

布尔表达式要维护真假两个出口链。a < b这种简单条件,真出口就是当前四元式的下一条,假出口挂到链上;and和or则是把左半边的假出口(或真出口)接到右半边的开头。这里的顺序一旦写反,条件会永远只走一半,现象是if (a > 0 && b > 0)只判断了a,而且不报错——属于最难查的那类问题。

def gen_cond(node): """返回 (truelist, falselist),两个都是待回填的四元式下标列表""" if node.op == 'and': lt, lf = gen_cond(node.left) rt, rf = gen_cond(node.right) backpatch(lf, len(quads)) # 左边为真才继续判右边 return lt + rt, rf if node.op == 'or': lt, lf = gen_cond(node.left) rt, rf = gen_cond(node.right) backpatch(lt, len(quads)) # 左边为假才继续判右边 return rt, lf + rf # 关系运算:生成条件跳转,真假各挂一条链 t = emit('j<', node.left.name, node.right.name) f = emit('jmp') return [t], [f]

backpatch(lf, len(quads))这一句就是短路求值的落点:左边为真时直接跳到右边的第一条指令,为假时走falselist,一路跳到整个条件之外。and和or的差别只在于把哪条链接过去,但接错了整个控制流就反了。

4.4 用一个两百行的虚拟机把四元式跑起来

四元式生成完,最后一步是解释执行。写一个while循环配pc,遇到跳转就改pc,遇到运算就从环境里取值、算完写回。

def execute(quads, out=print): env, pc, step = {}, 0, 0 while pc < len(quads): step += 1 if step > 1_000_000: # 死循环保护,课设必备 raise RuntimeError('执行步数超限,疑似死循环') op, a, b, r = quads[pc] pc += 1 if op == 'label': continue elif op == 'jmp': pc = r elif op == 'jf': # 条件为假才跳 pc = r if not env.get(a, a) else pc elif op == 'print': out(env.get(a, a)) elif op == '=': env[r] = env.get(a, a) elif op in ('+', '-', '*', '/'): x, y = env.get(a, a), env.get(b, b) if op == '/' and y == 0: raise ZeroDivisionError('整数除零') env[r] = {'+': x + y, '-': x - y, '*': x * y, '/': int(x / y)}[op] else: raise ValueError(f'未知指令 {op}')

env.get(a, a)这个写法是为了同时支持变量名和字面量:变量在环境里能取到值,字面量取不到就原样返回。步数保护看着多余,但真跑起来的时候,回填错了会变成无限跳转,没有这个上限,程序就卡死在那儿,你连日志都看不到。

5. 避坑与排查:五个在凌晨两点才暴露的细节

这一节里的每一条都是血泪经验,不是理论推演。共同点是:程序不报错,或者报错位置完全误导,必须靠打印中间结果才能定位。

5.1 词法与语法交界的三个坑

坑一:<=被切成<和=。现象是if (a <= b)解析成a < (= b),语法分析报“意外的 =”。原因是扫描器没做最长匹配,遇到<就立刻返回,根本没看下一位。解决方式是记录最后一个接受位置,走不动了再回退切词,同时在切完词后校验LEGAL_OP,非法组合当场报错而不是往后传。

坑二:递归下降直接栈溢出。现象是程序刚跑就段错误,栈回溯里同一层函数刷了几百遍。原因是文法里保留了左递归(E → E + T),递归下降第一件事就是调自己,永远进不去。解决分两步:先用算法消除左递归,再检查改写后有没有破坏结合性;实在不想改,就换 SLR(1),它天然支持左递归。

坑三:预测表里出现空洞,运行时KeyError。现象是大部分输入能过,只有含空语句或空参数的用例挂。原因基本锁定在 FOLLOW 集:A → αBβ且β可空时,忘了把FOLLOW(A)并进FOLLOW(B)。定位方法是把FIRST、FOLLOW两张表打出来,和手算结果逐行比对,别靠猜。

5.2 语义与代码生成的两个坑

坑四:作用域只push不pop,内层变量泄漏到外层。现象是同一个变量名在不同块里第二次定义时报“重复定义”,或者块外能访问到块内的局部变量。原因很直白:解析到{时建了新作用域,解析到}时忘了恢复父作用域。解决办法是把作用域进出绑定在块语句的入口和出口,用try/finally保证异常路径也能pop。

坑五:短路求值的真假链接反,条件只判一半。现象最隐蔽:if (a > 0 && b > 0)在a <= 0时行为正确,a > 0时却不检查b,而且没有任何报错。原因是gen_cond里and分支回填的是lt而不是lf。排查手段是把四元式列表整段打印出来,人工对一遍跳转目标,看假出口到底落在哪。这类问题没有捷径,就是要把中间结果拉出来看。

6. 用黄金用例把流水线钉死:三层验证与一个后悔药习惯

代码写完之后,光靠“跑通一个样例”是不够的,因为很多错误只在特定输入组合下才出现。我一般准备一组固定的黄金用例,分三层验证,每层看的东西不一样。

层次观察对象失败时的典型症状
词法token 流(类型、值、行列号)运算符被拆开、关键字没识别、列号飘
语法AST 结构或分析栈轨迹结合性反了、语句漏解析、报错位置错
语义符号表快照、类型错误信息重复定义误报、作用域泄漏
中间代码四元式列表跳转目标错、临时变量复用冲突
执行程序标准输出条件只走一半、整数除法结果不对

第一层是 token 快照。把a = 1 + 2.5;这种输入扫一遍,把 token 列表按kind:value:line:col打出来,和手写的期望值做 diff。这一步能挡住绝大多数词法问题,而且成本极低。

第二层是四元式快照。挑三个典型程序:一个只有赋值和算术,一个带if-else嵌套,一个带while循环。把四元式列表完整打印,人工核对跳转目标。如果嫌麻烦,就写一个小的diff脚本,把当前输出和一份确认过的基线文件比对,改代码之后先跑它。

第三层是执行结果比对。准备一组带预期输出的用例,比如阶乘、斐波那契、冒泡排序、带短路求值的条件判断,跑完直接比 stdout。这一步最像“真测试”,但它的定位能力最弱——结果错了你只知道错了,不知道错在哪。所以顺序不能反,先过前两层,再过这一层。

下面这个脚本把三层串起来,用法就是python run_tests.py cases/,任何一个用例失败都会打印出卡在哪一层。

# run_tests.py —— 三层验证的胶水代码 import sys, pathlib def check_case(src_path): src = src_path.read_text(encoding='utf-8') toks = scan(src) # 第一层:词法 ast = parse(toks) # 第二层:语法 + 语义 quads = gen(ast) # 第三层:中间代码 got = [] execute(quads, out=got.append) # 执行,把输出收集起来 expect = src_path.with_suffix('.out').read_text(encoding='utf-8').split() assert got == expect, f'{src_path.name}: 期望 {expect},实际 {got}' ok = 0 for p in sorted(pathlib.Path(sys.argv[1]).glob('*.src')): try: check_case(p) ok += 1 except Exception as e: print(f'[FAIL] {e}') print(f'{ok} 个用例通过')

这段脚本本身没什么技术含量,价值在于把“改完代码跑一下”这件事的成本压到一条命令。课设最后两天你会反复改回填和类型检查,没有这套东西,每次改完都靠手动输入样例,一天下来能浪费几个小时。

最后一个习惯,算是我自己的后悔药:每改一处扫描器或回填逻辑,先把上面这组用例全跑一遍再继续往下写,哪怕只改了一行常量。编译前端各阶段耦合太紧,词法里一个字符类的改动,可能到你执行阶段才以“结果差一”的形式暴露出来。从那以后我每次动状态表或者跳转链,都强制走一遍三层验证,宁可贵三十秒,也不要半夜对着四元式列表数下标。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 17:39:43

Geant4光学光子仿真实战:G4OpticksTest示例编译、参数调优与避坑指南

简介&#xff1a;G4OpticksTest 是一份面向高能物理仿真与光学光子传输开发者的 C 示例工程&#xff0c;重点演示如何在 Geant4 框架内调用光学元件完成光学光子的创建与传播&#xff0c;并正确配置光学环境。它适合已具备 Geant4 基础、希望将 GPU 加速光线追踪引入探测器模拟…

作者头像 李华
网站建设 2026/10/23 17:37:56

C# Winform影院售票系统:数据库设计与并发售票实战

简介&#xff1a;这份资源是一套基于 C# WinForm 开发的影院售票管理系统完整源码&#xff0c;面向正在学习桌面应用开发、需要课程设计或毕业设计参考的初、中级开发者&#xff0c;帮助解决窗体程序模块划分、数据库连接与业务逻辑组织等实际问题。压缩包共 93 个文件&#xf…

作者头像 李华
网站建设 2026/10/23 17:24:49

VB.NET全局异常捕获实战:解决程序闪退与日志空白问题

简介&#xff1a;本资源面向VB.NET开发者&#xff0c;尤其是需要为WinForms或WPF项目构建稳定异常兜底机制的初中级程序员。它围绕全局错误与全局异常捕获这一核心问题&#xff0c;讲解如何通过订阅AppDomain.UnhandledException与Application.ThreadException事件&#xff0c;…

作者头像 李华
网站建设 2026/10/23 17:16:35

VB.NET全局异常捕获失效排查:从根因到修复的完整指南

简介&#xff1a;这份资源面向VB.NET开发者&#xff0c;尤其是需要为WinForms或WPF项目构建稳定异常处理机制的中级程序员。它系统讲解如何通过订阅AppDomain.UnhandledException与Application.ThreadException事件&#xff0c;集中捕获应用程序域和UI线程中的未处理异常&#…

作者头像 李华
网站建设 2026/10/23 16:47:46

Delphi/C++Builder 源码版 FlexCel 6.0.2.0 编译安装与实战避坑指南

简介&#xff1a;TMS FlexCel Studio v6.0.2.0 是面向 Delphi 与 Borland C Builder 开发者的表格处理库完整源码包&#xff0c;适合需要在自有应用中集成 Excel 级表格能力、又不愿依赖 Microsoft Excel 的中高级开发者。它支持 XLS/XLSX 读写、复杂公式与图表、样式格式、屏幕…

作者头像 李华