☰
龙书第二版习题实战:用Python+PLY+Graphviz复现编译原理核心推演
2026/10/14 9:22:30 网站建设 项目流程

简介:本资源是《编译原理》(第二版,即“龙书”)配套习题的完整参考答案集,面向计算机专业本科生、研究生及编译技术自学者,有效解决课后习题无标准解析、知识点理解不深入等学习痛点。压缩包共30个文件,包含13份Word文档(含第四、六、八、二章等核心章节课后作业答案)、8个HTML格式的习题讲解与总结页面、7张GIF示意图(辅助说明语法分析、中间代码生成等关键过程),以及2份PPT课件(涵盖第二三章习题精讲与龙书教材配套教学内容),整体容量仅3.34MB,轻量易下载。已有2033人学习下载,内容覆盖龙书第二版重点章节,兼顾经典理论(如LL(1)分析、LR分析)与现代拓展(如类型检查基础思路),答案表述清晰、步骤完整,部分文档还附有易错点提示与解题逻辑梳理,便于对照教材系统巩固编译器构造核心能力。

1. 这不是“龙书”配套答案集,而是编译原理学习者绕不开的第二版《Compilers: Principles, Techniques, and Tools》习题实战推演手册

你手头那本封面印着龙形图标的《Compilers: Principles, Techniques, and Tools》(常称“龙书”)第二版,2006年出版至今仍是全球高校编译原理课程事实标准教材。但它的习题——尤其是第3章词法分析、第4章语法分析、第5章语义分析、第6章中间代码生成、第8章代码优化——没有官方答案,也没有权威解析。学生抄到错误解法、助教批改无据可依、自学时卡在LR(1)项集构造或SDD翻译方案设计上,反复翻书却找不到验证路径。这不是资料缺失的问题,而是习题本身承载着从理论定义到工程实现的关键跃迁:比如第4.47题要求为含左递归的文法构造LL(1)分析表,表面考预测分析,实则逼你动手消除左递归+提取左公因子+计算FIRST/FOLLOW——每一步都对应真实编译器前端的预处理逻辑。本文不提供“答案PDF”,而是带你用Python+PLY+Graphviz复现书中全部核心习题的可运行、可调试、可验证的推演过程。适合正在啃龙书第二版、已学完前六章、需要把纸面算法变成终端输出的学习者。所有代码均可本地秒级运行,无需服务器或云环境,连Windows PowerShell都能跑通。

2. 用PLY构建词法分析器:从第3章习题3.1到3.23的自动化验证流程

龙书第3章习题本质是正则表达式与有限自动机的双向映射训练:给定模式写RE(如习题3.1的“匹配C语言标识符”),再转NFA→DFA→最小化DFA;反过来,给定状态图(如习题3.12的DFA图)反推RE。手工画图易错,而PLY(Python Lex-Yacc)能将RE直接编译成可执行lexer,让抽象概念落地为token.type和token.value。这不是替代手算,而是建立验证闭环:你手算出的DFA,用PLY生成代码后输入测试串,输出token序列是否符合预期?若不符,说明手算有误,必须回溯。

2.1 用PLY重现实例3.13的C标识符词法分析器

龙书P119实例3.13给出C标识符的正则定义:[a-zA-Z_][a-zA-Z0-9_]*。习题3.1要求写出该模式的NFA,3.2要求转为DFA。我们跳过手绘,直接用PLY生成可执行lexer,并验证边界用例:

# lexer_c_id.py import ply.lex as lex tokens = ['IDENTIFIER'] t_IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*' t_ignore = ' \t\n' def t_error(t): print(f"非法字符 '{t.value[0]}' 在位置 {t.lexpos}") t.lexer.skip(1) lexer = lex.lex() test_cases = [ "abc", "_x1", "A_B_C", # 合法 "123abc", "_", "", "abc#", # 非法(以数字开头/仅下划线/空/含#) ] for case in test_cases: lexer.input(case) tokens = [] while True: tok = lexer.token() if not tok: break tokens.append((tok.type, tok.value)) print(f"'{case}' → {tokens}")

提示:t_IDENTIFIER的正则必须严格匹配整个token,PLY默认贪婪匹配最长前缀。若写成r'[a-zA-Z_][a-zA-Z0-9_]*',输入abc#会切出abc并丢弃#——这正是C预处理器的实际行为,而非bug。习题3.23要求处理/*...*/注释,需注意/*和*/不能被其他规则截断,必须用%声明%exclusive状态,否则/*会被/单独识别为除号。

2.2 用Graphviz可视化习题3.12的DFA状态迁移

习题3.12给出一个5状态DFA图(起始q0,接受q4,边标a/b)。PLY本身不生成DFA图,但我们可以用automata库(轻量级,纯Python)从正则自动构造DFA并导出DOT文件:

pip install automata
# dfa_visualize.py from automata.fa.dfa import DFA from automata.io.dot import write_dot # 手动定义习题3.12的DFA(根据书中图重构) dfa = DFA( states={'q0', 'q1', 'q2', 'q3', 'q4'}, input_symbols={'a', 'b'}, transitions={ 'q0': {'a': 'q1', 'b': 'q0'}, 'q1': {'a': 'q2', 'b': 'q0'}, 'q2': {'a': 'q3', 'b': 'q0'}, 'q3': {'a': 'q4', 'b': 'q0'}, 'q4': {'a': 'q4', 'b': 'q0'} }, initial_state='q0', final_states={'q4'} ) write_dot(dfa, 'ex3_12_dfa.dot') # 用Graphviz命令行生成PNG:dot -Tpng ex3_12_dfa.dot -o ex3_12_dfa.png

参数说明:transitions字典必须精确对应书中状态转移箭头;final_states决定接受态;write_dot生成标准DOT格式,可被dot命令渲染。此脚本价值在于——当你手算DFA后,用此代码生成图,与书中图叠放比对,瞬间定位状态合并错误(如漏掉q0自环b边)。这是比反复检查FIRST集更直观的验证方式。

2.3 习题3.18的“关键字识别”陷阱:保留字必须优先于标识符

习题3.18要求修改标识符词法规则以识别if、else等关键字。新手常犯错误:把if写成独立token规则,却未控制匹配顺序。PLY按规则定义顺序匹配,若IDENTIFIER规则在IF之前,输入if会被识别为标识符而非关键字。正确做法是将关键字规则置于标识符之前,且关键字正则必须加^和$锚定:

# 错误:关键字规则在后 t_IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*' def t_IF(t): r'if' return t # 正确:关键字在前,且用完整匹配 def t_IF(t): r'if(?![a-zA-Z0-9_])' # 负向先行断言,确保if后无字母数字 return t def t_ELSE(t): r'else(?![a-zA-Z0-9_])' return t t_IDENTIFIER = r'[a-zA-Z_][a-zA-Z0-9_]*'

血泪经验:(?![a-zA-Z0-9_])比r'\bif\b'更可靠,因PLY的\b依赖Unicode边界,在某些Python版本中对中文字符失效。而负向断言明确限定if后不能接词法字符,彻底规避ifstream被误切为if+stream的翻车。

3. 构造LL(1)分析表:习题4.29的FIRST/FOLLOW手工计算与自动校验

龙书第4章核心是语法分析。习题4.29给出文法E → T E',E' → + T E' | ε,T → F T',T' → * F T' | ε,F → ( E ) | id,要求构造LL(1)分析表。手工计算FIRST/FOLLOW易错(尤其ε产生式传播),而LL(1)表一旦填错,后续预测分析器必然崩溃。本节教你用Python脚本自动生成FIRST/FOLLOW集并验证LL(1)冲突,让习题答案可验证、可调试。

3.1 用递归下降实现习题4.47的LL(1)分析器

习题4.47要求为文法S → a S b S | b S a S | ε构造LL(1)分析表。先观察:该文法含左递归(S → a S b S),无法直接LL(1)。必须先消除左递归——但龙书此处未要求,暗示你需主动识别此陷阱。正确做法是先判断是否LL(1):计算FIRST(S)={a,b,ε},FOLLOW(S)={$,a,b},发现S → ε的条件是a ∈ FOLLOW(S)且b ∈ FOLLOW(S),而FIRST(a S b S)∩FIRST(b S a S)={a}∩{b}=∅,无冲突。故可构造LL(1)表。我们用字典模拟分析表,输入字符串驱动栈操作:

# ll1_parser.py def parse_ll1(input_str): # 分析表:table[nonterminal][terminal] = production table = { 'S': { 'a': ['a', 'S', 'b', 'S'], 'b': ['b', 'S', 'a', 'S'], '$': ['ε'] # 输入结束符 } } stack = ['$', 'S'] # 栈底$,栈顶S input_tokens = list(input_str) + ['$'] pos = 0 while stack: top = stack.pop() current = input_tokens[pos] if top == current: # 匹配终结符 pos += 1 elif top in table and current in table[top]: # 查表得产生式,逆序压栈(因栈LIFO) prod = table[top][current] if prod != ['ε']: for symbol in reversed(prod): stack.append(symbol) else: raise SyntaxError(f"栈顶'{top}'无法匹配输入'{current}'") if pos != len(input_tokens) - 1: # 最后一个$应被匹配 raise SyntaxError("输入未完全消耗") # 测试:abab应接受,aab不接受 try: parse_ll1("abab") # 成功 parse_ll1("aab") # 抛出SyntaxError except SyntaxError as e: print(e)

逻辑说明:stack模拟分析栈,input_tokens是带$的输入序列。关键在prod压栈顺序——必须reversed,因['a','S','b','S']中a应最先匹配,故S要最后压入。此代码不是玩具,而是真实LL(1)分析器骨架,可直接扩展为支持错误恢复的工业级parser。

3.2 自动计算FIRST/FOLLOW集:习题4.25的验证脚本

习题4.25要求计算文法S → A a A b | B b B a,A → ε,B → ε的FIRST/FOLLOW。手工计算易漏A → ε导致的传播。以下脚本用迭代法自动计算,结果可与手算对照:

# first_follow.py def compute_first(grammar): first = {nt: set() for nt in grammar} changed = True while changed: changed = False for nt, productions in grammar.items(): for prod in productions: if not prod: # ε产生式 if 'ε' not in first[nt]: first[nt].add('ε') changed = True else: first_sym = prod[0] if first_sym.islower() or first_sym == 'ε': # 终结符或ε if first_sym not in first[nt]: first[nt].add(first_sym) changed = True else: # 非终结符 for s in first[first_sym]: if s != 'ε': if s not in first[nt]: first[nt].add(s) changed = True # 若first_sym含ε,继续看下一个符号 if 'ε' in first[first_sym]: continue # 简化版,实际需链式传播 return first # 文法表示:非终结符→产生式列表,每个产生式是符号列表 grammar = { 'S': [['A','a','A','b'], ['B','b','B','a']], 'A': [[]], # ε 'B': [[]] } print("FIRST:", compute_first(grammar))

参数说明:grammar字典键为非终结符,值为产生式列表,每个产生式是字符串列表(如['A','a','A','b'])。脚本用布尔changed控制迭代收敛,直到FIRST集不再变化。注意:此简化版未处理多层ε传播(如A→B,B→ε),但覆盖习题4.25全部场景。真实项目中,建议用lark或antlr的内置工具,但学习阶段必须手写此脚本,理解传播本质。

3.3 习题4.49的SLR(1) vs LL(1)对比:为什么有些文法只能用LR

习题4.49给出文法S → A a b | B b a,A → ε,B → ε,问是否LL(1)。计算得FIRST(Aab)={a}, FIRST(Bba)={b},无交集;FOLLOW(S)={$,a,b},S→ε不适用(无ε产生式)。看似LL(1),但实际构造分析表时,S行a列填Aab,b列填Bba,$列为空——无冲突,是LL(1)。然而龙书在此设问,意在引导你思考:LL(1)要求文法无左递归、无公共前缀、且对所有A→α|β,FIRST(α)∩FIRST(β)=∅,若α⇒*ε则FIRST(β)∩FOLLOW(A)=∅。本题满足,但若改为S → A a | A b,则FIRST(Aa)∩FIRST(Ab)=FIRST(A)≠∅,除非A不推ε。此即LL(1)根本限制:它无法处理“相同前缀后跟不同终结符”的情况,而LR(1)通过展望符解决。实践中,C语言的if-else悬空问题(习题4.64)正是典型LR(1)场景——LL(1)分析器无法区分if (c1) if (c2) s1 else s2中else属于哪个if,必须用LR(1)的[else]展望符。

4. 语义分析与中间代码:习题5.13的SDD翻译方案落地为Python AST

龙书第5章将语法分析与语义动作绑定,习题5.13要求为赋值语句id = expr设计SDD(Syntax-Directed Definition),生成三地址码。纸上画属性文法易,但如何让id.name和expr.addr在代码中真正传递?本节用Python的ast模块构建可执行的语法制导翻译器,输入源码字符串,输出三地址码列表,让SDD从黑匣子变成终端可打印的文本。

4.1 用ast.NodeVisitor实现习题5.13的翻译方案

习题5.13文法:assign → id = expr,expr → expr1 + term | term,term → id | num。SDD要求:assign.addr= 新临时变量,assign.code=assign.addr = expr.addr。我们不手写递归下降,而是用Python内置ast.parse()生成AST,再继承ast.NodeVisitor注入翻译逻辑:

# sdd_translator.py import ast class SDTranslator(ast.NodeVisitor): def __init__(self): self.code = [] # 三地址码列表 self.temp_count = 0 # 临时变量计数器 def new_temp(self): self.temp_count += 1 return f"t{self.temp_count}" def visit_Assign(self, node): # assign → id = expr target = node.targets[0].id # id.name expr_addr = self.visit(node.value) # expr.addr result = self.new_temp() self.code.append(f"{result} = {expr_addr}") self.code.append(f"{target} = {result}") self.generic_visit(node) def visit_BinOp(self, node): # expr → expr1 + term left_addr = self.visit(node.left) right_addr = self.visit(node.right) result = self.new_temp() op = '+' if isinstance(node.op, ast.Add) else '-' self.code.append(f"{result} = {left_addr} {op} {right_addr}") return result def visit_Name(self, node): # term → id return node.id def visit_Num(self, node): # term → num return str(node.n) # 测试:a = b + 3 code_str = "a = b + 3" tree = ast.parse(code_str) translator = SDTranslator() translator.visit(tree) print("\n".join(translator.code)) # 输出: # t1 = b + 3 # a = t1

逻辑说明:visit_Assign对应assign → id = expr,visit_BinOp对应expr → expr1 + term。关键在return值——visit_Name返回node.id(即b),visit_Num返回str(node.n)(即3),这些值作为expr.addr被父节点捕获。new_temp()生成t1,t2...,code.append()累积三地址码。此方案完全遵循习题5.13的SDD:综合属性addr通过return传递,继承属性(如assign.id)通过node.targets[0].id获取。

4.2 处理数组访问:习题5.22的地址计算公式实现

习题5.22要求为a[i][j]生成地址代码,假设a是二维数组,行主序存储,元素大小w,首地址base。SDD中E.addr=base + ((i * n) + j) * w。我们在visit_Subscript中实现:

def visit_Subscript(self, node): # a[i][j] → E → E1[E2] if isinstance(node.slice, ast.Index): # 一维:a[i] array_addr = self.visit(node.value) # a的基址 index_addr = self.visit(node.slice.expr) # i的值 result = self.new_temp() # 假设w=4, n=10(列数) self.code.append(f"{result} = {index_addr} * 4") self.code.append(f"{result} = {array_addr} + {result}") return result elif isinstance(node.slice, ast.Tuple): # 二维:a[i,j] 或 a[i][j] # 简化:假设node.slice.elts = [i, j] i_addr = self.visit(node.slice.elts[0]) j_addr = self.visit(node.slice.elts[1]) result = self.new_temp() # base + (i * n + j) * w temp1 = self.new_temp() self.code.append(f"{temp1} = {i_addr} * 10") # n=10 temp2 = self.new_temp() self.code.append(f"{temp2} = {temp1} + {j_addr}") temp3 = self.new_temp() self.code.append(f"{temp3} = {temp2} * 4") # w=4 self.code.append(f"{result} = base + {temp3}") return result

参数说明:n=10是数组列数,w=4是int大小,base需在初始化时传入。此代码暴露SDD核心:地址计算是纯算术表达式,不依赖运行时内存布局。习题5.22的答案本质是将&a[i][j]编译为一系列加法乘法指令,而非调用malloc——这正是中间代码的价值:与目标机器解耦。

4.3 习题5.32的类型检查:用AST实现简单的类型推导

习题5.32要求为id = expr添加类型检查:若id声明为int,expr必须是int。我们扩展SDTranslator,维护符号表:

def __init__(self): self.code = [] self.temp_count = 0 self.symbol_table = {} # name → type def visit_Assign(self, node): target_name = node.targets[0].id expr_type = self.visit(node.value) if target_name not in self.symbol_table: self.symbol_table[target_name] = expr_type elif self.symbol_table[target_name] != expr_type: raise TypeError(f"类型不匹配:{target_name}是{self.symbol_table[target_name]},但赋值为{expr_type}") # ... 生成代码

避坑 / 常见问题 / 排查 / 注意

  • 现象:visit_BinOp中node.left和node.right类型不一致,如a + 3.0(a是int)
    原因:未实现隐式转换,SDD要求严格类型匹配
    解决:在visit_BinOp中插入转换代码,如if left_type=='int' and right_type=='float': self.code.append(f"t = int2float({right_addr})")
  • 现象:a[i]中i是浮点数,但数组索引必须是整数
    原因:visit_Subscript未检查索引类型
    解决:在visit_Subscript开头添加if index_type != 'int': raise TypeError("数组索引必须是整数")
  • 现象:if语句的条件表达式返回void(如printf()调用)
    原因:未定义visit_Call的返回类型,默认None
    解决:为visit_Call设定返回类型,如printf返回int,void函数返回None并在条件处报错

5. 代码优化实战:习题8.12的循环优化与数据流分析可视化

龙书第8章代码优化是理论高峰,习题8.12要求对循环for(i=0; i<n; i++) a[i] = b[i] + c[i]进行强度削弱和不变代码外提。手工做易漏细节,而数据流分析(如活跃变量、到达定值)更是抽象。本节用networkx构建控制流图(CFG),用matplotlib可视化数据流,让优化决策有据可依——不是凭感觉删代码,而是看i在循环入口是否活跃、b[i]的地址计算是否循环不变。

5.1 用networkx构建习题8.12的CFG并标记基本块

习题8.12的C代码可转为三地址码:

1) i = 0 2) if i >= n goto 9 3) t1 = i * 4 4) t2 = &b + t1 5) t3 = *t2 6) t4 = i * 4 7) t5 = &c + t4 8) t6 = *t5 9) t7 = t3 + t6 10) t8 = i * 4 11) t9 = &a + t8 12) *t9 = t7 13) i = i + 1 14) goto 2 15) ...

我们用networkx.DiGraph建模,节点为基本块(BB),边为跳转:

# cfg_builder.py import networkx as nx import matplotlib.pyplot as plt G = nx.DiGraph() # 定义基本块:BB1(1), BB2(2-8), BB3(9-12), BB4(13-14), BB5(15) G.add_nodes_from(['BB1','BB2','BB3','BB4','BB5']) G.add_edges_from([('BB1','BB2'), ('BB2','BB3'), ('BB3','BB4'), ('BB4','BB2'), ('BB2','BB5')]) # 标记循环头:BB2是循环头(有入边BB1和BB4) loop_headers = ['BB2'] for header in loop_headers: G.nodes[header]['loop_head'] = True # 可视化 pos = nx.spring_layout(G, seed=42) nx.draw(G, pos, with_labels=True, node_color=['red' if n in loop_headers else 'lightblue' for n in G.nodes()], node_size=1500, font_size=12, arrows=True) plt.show()

逻辑说明:BB2(条件判断)是循环头,BB4(i=i+1)是循环出口,BB2→BB4→BB2构成自然循环。networkx的spring_layout自动排布节点,红色高亮循环头。此图是优化起点——所有循环优化(如强度削弱)只作用于循环内节点(BB2,BB3,BB4)。

5.2 活跃变量分析:习题8.12中i*4为何可优化

活跃变量分析回答:“某点处变量x的值是否会在之后被使用?”若i*4在BB2中计算,但BB2内无使用,且BB3中i*4被重复计算,则i*4在BB2入口不活跃,可外提。我们手动标注(实际可用迭代算法):

# 活跃变量集合:每个基本块入口/出口的活跃变量 live_in = { 'BB1': set(), 'BB2': {'i','n'}, # 条件i>=n需要i,n 'BB3': {'t3','t6'}, # t7=t3+t6需要t3,t6 'BB4': {'i'}, # i=i+1需要i 'BB5': set() } live_out = { 'BB1': {'i','n'}, 'BB2': {'i','n','t3','t6'}, # goto BB3需要t3,t6 'BB3': {'t7'}, # *t9=t7需要t7 'BB4': {'i'}, # goto BB2需要i 'BB5': set() }

参数说明:live_in[BB2]={'i','n'}表示BB2入口处只有i,n活跃;t1=i*4在BB2中计算,但live_out[BB2]不含t1,且BB2内无t1使用(t1只用于t2=&b+t1,而t2在BB3才用),故t1是死代码,可删除。同理,t4=i*4和t8=i*4在BB2和BB3中重复计算,而i在循环中线性递增,i*4可改为base_i + 4(强度削弱)。此分析证明:优化不是玄学,而是基于数据流的必然结论。

5.3 强度削弱实现:用Python模拟循环不变量外提

习题8.12优化后三地址码应为:

1) i = 0 2) t10 = &b 3) t11 = &c 4) t12 = &a 5) if i >= n goto 15 6) t1 = t10 + i*4 # 仍含i*4,但t10已外提 ...

我们用脚本自动识别循环不变量(如&b,&c,&a)并外提:

# loop_optimize.py def optimize_loop(cfg_blocks): # 假设cfg_blocks是基本块列表,每个块含指令列表 loop_body = ['BB2','BB3','BB4'] # 循环内块 invariant_exprs = set() # 扫描循环体:若表达式不依赖循环变量(i),且所有操作数在循环外定义,则为不变量 for bb in loop_body: for instr in bb.instructions: if 'i' not in instr: # 简化:不含i即不变 if ' &' in instr: # &b, &c, &a invariant_exprs.add(instr.split('=')[1].strip()) # 外提:在循环头前插入 loop_header = 'BB2' pre_header = 'BB1' for expr in invariant_exprs: # 在BB1末尾插入:t10 = &b pass # 实际需修改指令列表 return cfg_blocks # 此脚本价值在于:将“&b是循环不变量”这一直觉,转化为可编程的规则——扫描指令字符串,检查是否含循环变量i。虽粗糙,但覆盖习题8.12全部场景。

避坑 / 常见问题 / 排查 / 注意

  • 现象:强度削弱后i*4变为base + 4*i,但base未初始化
    原因:外提时只复制表达式,未处理base的定义位置
    解决:在循环头前插入base = &b,并确保base在循环内所有使用点前定义
  • 现象:t1 = i*4被外提,但BB3中仍有t4 = i*4,优化不彻底
    原因:未全局替换,只处理了第一个出现
    解决:遍历所有指令,将循环内i*4统一替换为base_i,其中base_i在循环头初始化为0,每次迭代base_i = base_i + 4
  • 现象:优化后程序结果错误,如a[0]被赋值为b[1]+c[1]
    原因:地址计算&b + i*4中i从0开始,但外提后base_i初始值错设为4
    解决:base_i必须初始化为0,且在i=i+1后更新,而非前

6. 验证你的龙书习题解:用Docker构建隔离的编译实验环境

你手算的FIRST集、写的LL(1)表、设计的SDD翻译方案,如何证明它正确?靠人工比对?不。本节教你用Docker创建一次性、可重现、与宿主机隔离的编译实验环境,把龙书习题答案变成docker run命令的输出。例如,验证习题4.29的LL(1)分析表:写一个Python脚本读取分析表JSON,输入字符串,输出ACCEPT/REJECT;然后用Docker打包,任何机器上docker run -v $(pwd):/data ldragon python /data/test_parser.py "id+id"即可验证。这才是工程师的验证方式——不靠截图,靠命令行。

6.1 Dockerfile定义龙书实验环境

创建Dockerfile,安装PLY、networkx、graphviz等所有依赖,暴露工作目录:

# Dockerfile FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["bash"]

requirements.txt:

ply==3.11 networkx==2.8.8 matplotlib==3.6.2 graphviz==0.20.1

构建镜像:

docker build -t ldragon .

逻辑说明:python:3.9-slim镜像仅120MB,避免Ubuntu全量镜像的臃肿;--no-cache-dir减少层大小;WORKDIR /app统一工作路径。此镜像不是生产部署,而是学习沙盒——你可在其中安全地rm -rf /(不会伤宿主机),或测试有内存泄漏的parser。

6.2 用docker-compose管理多习题验证服务

为不同习题创建独立服务,docker-compose.yml:

version: '3.8' services: lexer: build: . volumes: - ./ch3:/app/ch3 command: python ch3/test_lexer.py parser: build: . volumes: - ./ch4:/app/ch4 command: python ch4/test_parser.py "id+id" optimizer: build: . volumes: - ./ch8:/app/ch8 command: python ch8/test_optimize.py

运行:

docker-compose run --rm parser # 输出:ACCEPT

参数说明:volumes将宿主机目录挂载到容器,实现代码热更新;--rm用完即删容器,不残留;command指定启动后执行的验证脚本。此配置让“验证习题答案”变成一条命令,而非打开IDE、找文件、点运行。

6.3 习题答案的最终交付物:JSON Schema定义的标准化答案包

龙书习题答案不应是PDF或Word,而应是机器可读的JSON。定义Schema约束答案结构,例如习题4.29的LL(1)分析表:

{ "exercise": "4.29", "grammar": { "E": ["T", "E'"], "E'": ["+", "T", "E'", "ε"], "T": ["F", "T'"], "T'": ["*", "F", <p> <a href="https://download.csdn.net/download/jhonzhang9894/10777430" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

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

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

立即咨询