前面内容当中,笔者已对语法分析器的概念进行了简要介绍。本文将进一步深入,通过理论与实践结合的方式探讨语法分析的核心原理及语法分析程序的实现思路。需要说明的是,本章内容与前文可能存在部分重叠,但笔者认为此举对读者具有积极意义。对于大多数软件工程师而言,尽管在学生阶段接触过编译原理课程,但在实际工作中较少有机会应用语言编译器相关知识。不可否认,该领域的理解存在一定门槛,而适度的内容复现有助于强化关键概念。以笔者自身为例,在写作过程中也常需回顾前期学习内容——语法分析涉及的概念体系庞杂,若能从不同维度对同一概念进行阐释,将显著提升理解的深度与全面性。
需特别强调的是,此处所谓“重叠”并非对前文内容的简单复制粘贴,而是针对特定概念或术语进行延展性说明。这些知识不仅能深化读者对学科的理解,更能在理论层面为DSL实现提供支撑,助力开发效率的提升。尽管读者可在阅读过程中选择性跳过重复内容,但笔者建议对相关段落稍作停留——或许能从中发掘出未曾留意的知识细节识。
回归主题。有关语法分析器的作用,可简单理解为如下两条:
- 从词法分析器一次或多次获取词法单元列表。
- 验证该列表是否可由文法生成。
上述内容有些让人费解,什么是“验证词法单元列表是否可由文法生成”呢?简单来说,其实就是指输入的词法单元列表能否由文法推导出来,否的话我们就认为源代码不符合语法约束。以二进制文法6-3为例,它允许的输入字符只能是0或1,当输入串为“12”的时候,输出的Token列表为<'1'>、<'2'>,很明显,第二个Token是无法根据文法推导出来的,因为在文法中找不到对应的产生式。让我们再看一下另外一个案例,语法规则如文法6-6所示:
文法6-6
S -> SUBJECT VERB OBJECT
SUBJECT -> '我'
VERB -> '爱' | '恨'
OBJECT -> '你'
当输入字符串分别为“我爱你”和“我喜欢你”的时候,推导示意图将如图 6.1所示。可以看到,当词法单元的词素为“喜”的时候,不能通过非终结符号VERB进行推导,这种情况下我们便可以认为输入的字符串“我喜欢你”是不符合文法6-6的。
事实上,语法分析并非如想象中那般晦涩神秘。以Java、C#等通用语言为例,其语法分析器的核心职责之一同样是验证输入字符串(即源代码)是否可通过文法推导生成,只不过实现逻辑更为复杂。当然,若仅将语法分析器的功能局限于语法规则验证,未免未能充分发挥其潜力。成熟的语法分析器通常还具备生成详细错误报告、执行语义分析以及代码优化等能力。许多商业IDE(如Intellij IDEA)所提供的代码补全、语法高亮、代码导航等功能,其底层实现基础亦源于语法分析技术。
对于DSL而言,(在大多数场景下)无需在语法分析器上投入过多复杂实现,只需满足以下核心需求:
- 验证DSL脚本是否符合预定义的语法规则。
- 当检测到语法错误时,能够提供精准的错误信息(如错误位置、期望的语法结构等)。
- (若有必要)支持构建抽象语法树、实现即时翻译等扩展任务。
关于错误提示机制,有必要进行进一步补充说明——这是一个常被忽视的技术细节。许多技术人员(包括笔者在内)在设计DSL时往往更关注功能实现,而对错误提示能力重视不足,导致随着时间推移,即便DSL的设计者也可能遗忘具体语法规则及编译器实现逻辑。此时,完善的错误提示机制便能体现其价值。需要强调的是,错误提示的设计必须从编译器设计阶段开始规划。读者应当还记得,Token对象包含的信息除类型和词素外,还包括行号、列号等位置信息,这些数据是实现精准错误提示的基础。由于此类信息难以在语法分析阶段动态获取,必须在词法分析过程中完成构造。若设计初期未考虑错误提示功能,后期往往需要对代码进行返工,不仅增加出错风险,还会导致资源浪费。
回归语法分析器的工作机制:尽管可以将一组Token批量输入至语法分析器,但其实际处理过程仍为逐个读取Token并进行分析——完成一个Token的分析后再取下一个,直至所有Token处理完毕或因异常提前终止。在语法分析过程中,可同步执行以下任务:
- 构建语法分析树,对输入Token进行语法检查及基础语义分析(如类型检查、变量声明合法性验证等)。
- 按需执行语法制导翻译,即在语法分析阶段嵌入语义动作(如计算表达式值、生成中间代码等)。
- 按需构建抽象语法树。
- 按需建立语义模型。
综上内容可知,语法分析器的工作机制可总结为图 6.2所示的流程。除构建语法分析树属必要环节之外,其它操作都是按需执行的。不过这并不是笔者所推荐的形式,尤其是在面对复杂场景的时候,语法分析器所承担的责任有些过重,图 6.3所示流程才是笔者所推荐的。
与图 6.2所示架构不同,按串行方式工作的语法分析器仅涉及三种核心任务。除构建语法分析树外,表达式计算和构建抽象语法树属于可选环节。实际应用中最常见的组合是同时执行语法分析树构建与抽象语法树构建,尤其当语法分析后存在代码生成等后续环节时,这些环节通常依赖语法分析器输出的语法树作为处理基础。
采用串行方式设计语言编译器时,语法分析器的职责边界得到显著简化,与前文定义的外部DSL架构完全契合。这种设计通过明确职责划分、降低模块间耦合度,有效提升了系统的可维护性与可扩展性。作为本书推荐的设计模式,后续案例将统一采用这一架构,即便在简单应用场景中亦保持设计一致性。
在前期内容中,笔者曾引入了“语法制导翻译”(Syntax-Directed Translation,SDT)这一重要概念。作为一种编译器构造技术,SDT允许将语义动作嵌入语法规则,从而在语法分析过程中根据规则匹配情况同步执行相应语义处理。在此模式下,语义动作与语法分析深度耦合,可支持语义模型组装、表达式计算、语法树构建等操作。实际上,图 6.2中虚线框标识的任务均可视为SDT的具体应用场景,相关细节将在后文展开详述。
关于语法分析器的输出形式,需根据具体场景而定。严格意义上的语法分析器(尤其是针对通用编程语言的实现)通常输出中间代码(如字节码、三地址码)或抽象语法树(AST),这一原则同样适用于DSL。不过在特定实现中,其输出也可能是表达式计算结果或语义模型实例,具体取决于语法分析器的设计目标与应用场景。