尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
自己动手开发编译器(六)上下文无关语言和文法
自己动手开发编译器六上下文无关语言和文法在前几篇文章中我们聊了词法分析学会了如何把源代码拆成一个个“单词”Token。但光有单词还不够就像你认识“我”、“爱”、“你”这三个词但如果不按语法规则排列就无法表达完整的意思。这一篇我们进入编译器的“语法分析”阶段核心就是上下文无关文法Context-Free GrammarCFG。### 为什么叫“上下文无关”想象一下在自然语言里“我打他”和“他打我”意思完全不同因为“打”这个动作的发出者和接受者取决于“上下文”谁在前面谁在后面。但在编程语言里我们看一个语句的结构不需要知道变量具体存了什么值只需要知道它的类型和语法位置。比如if (x 0) { y 1; }我们解析这个语句时只关心if后面是个括号表达式里面是个比较运算后面是花括号包裹的代码块——这些规则是固定的不依赖x和y的具体值。这种“只要看当前符号序列就能判断是否符合规则”的语言就叫上下文无关语言。### 文法的形式化定义一个上下文无关文法CFG就是一组规则形式如下A - α其中A是一个非终结符比如Expression、Statementα是一串终结符比如、x、5和非终结符的混合。终结符就是词法分析产出的 Token非终结符就是我们自己定义的抽象语法单元。举个例子一个简单的算术表达式文法Expression - Expression Term | TermTerm - Term * Factor | FactorFactor - ( Expression ) | Number这里Number就是终结符比如5、3Expression、Term、Factor是非终结符。这个文法能描述类似(5 3) * 2这样的表达式。### 上下文无关文法能做什么它能帮我们回答两个问题1.给定一串 Token它是否符合这个文法的规则判断正确性2.如果符合它对应的语法树AST长什么样为后续代码生成打基础我们举一个实际例子写一个简单的解析器用 Python 实现一个只支持加法和乘法的表达式解析器。python# 一个简单的递归下降解析器支持 和 * 遵循优先级乘法优先# 终结符NUMBER, , *, (, )class Token: def __init__(self, type, value): self.type type self.value valuedef tokenize(s): 把字符串拆成 Token 列表这里简化处理只支持数字和运算符 tokens [] i 0 while i len(s): if s[i].isdigit(): j i while j len(s) and s[j].isdigit(): j 1 tokens.append(Token(NUMBER, int(s[i:j]))) i j elif s[i] : tokens.append(Token(PLUS, )) i 1 elif s[i] *: tokens.append(Token(STAR, *)) i 1 elif s[i] (: tokens.append(Token(LPAREN, ()) i 1 elif s[i] ): tokens.append(Token(RPAREN, ))) i 1 else: raise ValueError(f无法识别的字符: {s[i]}) tokens.append(Token(EOF, None)) return tokensclass Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): return self.tokens[self.pos].type def consume(self): token self.tokens[self.pos] self.pos 1 return token # 文法规则 # expr - term ( term )* # term - factor ( * factor )* # factor - NUMBER | ( expr ) def parse_expr(self): 解析表达式最低优先级加法 left self.parse_term() while self.peek() PLUS: self.consume() right self.parse_term() left (, left, right) # 生成简单的AST节点 return left def parse_term(self): 解析项优先级高于加法 left self.parse_factor() while self.peek() STAR: self.consume() right self.parse_factor() left (*, left, right) return left def parse_factor(self): 解析因子最高优先级数字或括号 if self.peek() NUMBER: token self.consume() return token.value elif self.peek() LPAREN: self.consume() expr self.parse_expr() if self.peek() ! RPAREN: raise SyntaxError(缺少右括号) self.consume() return expr else: raise SyntaxError(语法错误)# 测试tokens tokenize(3 5 * ( 2 4 ))parser Parser(tokens)ast parser.parse_expr()print(AST:, ast)这段代码实现了递归下降解析它直接按照文法规则一层层递归生成一个嵌套的树结构。你可以看到parse_expr调用了parse_termparse_term又调用了parse_factor这就是“上下文无关”的体现每个函数只关心当前输入是否符合自己的规则不关心外面发生了什么。### 二义性与优先级你可能会问为什么我们要把加法放在term外面而不是直接写expr - expr expr | expr * expr因为那样会产生二义性。比如输入1 2 * 3如果文法写成expr - expr expr | expr * expr | NUMBER那么既可以把1 2看作一个整体再乘以3也可以把2 * 3看作一个整体再加1。这会导致解析器无法确定该用哪条规则产生多种可能的语法树。而我们的设计乘法优先就消除了二义性乘法在term层加法在expr层这样1 2 * 3只能被解析为1 (2*3)因为expr先看到1然后遇到再调用term去解析2 * 3而不是反过来。### 消除左递归另一个重要问题是左递归。比如文法expr - expr term | term我们的解析器在parse_expr里一开始就调用parse_expr会无限递归下去导致栈溢出。解决办法是把它改写成右递归或迭代形式。上面代码中我们用了while循环这就是把expr - expr term改写成expr - term ( term )*的效果——星号表示零次或多次重复用循环处理。### 构建语法树AST解析器在匹配规则时可以同时构建抽象语法树AST。上面代码中我们用元组表示节点比如(, left, right)。真正的编译器会定义更复杂的节点类但核心思想一样根据文法规则把 Token 序列转换成一个树形结构。后续的语义分析和代码生成都基于这棵树。### 代码示例二用 Python 生成一个简单的计算器我们扩展上面的解析器加入求值功能这样就能实际计算表达式了。python# 在之前 Parser 基础上增加求值功能def evaluate(node): 对 AST 进行求值node 可以是数字或者 (, left, right) 这样的元组 if isinstance(node, int): return node elif isinstance(node, tuple): op node[0] if op : return evaluate(node[1]) evaluate(node[2]) elif op *: return evaluate(node[1]) * evaluate(node[2]) else: raise ValueError(f未知节点: {node})# 测试tokens tokenize(2 3 * ( 4 5 ))parser Parser(tokens)ast parser.parse_expr()result evaluate(ast)print(计算结果:, result) # 输出 2 3 * 9 29这个例子展示了如何把“语法分析”和“语义处理”分开解析器负责生成树求值器负责遍历树。真实编译器的代码生成阶段也是类似只不过输出的是汇编代码而不是数字。### 上下文无关文法的实际应用场景-语法高亮编辑器根据文法规则给代码上色。-静态分析工具比如 ESLint 或 Pyflakes它们解析代码后检查潜在错误。-模板引擎如 Jinja2、Mustache它们解析模板字符串生成渲染逻辑。-数据库查询语言SQL 解析器也是用 CFG 实现的。### 总结上下文无关文法是编译器前端词法分析 语法分析的理论基石。它让我们可以用一套形式化的规则描述编程语言的语法并据此写出解析器。本篇我们介绍了- 什么是上下文无关语言和文法CFG- 如何用递归下降解析器实现简单的文法- 如何处理优先级、左递归和二义性- 如何构建与求值语法树掌握了这些你就具备了实现一个完整语法分析器的能力。下一步我们会讨论语义分析比如类型检查敬请期待
RELATED

相关推荐

BepInEx模组开发入门:从Unity游戏插件加载到Harmony代码注入实战

BepInEx模组开发入门:从Unity游戏插件加载到Harmony代码注入实战

1. 项目概述:为什么选择BepInEx作为你的模组开发起点?如果你玩过一些基于Unity引擎开发的PC游戏,比如《雨中冒险2》、《星露谷物语》的某些大型模组,或者《英灵神殿》的社区扩展,你很可能已经间接接触过BepInEx了。它不…

📅 2026/9/10 2:51:05
无犯罪记录公证怎么办理?2026年线上全流程操作指南(无需跑动)

无犯罪记录公证怎么办理?2026年线上全流程操作指南(无需跑动)

出国留学、海外求职或是办理移民,往往都需要一份无犯罪记录公证书。很多人一听到“公证”两个字就头大,脑海里浮现出请假扣薪、排队半天、材料反复补办的繁琐画面。其实,随着政务服务的不断升级,现在办理公证早已告别了“跑断腿”…

📅 2026/9/8 15:42:13
3 分钟避坑!澳洲 600 签证材料翻译去哪里办理

3 分钟避坑!澳洲 600 签证材料翻译去哪里办理

身边不少朋友办澳洲 600 签证,都栽在材料翻译这一步。上个月同事小林就踩了大坑:图省事找街边打印店翻译存款证明与在职证明,递签后直接被领馆退回,理由是翻译无资质、信息错漏,不仅耽误行程,还白白花了冤枉…

📅 2026/9/11 18:25:37
MORE NEWS

更多资讯

📰

云智变AI开题报告:为什么你的开题答辩像一场“审讯”,而别人的像一场“讨论”?

云智变AI官网:www.yunzhibian.cn | 微信公众号搜一搜:云智变AI学术 开题答辩现场有两种人。 第一种人站上去,PPT翻到第三页,评委开始皱眉:“你这个研究问题到底是什么?”“你说的这个变量怎么…

📰

快速做网站详情页还怕没流量?这份保姆级建站教程救急

快速做网站详情页还怕没流量?这份保姆级建站教程救急 网站做好了没人访问,是不是让你头大如斗?别慌,很多前端新手都卡在这一步。其实不是代码写得不够炫,而是没懂搜索引擎怎么“看”你的页面。今天这篇保姆级建站教程,专门教你怎么快速做网站详情页,让…

📰

ROS2 Humble Nav2 从零搭建:A* 全局规划与 DWA 局部避障实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

3步搞定家乡网页模板,保姆级建站教程让流量翻倍

3步搞定家乡网页模板,保姆级建站教程让流量翻倍 网站做好了没人访问,是不是你的常态?别慌,我见过太多人花几千块做个“家乡网页模板”,上线后日UV只有个位数。今天这篇保姆级建站教程,不聊虚的,直接教你怎么从0到1,把一个冷清的家乡主题站,变成…

📰

树莓派OV5647摄像头CSI排线连接与配置避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

融合AI辅助MBTI性格测试的个性化健身小程序(DeepSeek AI 健身计划、AI 饮食方案、MBTI 四维性格测评、体征与营养记录、训练打卡日历、个人健身数据分析、AI方案人工审核、EChar)

融合 AI 辅助 MBTI 性格测试的个性化健身小程序(UniApp Spring Boot DeepSeek) 做这套系统起因挺实在。我自己健身走过不少弯路:跟着别人的计划练,两天就膝盖不舒服;换成随便搜的减脂攻略,又跟自己想增肌…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬