news 2026/10/10 20:43:15

手写语法分析器:从LL(1)文法到可执行解析器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写语法分析器:从LL(1)文法到可执行解析器

简介:本资源是一份面向计算机专业本科生及编译原理初学者的语法分析实践教学材料,聚焦编译器核心环节——语法分析的原理理解与代码实现。内容覆盖上下文无关文法定义、LL/LR分析对比、递归下降与Yacc/Bison工具应用、抽象语法树构建及语法错误处理等关键知识点,兼顾理论梳理与工程落地。压缩包共4个文件(453KB),含C++源码(语法分析.cpp)用于手写解析器实现、可执行程序(语法分析.exe)支持快速验证、实验报告文档(语法分析.docx)详述设计思路与测试用例、辅助说明文本(file_out.txt)记录运行输出,结构精炼、即下即用。已有3106人学习下载,适合课程实验复现、期末项目参考或编译器开发入门实践,助读者打通从文法定义到AST生成的完整技术链路。

1. 为什么写“语法分析实验报告(含代码)”不是交作业,而是练出编译器工程的底层手感?

你手头这份《语法分析实验报告(含代码)》,大概率来自编译原理课的课程设计——但别急着把它当应付学分的文档扔进回收站。它其实是你第一次亲手把「文法→推导→树→动作」这条链路从纸面拽进内存的真实切口:不是调用现成 parser generator,而是用递归下降或 LL(1) 手写一个能读入a + b * c并输出带节点标记的 AST 的解析器;不是只画个分析表就收工,而是让程序真正在终端里报出Error at line 3, col 5: expected ')' but got ';'。这类实验的核心价值,从来不在“报告格式是否规范”,而在于它强制你直面词法与语法的边界、冲突消解的代价、错误恢复的妥协点——这些正是你在后续做 DSL 设计、配置文件校验、前端模板编译、甚至自研规则引擎时,绕不开的黑匣子。如果你正卡在“能看懂 Dragon Book 却写不出可运行的 parser”,或者想把 YAML/JSON Schema 验证逻辑下沉到更可控的层级,这份实验就是最薄、最硬、最不骗人的第一块垫脚石。


2. 从文法定义到可执行解析器:递归下降法的完整落地路径

递归下降是语法分析实验中最常被选用的方法——它不依赖外部工具,代码结构与文法直接对应,调试直观,且能自然嵌入语义动作。但“手写递归下降”绝不是照抄教材伪码就能跑通的事。下面以经典的算术表达式文法为例,拆解从文法书写到可运行代码的每一步真实操作。

2.1 文法设计:先砍掉左递归,再确认 FIRST/FOLLOW 集可用性

实验中常见的错误起点,是直接拿教科书上的 E → E + T | T 这类左递归文法开干。这会导致无限递归,必须先改写。我们采用标准消除法,得到无左递归、适合递归下降的等价文法:

E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id | num

提示:这个改写不是为了“看起来更学术”,而是为后续预测分析铺路。E' 和 T' 的 ε 产生式意味着它们对应函数必须能接受空输入——这直接影响你代码里if lookahead in FIRST(E')的判断逻辑,也决定了错误恢复点的位置。

验证该文法是否满足 LL(1) 条件,关键看每个非终结符的产生式右部 FIRST 集是否两两不相交,且若含 ε,则该非终结符的 FOLLOW 集与其它 FIRST 集也不相交。手动计算得:

  • FIRST(E') = { '+', ε }
  • FOLLOW(E') = { ')', '$' }($ 表示输入结束)
  • FIRST(T') = { '*', ε }
  • FOLLOW(T') = { '+', ')', '$' }

所有交集为空,文法合法。注意:这步不能跳过。很多同学后期 parser 总在某处卡死或误报错,根源就是文法本身不满足 LL(1),却强行用递归下降硬套。

2.2 词法扫描器(Lexer):用正则预编译,拒绝手写状态机

语法分析器的输入不是原始字符串,而是 Token 流。实验里最容易被低估的环节,就是词法扫描器的质量。别用str.split()或简单if s.startswith('if')——这会毁掉整个语法分析的健壮性。

我一般用 Python 的re模块预编译一组带命名组的正则,按优先级顺序匹配:

import re TOKEN_SPEC = [ ('NUMBER', r'\d+(\.\d*)?'), # 整数或浮点数 ('ID', r'[a-zA-Z_]\w*'), # 标识符 ('OP_ADD', r'\+'), ('OP_MUL', r'\*'), ('LPAREN', r'\('), ('RPAREN', r'\)'), ('WS', r'\s+'), # 空白,跳过 ('MISMATCH', r'.'), # 兜底,报错用 ] # 编译为单个正则,用命名组区分类型 tok_regex = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPEC) get_token = re.compile(tok_regex).match class Lexer: def __init__(self, text): self.text = text self.pos = 0 self.current_token = None self.next_token() def next_token(self): while self.pos < len(self.text): mo = get_token(self.text, self.pos) if not mo: raise SyntaxError(f'Unexpected character {self.text[self.pos]} at position {self.pos}') kind = mo.lastgroup value = mo.group() self.pos = mo.end() if kind == 'WS': # 跳过空白 continue if kind == 'MISMATCH': raise SyntaxError(f'Illegal character {value} at position {self.pos - len(value)}') self.current_token = (kind, value) return self.current_token = ('EOF', '')

参数说明:

  • TOKEN_SPEC中正则顺序即匹配优先级,WS放中间靠后,确保+不被当作+123的一部分;
  • MISMATCH是兜底项,捕获所有未定义字符,避免静默失败;
  • next_token()内部循环跳过空白,使语法分析器完全不用处理空格逻辑——这是解耦的关键。

2.3 递归下降解析器:函数即非终结符,return 即推导成功

每个非终结符对应一个函数,函数名即非终结符名(如parse_E,parse_T),函数体即该非终结符的产生式展开。核心原则:当前函数只消费它能确定的 Token,绝不回溯,绝不猜测。

class Parser: def __init__(self, lexer): self.lexer = lexer self.current_token = lexer.current_token def eat(self, token_type): """消费指定类型的 Token,失败则抛异常""" if self.current_token[0] == token_type: self.lexer.next_token() self.current_token = self.lexer.current_token else: raise SyntaxError( f'Expected {token_type}, got {self.current_token[0]} ' f'at position {self.lexer.pos - len(self.current_token[1])}' ) def parse_E(self): """E → T E'""" node = {'type': 'E', 'children': []} node['children'].append(self.parse_T()) node['children'].append(self.parse_Eprime()) return node def parse_Eprime(self): """E' → + T E' | ε""" if self.current_token[0] == 'OP_ADD': node = {'type': 'Eprime', 'children': []} self.eat('OP_ADD') node['children'].append(self.parse_T()) node['children'].append(self.parse_Eprime()) return node else: # 匹配 ε,返回空节点或 None,由上层决定如何处理 return {'type': 'Eprime', 'children': [], 'epsilon': True} def parse_T(self): """T → F T'""" node = {'type': 'T', 'children': []} node['children'].append(self.parse_F()) node['children'].append(self.parse_Tprime()) return node def parse_Tprime(self): """T' → * F T' | ε""" if self.current_token[0] == 'OP_MUL': node = {'type': 'Tprime', 'children': []} self.eat('OP_MUL') node['children'].append(self.parse_F()) node['children'].append(self.parse_Tprime()) return node else: return {'type': 'Tprime', 'children': [], 'epsilon': True} def parse_F(self): """F → ( E ) | id | num""" if self.current_token[0] == 'LPAREN': self.eat('LPAREN') node = self.parse_E() self.eat('RPAREN') return {'type': 'F', 'children': [node], 'is_paren': True} elif self.current_token[0] in ('ID', 'NUMBER'): token = self.current_token self.eat(token[0]) return {'type': 'F', 'value': token[1], 'token_type': token[0]} else: raise SyntaxError(f'Expected ID or NUMBER or LPAREN, got {self.current_token[0]}') # 使用示例 if __name__ == '__main__': code = "a + 2 * (b - c)" lexer = Lexer(code) parser = Parser(lexer) try: ast = parser.parse_E() print("Parse success. AST:", ast) except SyntaxError as e: print("Parse error:", e)

逻辑说明:

  • eat()是原子操作,保证 Token 消费的确定性;
  • 每个parse_*函数返回 AST 片段(字典结构),便于后续语义分析或解释执行;
  • ε 产生式用带'epsilon': True的空节点表示,比返回None更利于统一遍历;
  • 错误信息包含精确位置(self.lexer.pos - len(...)),这是调试阶段救命的细节。

3. LL(1) 分析表驱动法:从手写函数到查表跳转的范式切换

当文法变大、产生式增多,递归下降的手动编码维护成本急剧上升。此时,LL(1) 分析表驱动法成为更工程化的选择——它把文法逻辑从代码中抽离,固化为一张二维表,解析器退化为纯粹的栈+查表机。这不仅是“换种写法”,更是理解预测分析本质的必经之路。

3.1 构建 FIRST/FOLLOW 集:用集合运算代替人肉枚举

对中等规模文法(如含 if/while/assign 的小语言),手动计算 FIRST/FOLLOW 极易出错。我习惯用 Python 写一个通用计算脚本,输入文法字符串,输出各非终结符的 FIRST 和 FOLLOW 集:

from collections import defaultdict, deque def compute_first_sets(grammar): """ grammar: dict, key=nonterminal, value=list of productions (each prod is list of symbols) Returns: dict mapping nonterminal -> set of terminals """ first = defaultdict(set) # 初始化:终结符的 FIRST 就是自身 for nt in grammar: first[nt] = set() # 终结符不参与计算,假设已知所有终结符集合 terminals = {'id', 'num', '+', '*', '(', ')', ';', 'if', 'then', 'else', 'while', 'do', '$'} changed = True while changed: changed = False for nt, productions in grammar.items(): for prod in productions: # 对每个产生式,计算其 FIRST i = 0 while i < len(prod): sym = prod[i] if sym in terminals: # 终结符,加入 FIRST if sym not in first[nt]: first[nt].add(sym) changed = True break elif sym in grammar: # 非终结符 # 加入 FIRST(sym),若 ε 在 FIRST(sym) 中,则继续下一个符号 before = len(first[nt]) first[nt].update(first[sym] - {'ε'}) if 'ε' not in first[sym]: break i += 1 else: break else: # 所有符号都能推出 ε,加入 ε if 'ε' not in first[nt]: first[nt].add('ε') changed = True return first def compute_follow_sets(grammar, first_sets, start_symbol='S'): follow = defaultdict(set) follow[start_symbol].add('$') # 开始符号 FOLLOW 包含 $ changed = True while changed: changed = False for nt, productions in grammar.items(): for prod in productions: for i, sym in enumerate(prod): if sym in grammar: # sym 是非终结符 # 查找 sym 后的符号串 β beta = prod[i+1:] if beta: # FIRST(β) 中非 ε 元素加入 FOLLOW(sym) first_beta = set() j = 0 while j < len(beta): b = beta[j] if b in first_sets: first_beta.update(first_sets[b] - {'ε'}) if 'ε' not in first_sets[b]: break else: first_beta.add(b) break j += 1 else: # β 可推出 ε,则 FOLLOW(nt) 加入 FOLLOW(sym) first_beta.discard('ε') if 'ε' in first_sets.get(beta[0], set()) if beta else False: pass # 已处理 before = len(follow[sym]) follow[sym].update(first_beta) if len(follow[sym]) > before: changed = True else: # β 为空,FOLLOW(nt) 加入 FOLLOW(sym) before = len(follow[sym]) follow[sym].update(follow[nt]) if len(follow[sym]) > before: changed = True return follow

参数说明:

  • grammar输入为字典,如{'E': [['T', 'E\'']], 'E\'': [['+', 'T', 'E\''], ['ε']]};
  • first_sets是上一步compute_first_sets()的输出;
  • start_symbol指定文法起始符号,默认'S';
  • 脚本自动处理 ε 推导链和 FOLLOW 传播,避免人肉漏算。

3.2 构造 LL(1) 分析表:二维字典 + 冲突检测

有了 FIRST/FOLLOW,分析表M[A, a]就是:对每个产生式A → α,将α填入M[A, b],其中b ∈ FIRST(α);若α ⇒* ε,则将α填入M[A, b],其中b ∈ FOLLOW(A)。

def build_ll1_table(grammar, first_sets, follow_sets): table = defaultdict(dict) for A, productions in grammar.items(): for prod in productions: # 计算 prod 的 FIRST first_prod = set() i = 0 while i < len(prod): sym = prod[i] if sym in first_sets: first_prod.update(first_sets[sym] - {'ε'}) if 'ε' not in first_sets[sym]: break else: first_prod.add(sym) break i += 1 else: # prod 可推出 ε first_prod.discard('ε') first_prod.update(follow_sets[A]) # 填表 for a in first_prod: if a in table[A]: print(f"CONFLICT: M[{A}, {a}] already has {table[A][a]}, now adding {prod}") table[A][a] = prod return dict(table) # 示例文法(简化版) grammar = { 'E': [['T', 'E\'']], 'E\'': [['+', 'T', 'E\''], ['ε']], 'T': [['F', 'T\'']], 'T\'': [['*', 'F', 'T\''], ['ε']], 'F': [['(', 'E', ')'], ['id']] } first = compute_first_sets(grammar) follow = compute_follow_sets(grammar, first, 'E') table = build_ll1_table(grammar, first, follow) print("LL(1) Parsing Table:") for nt, row in table.items(): for t, prod in row.items(): print(f"M[{nt}, {t}] = {prod}")

输出示例:

M[E, id] = ['T', "E'"] M[E, (] = ['T', "E'"] M[E', +] = ['+', 'T', "E'"] M[E', )] = ['ε'] M[E', $] = ['ε'] ...

关键点:

  • build_ll1_table()中的CONFLICT打印是血泪经验——只要出现冲突,文法就不满足 LL(1),必须重构;
  • 表结构用defaultdict(dict),访问table['E']['+']直接得产生式,无需额外索引;
  • 实际工程中,此表可序列化为 JSON 存储,实现文法与解析器逻辑分离。

3.3 表驱动解析器:栈+查表,三步完成一次规约

表驱动解析器极度简洁:一个符号栈、一个输入指针、一个查表动作。它不关心文法语义,只忠实执行M[A, a]指令。

class LL1Parser: def __init__(self, table, start_symbol='E'): self.table = table self.start_symbol = start_symbol def parse(self, tokens): """ tokens: list of (type, value) tuples, ending with ('EOF', '') Returns: AST root node, or raises SyntaxError """ stack = [self.start_symbol] pos = 0 ast_nodes = [] # 用于构建 AST,此处简化为记录规约序列 while stack: top = stack.pop() if top in self.table and pos < len(tokens): a = tokens[pos][0] # 当前输入符号类型 if a in self.table[top]: prod = self.table[top][a] # 将产生式逆序压栈(因栈是 LIFO) for symbol in reversed(prod): if symbol != 'ε': stack.append(symbol) # 记录本次规约:top => prod ast_nodes.append({'rule': f'{top} → {" ".join(prod)}', 'children': []}) else: raise SyntaxError(f"No entry in table for M[{top}, {a}] at position {pos}") elif top in ('id', 'num', '+', '*', '(', ')'): # 终结符 if pos >= len(tokens): raise SyntaxError(f"Unexpected end of input, expected {top}") if tokens[pos][0] == top: pos += 1 else: raise SyntaxError(f"Expected {top}, got {tokens[pos][0]} at {pos}") else: raise SyntaxError(f"Unknown symbol {top} on stack") if pos != len(tokens) - 1: # tokens 末尾是 EOF,应已消耗完 raise SyntaxError(f"Input not fully consumed, remaining: {tokens[pos:]}") return {'type': 'Root', 'children': ast_nodes} # 使用示例 tokens = [('id', 'a'), ('OP_ADD', '+'), ('id', 'b'), ('EOF', '')] parser = LL1Parser(table, 'E') try: result = parser.parse(tokens) print("LL(1) parse success:", result) except SyntaxError as e: print("LL(1) error:", e)

逻辑说明:

  • stack初始为[start_symbol],代表待展开的最左推导;
  • pop()得top,若为非终结符,查表得产生式,reversed()后压栈,模拟推导;
  • 若top为终结符,直接与tokens[pos]匹配,成功则pos++;
  • tokens必须以('EOF', '')结尾,确保输入耗尽判断准确;
  • 此版本 AST 构建极简,实际项目中可在每个prod规约时注入语义动作函数。

4. 避坑指南:语法分析实验里 5 个高频翻车现场与硬核解法

写语法分析器不是写 Hello World,90% 的时间花在调试和修复隐性缺陷上。以下是我在带学生、做内部 DSL 工具时反复踩过的坑,每一条都附带现象、根因和可立即复用的解法。

4.1 现象:SyntaxError: Expected ID, got '+'—— 词法扫描器吞掉了关键 Token

原因:Lexer 的正则顺序错误,导致+被更长的模式(如+=)提前匹配,或空白处理逻辑遗漏,使+前后存在未跳过的\n导致current_token滞后。

解法:

  • 在Lexer.__init__()末尾加断言:assert self.current_token[0] != 'WS',确保首个 Token 不是空白;
  • 为每个 Token 类型添加长度测试:print(f"[DEBUG] Matched {kind}: '{value}' at {mo.start()}-{mo.end()}");
  • 终极验证:写一个dump_tokens(text)函数,输出所有 Token 的(type, value, start_pos, end_pos),肉眼检查是否连续无间隙。

4.2 现象:递归下降 parser 在a + b * c处优先级错乱,生成(a + b) * c的 AST

原因:文法未正确体现运算符结合性。E → E + T | T消除左递归后,若E'的调用位置不对(如在parse_T()内而非parse_E()内),会导致乘法节点被挂到加法节点下。

解法:

  • 严格遵循文法映射:E处理+,T处理*,F处理原子;
  • 在parse_E()中,必须先parse_T(),再parse_Eprime();
  • AST 验证技巧:对输入a + b * c,打印最终 AST 的json.dumps(ast, indent=2),检查*节点是否在+的children[1](即右子树)内,而非同级。

4.3 现象:LL(1) 表构造脚本输出CONFLICT,但文法看起来“应该没问题”

原因:FIRST集计算未考虑间接 ε 推导。例如A → B C,B → ε,C → d,则FIRST(A)应含d,但脚本若未递归传播B的 ε,就会漏算。

解法:

  • 修改compute_first_sets(),增加迭代轮次计数,直到first集不再变化;
  • 添加 debug 输出:print(f"Round {round}: {first}"),观察 ε 如何逐层传播;
  • 人工验证法:对每个含 ε 的非终结符,手动列出所有能由它推出的终结符序列,与脚本输出比对。

4.4 现象:表驱动 parser 报No entry in table for M[T, id],但文法明明有T → F T'

原因:FIRST(F)未正确计算。若F → id | ( E ),则FIRST(F) = {id, (},但若FIRST脚本漏了id(因将其视为终结符未纳入计算),M[T, id]就为空。

解法:

  • 在compute_first_sets()开头,显式声明terminals = {'id', 'num', '+', ...},并确保所有终结符都在此集合;
  • 为F单独运行print("FIRST(F) =", first['F']),确认输出为{'id', '('};
  • 防御性编程:在build_ll1_table()中,对每个prod,打印其first_prod,确认id在其中。

4.5 现象:错误恢复失效,一个;写成:后,后续所有行都报错

原因:eat()函数过于刚性,遇到错误直接抛异常,未提供跳过非法 Token 并同步到下一个合法 Token 的机制。

解法:

  • 在Parser类中添加sync_to(tokens)方法,传入预期 Token 类型列表(如[';', '}', ')']),向前扫描直到匹配任一;
  • 修改eat()为eat_or_sync(expected_types, sync_tokens),失败时自动同步;
  • 最小可行恢复:在parse_Eprime()开头加if self.current_token[0] not in ['OP_ADD', 'RPAREN', 'EOF']: self.sync_to(['OP_ADD', 'RPAREN', 'EOF'])。

5. 从实验报告到生产级工具:三个可立即落地的进阶技巧

语法分析实验的价值,绝不止于交一份 PDF 报告。当你把parse_E()跑通那一刻,你就已经拥有了一个可插拔的语法解析内核。下面这三个技巧,是我把课堂代码真正用进项目里的血泪经验,每个都经过至少三个不同场景验证。

5.1 技巧一:用 AST 节点绑定语义动作,零成本实现计算器

实验报告常止步于生成 AST,但 AST 本身就是执行指令的蓝图。无需额外解释器,直接在parse_F()返回节点时计算值,在parse_Eprime()中执行加法:

def parse_F(self): if self.current_token[0] == 'LPAREN': self.eat('LPAREN') val = self.parse_E() self.eat('RPAREN') return val # val 是数值,不是节点 elif self.current_token[0] == 'NUMBER': val = float(self.current_token[1]) self.eat('NUMBER') return val elif self.current_token[0] == 'ID': # 这里可查变量表,实验中暂返回 0 self.eat('ID') return 0 def parse_E(self): left = self.parse_T() return self.parse_Eprime(left) def parse_Eprime(self, left): if self.current_token[0] == 'OP_ADD': self.eat('OP_ADD') right = self.parse_T() return left + right else: return left # 测试 code = "2 + 3 * 4" lexer = Lexer(code) parser = Parser(lexer) result = parser.parse_E() print("Result:", result) # 输出 14.0

关键点:

  • parse_*函数返回值类型从dict切换为float/int,AST 隐式构建在调用栈中;
  • 语义动作与语法结构严格对齐:Eprime的+动作就是left + right;
  • 此模式可无缝扩展:ID返回变量值,parse_Assign()更新变量表,parse_If()控制执行流。

5.2 技巧二:用 Python AST 模块反向生成可执行代码,验证解析正确性

手写 parser 容易在细节上出错(如括号匹配、运算符结合性)。一个硬核验证法:把你的 parser 输出的 AST,用ast.unparse()转回 Python 代码,再用compile()执行,对比结果。

import ast # 假设你的 parser 返回的是标准 Python AST 节点(如 ast.BinOp) def ast_to_python_code(node): """将自定义 AST 转为 Python AST,再 unparse""" # 示例:将 {'type': 'BinOp', 'op': '+', 'left': ..., 'right': ...} 映射为 ast.BinOp if node['type'] == 'BinOp': left = ast_to_python_code(node['left']) right = ast_to_python_code(node['right']) op = ast.Add() if node['op'] == '+' else ast.Mult() return ast.BinOp(left=left, op=op, right=right) elif node['type'] == 'Num': return ast.Constant(value=node['value']) elif node['type'] == 'Name': return ast.Name(id=node['id'], ctx=ast.Load()) # ... 其他类型 return ast.Constant(value=0) # 验证流程 my_ast = parser.parse_E() # 你的 parser 输出 py_ast = ast_to_python_code(my_ast) code_str = ast.unparse(py_ast) print("Generated Python code:", code_str) # e.g., '(2 + 3) * 4' compiled = compile(code_str, '<string>', 'eval') result = eval(compiled)

价值:

  • ast.unparse()是 Python 3.9+ 的官方工具,生成的代码可直接执行,是黄金验证标准;
  • 若code_str与原始输入语义不等价(如a+b*c变成a+(b*c)),说明你的 AST 构建逻辑有偏差;
  • 此技巧可自动化:对测试集批量生成、编译、执行,覆盖率远超手工检查。

5.3 技巧三:把 parser 封装为 CLI 工具,支持-c和文件输入

实验报告代码往往锁在.py文件里,但真正的工程价值在于可复用。用argparse封装,让它像grep一样工作:

import argparse import sys def main(): parser = argparse.ArgumentParser(description='Simple expression parser') parser.add_argument('-c', '--code', help='Expression to parse (e.g., "2 + 3 * 4")') parser.add_argument('file', nargs='?', help='Input file path (if not using -c)') parser.add_argument('--ast', action='store_true', help='Print AST instead of result') args = parser.parse_args() if args.code: text = args.code elif args.file: with open(args.file) as f: text = f.read() else: text = sys.stdin.read() try: lexer = Lexer(text) parser_obj = Parser(lexer) if args.ast: ast = parser_obj.parse_E() import json print(json.dumps(ast, indent=2)) else: result = parser_obj.parse_E() # 假设已改造为计算模式 print(result) except SyntaxError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()

使用示例:

# 直接计算 $ python parser.py -c "10 / 2 - 3" 2.0 # 解析文件并输出 AST $ echo "a * (b + c)" > expr.txt $ python parser.py --ast expr.txt # 管道输入 $ echo "2 ** 3" | python parser.py 8.0

工程意义:

  • CLI 是模块化的第一道门槛,别人pip install my-parser后就能my-parser -c "...";
  • --ast开关让调试可视化,比print(ast)更清晰;
  • stdin 支持让工具可融入 shell pipeline,这是 DevOps 场景的刚需。

我带过的实习生,第一个月的任务就是把课堂 parser 改造成这样的 CLI。三个月后,他用同一套解析器,给公司内部的配置校验系统写了 DSL 支持——不是重写,只是换了 Token 定义和语义动作。语法分析实验报告里的每一行代码,都不是终点,而是你亲手锻造的第一把编译器刻刀。希望帮到你。

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

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

MATLAB实现SAO雪消融优化算法优化SVR回归预测模型全流程

做预测的朋友应该都有同感&#xff1a;支持向量机回归&#xff08;SVR&#xff09;虽然好用&#xff0c;但C、gamma、epsilon这三个参数真的要调到血压升高。最近我在MATLAB里把雪消融优化算法&#xff08;SAO&#xff09;和SVR结合了一把&#xff0c;做了一套可以直接跑起来的…

作者头像 李华
网站建设 2026/10/10 20:39:11

基于PJ85718DM与STM32F031C6的双温度监测方案设计与实现

1. 从一颗传感器和一颗MCU说起&#xff1a;这个组合到底在解决什么问题温度监测这件事&#xff0c;听起来简单&#xff0c;做起来全是细节。尤其是当你需要同时盯着本地机箱内的温度和几十米外某个房间的温度时&#xff0c;问题就来了&#xff1a;用同一个传感器&#xff1f;信…

作者头像 李华
网站建设 2026/10/10 20:37:39

MiniMax 白送 3000 积分:0 元上手 H3 文生视频全流程

MiniMax 白送 3000 积分&#xff1a;0 元上手 H3 文生视频全流程 【免费下载链接】MiniMax-H3 MiniMax H3 是一个通用的全模态生成系统。它支持对由文本、图像、视频和音频组成的多模态上下文进行统一理解&#xff0c;并能生成分辨率高达 2K、时长可达 15 秒的带原生立体声音频…

作者头像 李华
网站建设 2026/10/10 20:34:30

动态规划序列问题实战:从最长公共子序列到最大子序和

2. 动态规划入门&#xff1a;从最长公共子序列到最大子序和1. 内容整体设计与思路拆解刷到第43天&#xff0c;动态规划已经进入“序列问题”的核心区域。今天这四道题放在一起&#xff0c;其实有很清晰的递进关系&#xff1a;1143最长公共子序列是基础母题&#xff0c;1035不相…

作者头像 李华
网站建设 2026/10/10 20:31:03

OpenRouter 平替实操:LiteLLM 把成本与链路透明度拿回来

OpenRouter 平替实操&#xff1a;LiteLLM 把成本与链路透明度拿回来 【免费下载链接】litellm The fastest, litest AI Gateway. Rust core with Python SDK. Call 100 LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [B…

作者头像 李华
网站建设 2026/10/10 20:23:21

基于Python的手写数字识别系统:从MNIST到卷积网络实战

简介&#xff1a;这份资源面向Python初学者、机器学习入门者以及需要完成课程设计的学生&#xff0c;提供一套完整的手写数字识别系统实现方案。核心思路是先用Windows画图软件绘制2828像素、黑底白字的数字图像作为输入&#xff0c;再交由训练好的多元线性回归模型完成0~9的十…

作者头像 李华