更新于:2025年6月21号 上午10:38
(0)写在前面的两个引子
有人可能要问:
手撸了将近:1文件夹的600 + 2100例问题的5000+ 3 牛客100热题100*90= 9000 + 4 c语言复习总体代码(包含全部知识点的)5k
---将近2万行源码
为什么要吃错了药搞这个编译器,这不是最难的部分?
为什么还要来这么一个编译器如此底层的帖子?
没办法,这个太重要太底层太硬核了!不能拒绝如此提升自己体术的时刻!
以至于写这个忒子的时候:
我的edge浏览器已经因为输入将近20w字卡顿不堪了!
什么概念?
只要一打开edge编辑这篇文章cpu直接破50+,
亏我是个10400的台式机u。。。。
但是没办法,最硬核的东西就需要最认真的对待!
引子1:为什么C语言能“打遍天下无敌手”?——从底层到性能的深度剖析
各位编程世界的探索者们,当我们谈论C语言时,脑海中浮现的往往不仅仅是那简洁而强大的语法,更是一种对计算机底层无与伦比的掌控感。它就像一位隐居深山的武林宗师,看似朴实无华,实则内力深厚,招招直指要害。C语言为何能“打遍天下无敌手”,在操作系统、嵌入式系统、高性能计算等领域占据着不可撼动的地位?这绝非偶然,其奥秘深藏于它与硬件的“亲密关系”,以及将高级逻辑转化为机器指令的“翻译官”——编译器的宏伟工作中。
C语言作为一门中级语言,巧妙地平衡了高级语言的抽象表达能力与低级语言的硬件控制能力。它不像Python、Java那样拥有丰富的运行时环境和自动内存管理,却因此获得了直接与CPU、内存对话的特权。这种“信任程序员”的设计哲学,赋予了我们直接操作内存(通过指针)、进行位级操作的强大能力。正是这种能力,让C语言编写的程序能够以极高的效率利用计算机资源,从而在性能上傲视群雄。这种贴近硬件的特性,使得C语言编写的程序能够更高效地利用CPU和内存资源,从而在性能上超越许多高级语言。编译器在将C代码转换为机器码时,能够充分利用这种底层控制能力,生成高度优化的指令 。
C语言的性能优势并非偶然,它与编译器的紧密结合是关键。编译器能够将C代码高效地转化为机器码,并进行各种优化,使其在运行时达到极致。可以说,C语言的强大,一半在于其语言设计,另一半则在于其背后那套强大而精密的编译工具链。
引子2 :
编译器的核心使命:从人类语言到机器指令的“翻译官”
在计算机的世界里,CPU只认识一种语言:机器码,一串串冰冷的二进制指令。而我们人类编写的C语言代码,则是充满逻辑和语义的高级语言。要让CPU理解我们的意图,就需要一个“翻译官”——编译器。
什么是编译器?
编译器,顾名思义,是一种特殊的计算机程序,它的主要职责是将用高级编程语言(如C、C++、Java等)编写的源代码,一次性地翻译成计算机或虚拟机能够直接执行的低级语言,通常是机器码或汇编代码 。你可以把它想象成一位高明的同声传译员,将你用C语言表达的复杂思想,精准无误地转化为CPU能够直接执行的指令序列。
编译器与解释器的区别
在编程语言的实现方式上,编译器和解释器是两种截然不同的策略。理解它们的区别,能让我们更深刻地体会C语言为何如此高效:
-
编译器: 像一位严谨的“笔译专家”,它会一次性地将整个源代码文件(或多个文件)全部翻译成机器可执行的二进制代码,然后生成一个独立的可执行程序 。这个可执行文件一旦生成,就可以在目标机器上独立运行,无需再次翻译。C语言正是采用了这种编译模式,这也是其高性能的基石。
-
解释器: 则更像一位“口译专家”,它逐行或逐语句地读取源代码,边翻译边执行 。这意味着每次程序运行时,解释器都需要重复翻译过程。
这种“一次性翻译”的模式,是C语言性能优势的决定性因素。解释器逐行翻译执行,每次执行都需要重复翻译过程,这必然带来运行时开销。而编译器,例如C语言编译器,则是一次性将整个程序翻译成机器码,生成独立的可执行文件 。这意味着程序一旦编译完成,后续执行时无需再次翻译,直接运行机器码,从而大大减少了运行时开销,实现了更高的执行效率。这正是C语言在操作系统、嵌入式系统等性能敏感领域占据主导地位的根本原因。
(1)一步步揭开神秘面纱C语言面纱 :预处理、编译、汇编、链接、加载——
一个C语言程序从我们敲下的第一行代码,到最终在计算机上欢快地运行起来,并非一蹴而就。它要经历一个漫长而精密的“流水线”作业,通常分为预处理、编译、汇编、链接和加载这五个主要阶段 。每个阶段都有其独特的任务和输出,环环相扣,共同铸就了C语言程序的强大生命力。
预处理 (Preprocessing)
这是C语言编译过程的第一道关卡,也是最容易被忽视,却又充满“陷阱”的阶段。预处理器,顾名思义,是一个“预先处理”源代码的程序。它本质上是一个文本处理器,它的任务不是分析代码的语法或语义,而是对源代码文本进行一系列的“宏观”操作,比如文本替换和文件包含 。
预处理器会处理以下指令:
-
#include
:将指定的头文件内容插入到当前文件中。这就像把一张张写满函数声明和宏定义的纸条,粘贴到你的主代码文件里。 -
#define
:进行宏定义和宏替换。这是一种纯粹的文本替换,预处理器会找到代码中所有匹配宏名称的地方,然后用宏定义的内容替换掉它们。 -
#ifdef
,#ifndef
,#if
,#else
,#endif
:条件编译指令。这些指令允许你根据不同的条件,选择性地编译或忽略代码块,这在跨平台开发或调试时非常有用。 -
#undef
:取消宏定义。 -
#line
:改变当前行号和文件名,常用于调试。
预处理后的文件通常以.i
为扩展名。这个阶段的特点是其操作的“盲目性”——它只进行文本级别的替换和文件包含,而不进行语法或语义分析 。这种设计使得C代码可以根据不同的环境动态变化 ,例如通过宏实现跨平台兼容或简化代码。然而,正是这种纯文本替换的特性,可能导致宏展开后产生意想不到的副作用、优先级问题或难以调试的BUG ,因为它缺乏编译阶段的语法和类型检查。因此,在手撸编译器时,通常可以假设输入是已经过预处理的C代码,从而大大简化编译器核心部分的复杂性。
编译 (Compilation)
预处理后的.i
文件,现在变得“纯净”了,去除了所有的宏和#include
指令,只剩下纯粹的C语言代码。这个文件被送入真正的“编译器”阶段。在这个阶段,源代码会被彻底剖析、理解,并最终转化为目标机器的汇编语言代码 。这个阶段是整个编译流程的核心,它又细分为几个子阶段:
词法分析 (Lexical Analysis)
这是编译器的“眼睛”,它从左到右扫描源代码字符流,将其分解成一个个有意义的最小单元,我们称之为“词素”(Token)。想象一下,你正在阅读一篇文章,词法分析器就像是把文章中的每个单词、标点符号都单独挑出来,并给它们贴上“名词”、“动词”、“逗号”等标签。它还会在此阶段移除所有的空白字符和注释。
例如,对于C语言代码 int main() { return 2; }
,词法分析器可能会产生以下Token序列:
-
KEYWORD(int)
-
IDENTIFIER(main)
-
LPAREN
(左括号) -
RPAREN
(右括号) -
LBRACE
(左大括号) -
KEYWORD(return)
-
NUMBER(2)
-
SEMICOLON
(分号) -
RBRACE
(右大括号)
语法分析 (Syntax Analysis)
词法分析器提供了一串“单词”,但这些单词如何组合才能构成有意义的句子?这就是语法分析器的任务。它根据编程语言的语法规则(通常用上下文无关文法,如巴科斯范式BNF描述),将词素序列构建成一棵抽象语法树(Abstract Syntax Tree, AST)。AST是源代码语法结构的一种抽象表示,它移除了具体语法中的冗余细节(比如括号,它们在树的结构中被隐含),更便于后续处理 。
举个例子,if (a > b) { c = a - b; } else { c = b - a; }
这样的语句,在AST中可能表示为一个带有三个分支的节点:条件表达式、if分支体和else分支体 。AST是程序的一种内存中的高级表示,它捕获了程序的本质结构,而忽略了原始文本的表面细节。
语义分析 (Semantic Analysis)
如果说语法分析器检查的是“句子是否合乎语法”,那么语义分析器检查的就是“句子是否有意义”。它会对AST进行深层次的语义检查,确保代码的逻辑正确性和有效性 。这个阶段会处理以下问题:
-
类型检查: 变量的类型是否匹配?例如,你不能将一个字符串赋值给一个整型变量,或者对一个非数字类型进行算术运算。
-
变量声明与使用: 所有使用的变量是否都已声明?是否在正确的作用域内使用?
-
作用域管理: 确定变量、函数等标识符的有效范围。
-
符号表构建: 语义分析阶段会构建和使用一个至关重要的数据结构——符号表 。符号表存储了程序中所有标识符(变量、函数、类型等)的信息,包括它们的名称、类型、作用域、内存位置等。
符号表在编译器中扮演着“字典”的角色。一种简单的实现方式是使用线性链表,将所有符号串联起来 。这种方式插入新符号的时间复杂度是O(1)(只需插入到表头),但查找符号则需要遍历链表,时间复杂度是O(n) 。对于大型项目,这可能会成为性能瓶颈,因此实际编译器会采用更高效的数据结构,如哈希表或树。但对于手撸一个简单编译器,线性链表是一个很好的起点。
中间代码生成 (Intermediate Code Generation)
在语义分析之后,编译器通常不会直接生成目标机器码,而是先将AST转换为一种更抽象、与目标机器无关的中间代码表示 。这种中间代码(Intermediate Representation, IR)可以是三地址码(Three-Address Code)、P-code、字节码等。它的作用是作为前端(分析部分)和后端(代码生成部分)之间的桥梁,使得编译器设计更加模块化和灵活。
例如,对于表达式 x = 10 + 20;
,可以生成如下三地址码 :
t1 = 10
t2 = 20
t3 = t1 + t2
x = t3
这种表示形式将复杂的表达式分解为一系列简单的、原子性的操作,每个操作最多涉及三个地址(两个操作数和一个结果),这极大地简化了后续的代码优化和目标代码生成。
代码优化 (Code Optimization)
中间代码生成之后,编译器会进入一个可选但至关重要的阶段——代码优化。这个阶段的目标是改进中间代码,使其在运行时执行得更快、占用更少的内存或更小的代码体积 。优化技术多种多样,例如:
-
常量折叠 (Constant Folding): 在编译时计算常量表达式的值。例如,
x = 10 + 20;
可以直接优化为x = 30;
。 -
死代码消除 (Dead Code Elimination): 移除永远不会被执行到的代码。
-
循环展开 (Loop Unrolling): 复制循环体,减少循环控制的开销。
-
寄存器分配 (Register Allocation): 智能地将变量分配到CPU寄存器中,以减少内存访问,提高速度。
代码优化可以在函数内部进行(函数内分析优化),也可以跨越整个程序进行(函数间分析优化)。函数内优化开销小但效果有限,函数间优化质量高但耗时更长。一个优秀的优化器能够显著提升程序的性能,这也是现代C语言编译器“牛逼”之处的重要体现。
最终,经过优化后的中间代码会被翻译成特定CPU架构的汇编语言代码 。输出文件通常以
.s
为扩展名。
汇编 (Assembly)
汇编器是编译流水线中的下一个重要工具。它接收编译器生成的汇编代码(.s
文件),并将其翻译成机器可执行的二进制目标代码(Object Code),通常以.o
或.obj
为扩展名 。
目标代码文件不仅仅包含CPU能够直接执行的机器指令,还包含了链接器所需的信息,例如符号表(记录了函数和变量的名称及其在当前文件中的地址或占位符)和重定位信息。这些信息对于后续的链接阶段至关重要。
链接 (Linking)
现在我们有了一个或多个目标文件(.o
文件),它们可能包含了程序的不同部分,也可能包含了标准库(如printf
函数所在的C运行时库)的目标代码。链接器(Linker)的任务就是将这些分散的目标文件“缝合”在一起,形成一个完整、最终的可执行文件 。
链接器的主要工作包括:
-
符号解析: 在程序中,我们可能会调用其他文件中定义的函数,或者引用其他文件中的全局变量。这些在当前目标文件中“未定义”的符号,链接器会在其他目标文件或库文件中寻找到它们的实际定义,并将占位符替换为实际的内存地址 。
-
地址重定位: 每个目标文件都是独立编译的,它们内部的地址都是相对于各自文件起始位置的。当多个目标文件被组合在一起时,它们的相对位置会发生变化。链接器需要调整这些地址,确保所有代码和数据在最终可执行文件中的绝对地址都是正确的 。
-
段合并: 将不同目标文件中相同类型的段(如所有代码段
.text
、所有数据段.data
)合并到最终可执行文件的相应段中 。
链接过程可以分为两种主要形式:
-
静态链接 (Static Linking): 链接器会将所有被程序使用的库函数的实际代码,从库文件中完整地复制到最终生成的可执行文件中 。
-
优点: 生成的可执行文件是“自包含”的,不依赖外部库文件,因此加载速度快,执行速度略快 。在发布程序时,无需担心用户机器上是否存在特定版本的库文件(避免了“DLL地狱”问题)。
-
缺点: 可执行文件体积较大,如果多个程序都静态链接了同一个库,会导致内存和磁盘空间的浪费,因为相同的库代码被复制了多份 。此外,库文件更新后,所有静态链接的程序都需要重新链接才能获得更新 。
-
-
动态链接 (Dynamic Linking): 链接器在生成可执行文件时,并不会将库函数的实际代码复制进来,而只是在可执行文件中记录下对这些库函数的引用信息 。真正的链接和加载操作会延迟到程序运行时才进行。
-
优点: 可执行文件体积小,因为多个程序可以共享同一个动态链接库(如
.so
或.dll
文件),从而节省内存和磁盘空间 。库文件更新后,只需替换动态库文件,无需重新编译和链接主程序,极大地提高了可维护性和可扩展性 。不同编程语言编写的程序只要遵循相同的函数调用约定,就可以调用同一个动态链接库中的函数 。 -
缺点: 程序的运行速度可能略慢,因为在程序启动时需要额外的加载和链接步骤 。程序不再是自完备的,如果所需的动态库不存在或版本不兼容,程序将无法执行 。
-
链接器通过解析符号引用和地址重定位,使得我们可以将大型程序拆分成多个独立的源文件或库文件进行开发,极大地提高了开发效率和代码复用性 。然而,静态链接会将所有库代码直接嵌入到可执行文件中,导致文件体积大、更新困难,但运行时性能略高 。动态链接则将库作为独立文件,运行时才加载,节省内存和磁盘空间,便于更新,但启动稍慢且依赖外部文件 。C语言的生态系统同时支持这两种链接方式,这赋予了开发者在不同场景下进行优化的灵活性,例如嵌入式系统常使用静态链接以确保自包含和确定性,而桌面应用则偏好动态链接以节省资源和方便升级。
加载 (Loading)
经过了预处理、编译、汇编和链接的洗礼,我们的C程序终于变成了一个可以在操作系统上运行的“可执行文件”(在Linux上通常是ELF格式 )。但它仍然只是硬盘上的一个静态文件。要让它真正“活”起来,就需要操作系统的“加载器”(Loader)登场了。
加载器是操作系统中一个至关重要的组成部分,它负责将程序和库从硬盘加载到内存中,并为程序的执行做好一切准备 。这个过程包括:
-
内存映射: 加载器会解析可执行文件(如ELF文件)中的程序头表,根据其中的描述将程序的各个“段”(如代码段、数据段、BSS段等)映射到进程的虚拟内存空间中 。
-
分配内存: 为程序的代码、数据、堆、栈等区域分配内存空间 。
-
符号解析与重定位(针对动态链接): 如果程序使用了动态链接库,加载器(或动态链接器
ld.so
)会在此时查找所需的共享库文件(.so
文件),将它们加载到内存中,并解析程序中对这些库的符号引用,将程序中的符号指向共享库中对应的实际地址 。 -
初始化: 执行一些必要的运行时初始化工作,例如零初始化BSS段(未初始化的全局变量)。
-
设置执行环境: 设置程序的入口点、栈指针等,为CPU执行程序做好准备。
-
移交控制权: 当所有准备工作完成后,操作系统会将CPU的控制权移交给加载好的程序,程序便开始正式运行 。
可执行文件(如ELF格式)在硬盘上只是静态的数据,真正赋予其“生命”并使其运行起来的是操作系统的加载器 。加载器不仅负责将程序代码和数据复制到内存 ,更重要的是它会解析ELF文件中的程序头表,根据其中的描述将不同的“段”(如代码段、数据段)映射到进程的虚拟内存空间,并设置正确的内存权限 。ELF格式的灵活和可扩展性 使得它能够支持不同的CPU架构和操作系统,这与C语言的跨平台特性相辅相成。编译器将C代码编译成ELF等标准格式,使得程序可以在多种操作系统上通过各自的加载器启动,这是C语言在系统级开发中无处不在的根本原因之一。
手撸编译器前的“热身”:搭建开发环境,理解基础工具链
在真正动手手撸一个C语言编译器之前,我们需要先了解和熟悉现有的C语言开发环境。这些成熟的工具将是我们学习、实践和验证自己编译器的“脚手架”和“参照物”。
GCC与Clang
在C语言的世界里,GCC(GNU Compiler Collection)和Clang是两大主流编译器,它们各自拥有独特的魅力和优势:
-
GCC: 作为GNU项目的一部分,GCC是一个历史悠久、功能强大的编译器套件,支持C、C++、Objective-C、Fortran、Java、Ada和Go等多种编程语言 。它是一个工具链,用于编译代码,将代码与各种库依赖项连接,并最终形成可执行文件 。GCC遵循标准的UNIX设计理念,工具简单,但效果和性能一流 。
-
Clang: 是LLVM项目的前端,支持C、C++和Objective-C语言 。它以其编译速度快、内存占用小和友好的诊断报告而闻名,常被视为GCC的有力替代品 。Clang采用基于库的模块化设计,易于集成到IDE中,并支持代码重构、动态分析、代码生成等多样化的编译需求 。
现代编译器架构,如LLVM/Clang的模块化设计,为我们手撸编译器提供了宝贵的设计灵感。传统的GCC设计,其驱动器通常以安装为单位处理单个目标库,导致需要为不同目标部署不同GCC版本 。而Clang/LLVM采用模块化、基于库的设计 ,前端(Clang)与后端(LLVM)分离,允许动态选择目标,一个Clang驱动器即可支持多个目标 。这种模块化设计不仅提高了编译器的可维护性和可扩展性 ,也为我们手撸编译器提供了宝贵的架构参考:我们可以专注于实现C语言的核心编译逻辑,而将汇编、链接等复杂任务交由现有的成熟工具(如GCC的汇编器和链接器)来完成 ,从而大大降低了手撸编译器的门槛和复杂性。这正是“站在巨人肩膀上”的智慧。
交叉编译
C语言之所以能“打遍天下无敌手”,一个重要原因在于其卓越的跨平台能力。这并非C语言本身能直接在任何硬件上运行,而是其源代码经过针对特定目标平台的编译器编译后才能运行。而实现这一特性的关键,就是交叉编译器。
-
定义: 交叉编译器是指在一种计算机平台(宿主机,如你的x86架构的PC)上,生成可以在另一种不同架构的计算机平台(目标机,如ARM架构的嵌入式设备)上运行的代码的编译器 。
-
重要性: 由于嵌入式系统通常没有完整的软件开发环境,因此为这类系统开发软件时,通常需要使用交叉编译器 。C语言在嵌入式系统中的广泛应用,使得交叉编译成为其不可或缺的重要特性。
C语言的跨平台特性在很大程度上得益于编译器的交叉编译能力。它允许开发者在一种操作系统/架构(如x86 Linux)上为另一种操作系统/架构(如ARM嵌入式系统)生成可执行代码 。这意味着C语言的强大之处,不仅仅在于其语法和底层控制能力,更在于其背后强大的工具链,特别是能够为各种异构硬件生成高效机器码的编译器。这拓展了C语言的应用边界,使其成为从微控制器到超级计算机的通用语言。
LLVM/Clang在交叉编译方面表现出色,它支持50多个后端,可以通过简单的命令行选项动态选择目标平台,而无需像传统GCC那样为每个目标安装不同的版本 。这种设计上的灵活性,使得Clang成为构建多目标工具链的理想选择。
代码示例:简单C程序在不同编译阶段的输出对比,展示C语言底层内存布局的初步概念
接下来,我们将通过一个最简单的C语言程序,一步步揭开它在编译流水线中“变身”的秘密。我们将看到源代码如何经过预处理、编译(生成汇编)、汇编(生成目标文件)和链接(生成可执行文件),最终成为CPU可以理解的机器码。
示例程序
我们从一个最简单的C程序开始,它只包含一个main
函数,并返回一个整数值2
。
// 文件名: return_2.c
// 这是一个极简的C语言程序,用于演示编译过程。
// 它的唯一功能是返回整数值 2。
#include <stdio.h> // 包含标准输入输出库,虽然在这个程序中没有直接使用,
// 但为了模拟真实C程序的编译环境,我们依然包含它。
// 预处理器会处理这个指令,将 stdio.h 的内容插入到这里。
int main() {
// main 函数是C程序的入口点。
// 编译器的最终目标是将这行简单的 'return 2;' 代码,
// 转化为CPU能够直接理解和执行的机器指令。
// 这将涉及将立即数 2 放入一个特定的寄存器,然后执行返回操作。
return 2;
}
/*
* 逻辑分析:
* 这个程序虽然简单,但麻雀虽小,五脏俱全。它包含了C语言程序的基本结构:
* 1. 头文件包含:`#include <stdio.h>` 是预处理器指令,它会在编译前将 `stdio.h` 的内容“复制”到当前文件中。
* 这使得我们可以使用标准库中定义的函数和类型(尽管本例中没有直接调用)。
* 2. main 函数:`int main()` 是程序的入口点,操作系统在启动程序时会调用它。
* 3. return 语句:`return 2;` 表示程序执行成功,并向操作系统返回一个状态码 `2`。
* 这个返回值在命令行中可以通过 `echo $?` (Linux/macOS) 或 `echo %errorlevel%` (Windows) 来查看。
* 编译器需要将这个高级语言的逻辑,映射到目标CPU的寄存器和指令集上。
* 例如,在x86-64架构下,函数的返回值通常存放在 `%eax` 或 `%rax` 寄存器中。
*/
预处理后的输出
使用gcc -E return_2.c -o return_2.i
命令,我们可以得到预处理后的文件return_2.i
。return_2.i
文件会非常庞大,因为它包含了stdio.h
的全部内容。这里我们只展示main
函数附近的关键部分。
C
// 文件名: return_2.i (预处理后的部分内容)
//... (此处省略了数千行来自 <stdio.h> 的标准库声明和宏定义)...
// #line 4 "return_2.c"
// 上一行是预处理器插入的指令,用于指示原始文件和行号,方便调试。
// 预处理器已经移除了原始文件中的所有注释。
extern int main (); // 这是一个函数声明,可能来自某个标准头文件,或者由预处理器生成。
// 它告诉编译器 main 函数是外部定义的。
int main() {
// 原始文件中的注释已经被移除。
// 宏定义(如果存在)也在此处被替换成了它们的实际文本。
return 2; // 这行代码保持不变,因为没有宏或 #include 影响它。
}
/*
* 逻辑分析:
* 预处理阶段的输出 `return_2.i` 揭示了预处理器的工作本质:纯粹的文本操作。
* 1. 文件包含:`#include <stdio.h>` 指令导致 `stdio.h` 的所有内容(包括大量的函数声明、类型定义、宏等)
* 被直接“粘贴”到了 `return_2.i` 文件的开头。这解释了为什么预处理后的文件会变得非常大。
* 2. 注释移除:原始 `return_2.c` 中的所有 `//` 和 `/*... */` 注释都被移除了。
* 预处理器认为注释是给人类看的,而不是给编译器看的。
* 3. 宏展开:如果 `return_2.c` 中定义了宏,例如 `#define MY_RETURN_VALUE 2`,
* 那么 `return MY_RETURN_VALUE;` 会被替换成 `return 2;`。在这个简单例子中没有宏。
* 4. 行号信息:`#line 4 "return_2.c"` 这样的指令是预处理器添加的,
* 它告诉后续的编译阶段,当前的代码行来自 `return_2.c` 文件的第4行。
* 这对于编译器在报错时能够准确指出源代码位置至关重要。
*
* 关键点:预处理器不进行任何语法或语义检查。它只管“剪切、粘贴、替换”。
* 如果你的宏定义有问题,或者 `#include` 的文件不存在,预处理器可能会报错,
* 但它不会告诉你 `return 2;` 这样的语句是否符合C语言语法。
* 它的输出是为真正的编译器准备的“干净”输入。
*/
编译后的汇编代码
现在,我们将预处理后的return_2.i
文件送入编译器的核心部分,生成汇编代码。使用gcc -S return_2.i -o return_2.s
命令。汇编代码是介于高级语言和机器码之间的一种低级语言,它用助记符来表示机器指令,更易于人类理解。
程式碼片段
; 文件名: return_2.s (由 GCC 编译 return_2.i 生成的 x86-64 AT&T 语法汇编代码示例)
; 该文件展示了C语言代码如何被翻译成CPU能够理解的底层指令。
.file "return_2.c" ; 指示源文件名称,用于调试信息。
; 编译器通常会保留原始文件名。
.text ;.text 段是代码段,存放可执行的机器指令。
; 这是程序的主要逻辑所在。
.globl main ;.globl 指令声明 'main' 符号是全局可见的。
; 这对于链接器至关重要,因为它需要找到程序的入口点。
; 在某些Unix-like系统上,可能直接是 'main',没有下划线。
.type main, @function ; 声明 'main' 是一个函数类型。
main: ; 'main:' 是函数的标签,表示 'main' 函数的起始地址。
.LFB0: ; 这是一个内部标签,通常用于调试器或编译器内部引用。
.cfi_startproc ; GCC特有的调试指令,用于栈帧信息。
; 帮助调试器理解函数调用和栈回溯。
; 核心逻辑:C语言的 'return 2;' 语句被翻译成了以下两条汇编指令。
; 这直接揭示了C语言代码是如何与CPU的寄存器和指令集进行交互的。
movl $2, %eax ; 1. 将立即数 2 (用 '$' 前缀表示) 移动到 %eax 寄存器中。
; 在x86-64 System V ABI (应用程序二进制接口) 中,
; %eax (或 %rax 在64位模式下) 约定用于存储函数的返回值。
; 'movl' 表示移动一个双字 (32位) 数据。
; 这里使用 movl 是因为 2 是一个小的整数,
; 32位寄存器足以存放,并且可以兼容32位ABI。
ret ; 2. 'ret' 指令表示函数返回。
; 它会从栈中弹出返回地址,并将控制权转移到该地址,
; 即调用 'main' 函数的指令之后的位置 (通常是C运行时启动代码)。
.cfi_endproc ; GCC特有的调试指令,表示栈帧信息结束。
.LFE0: ; 内部标签,与.LFB0 对应。
.size main,.-main ; 声明 'main' 函数的大小,用于链接器和调试器。
; '.-main' 表示当前位置减去 'main' 标签的地址。
.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
; 标识生成此汇编文件的编译器版本。
.section .note.GNU-stack,"",@progbits
; 一个特殊的段,指示该程序使用了GNU栈保护机制。
; 它通常是空的,但对链接器有特殊含义。
/*
* 逻辑分析:
* 编译阶段的输出 `return_2.s` 是C语言编译器(狭义上的“compiler”)的核心成果。
* 它将高级的 `return 2;` 语句翻译成了两条汇编指令:`movl $2, %eax` 和 `ret`。
* 1. **寄存器使用:** `movl $2, %eax` 这条指令直接揭示了C语言的返回值是如何传递给调用者的。
* 在x86-64架构的约定中,整数返回值通常通过 `%eax` 寄存器传递。
* 这体现了C语言与底层硬件(CPU寄存器)的紧密结合。
* 2. **函数返回:** `ret` 指令是CPU级别的函数返回操作。它会利用栈来管理函数的调用流程。
* 当一个函数被调用时,调用者会将返回地址压入栈中;当函数执行 `ret` 时,它会从栈中弹出这个地址并跳转过去。
* 3. **符号可见性:** `.globl main` 指令非常重要,它告诉汇编器和链接器,`main` 是一个外部可见的符号。
* 这意味着其他模块(例如C运行时库的启动代码,它会调用 `main` 函数)可以找到并调用 `main`。
* 4. **段的划分:** `.text` 段明确了接下来的内容是可执行代码。
* 这反映了程序在内存中的逻辑划分:代码、数据、栈等都存放在不同的区域。
*
* 关键点:汇编代码是机器码的人类可读形式。它已经非常接近CPU的思维方式。
* 编译器在这里完成了从高级逻辑到低级指令的“翻译”,但它还没有生成最终的二进制文件,
* 也没有解决跨文件引用(例如,如果 `main` 函数调用了 `printf`,`printf` 的实际地址还未确定)。
* 这些任务将由后续的汇编器和链接器来完成。
*/
目标文件
汇编器将汇编代码(.s
文件)翻译成机器可执行的二进制目标代码(Object Code),通常以.o
或.obj
为扩展名。使用as return_2.s -o return_2.o
命令。
目标文件是二进制格式,无法直接通过文本编辑器查看其内容。但我们可以用objdump -d return_2.o
或readelf -s return_2.o
等工具来检查其内部结构。
-
逻辑分析: 目标文件是编译过程中的一个中间产物,它包含了:
-
机器指令: 汇编代码中的每一条指令都被翻译成了对应的二进制机器码。
-
数据: 如果程序中定义了全局变量或静态变量,它们的数据也会被存放在目标文件中。
-
符号表: 记录了当前文件中定义和引用的所有符号(如
main
函数),以及它们在文件中的偏移量或是否为未定义符号。 -
重定位信息: 标记了哪些地方的地址需要在链接时进行修正(例如,对外部函数的调用地址,在当前目标文件中只是一个占位符)。
-
目标文件是二进制格式,无法直接查看其内容,但它包含了机器指令、数据、以及符号表(记录了函数和变量的名称及其地址或占位符)和重定位信息。它是链接器的输入。这个阶段的产物,是为最终的“拼图”——可执行文件——准备好的单个“碎片”。
可执行文件
最后,链接器将目标文件(return_2.o
)与C运行时库等必要的组件链接起来,生成最终的可执行文件。在Linux系统上,这通常是一个ELF(Executable and Linkable Format)格式的文件。我们可以使用gcc return_2.o -o return_2
(GCC会自动调用链接器ld
)或直接使用ld return_2.o -o return_2
命令。
-
逻辑分析: 链接器将
return_2.o
与标准C库中的启动代码(例如,_start
函数,它是程序真正的入口点,它会负责设置运行环境,然后调用我们的main
函数)以及其他必要的运行时组件合并。它解析所有符号引用,将占位符替换为实际的内存地址,并进行必要的重定位,最终生成一个完整的、可以在操作系统上直接运行的二进制文件(如ELF格式)。-
这个可执行文件包含了程序运行所需的所有代码和数据,并且所有内部和外部的引用都已被解析并指向正确的内存地址。
-
链接器通过解析符号引用和地址重定位,使得我们可以将大型程序拆分成多个独立的源文件或库文件进行开发,极大地提高了开发效率和代码复用性 。
-
最终生成的可执行文件(在Linux上通常是ELF格式) 并不是直接运行,而是由操作系统的加载器负责将其从硬盘加载到内存中,并进行必要的准备工作,如内存映射、符号解析等,然后将控制权交给程序,使其开始执行 。
重要表格:编译器各阶段输入与输出一览表
为了更清晰地理解C语言程序从源代码到可执行文件的完整生命周期,下表总结了编译过程的各个阶段、它们的输入、输出以及主要工具和任务。
阶段 |
输入文件类型/扩展名 |
输出文件类型/扩展名 |
主要工具/程序 |
核心任务 |
预处理 |
C源代码 ( |
预处理后的C代码 ( |
预处理器 ( |
宏展开、文件包含、条件编译、注释移除 |
编译 |
预处理后的C代码 ( |
汇编代码 ( |
编译器 ( |
词法分析、语法分析(生成AST)、语义分析(构建符号表)、中间代码生成、代码优化 |
汇编 |
汇编代码 ( |
目标代码 ( |
汇编器 ( |
将汇编指令翻译成机器码,生成包含机器码和链接信息的程序模块 |
链接 |
目标代码 ( |
可执行文件 (无特定扩展名,Linux上为ELF格式) |
链接器 ( |
解析符号引用、地址重定位、合并目标文件和库文件,生成最终可执行程序 |
加载 |
可执行文件 (ELF, PE等) |
内存中的进程映像 |
操作系统加载器 ( |
将可执行文件从硬盘加载到内存,进行内存映射、动态链接(若有)、初始化,并启动程序执行 |
小结:
通过对C语言编译全流程的深入剖析,我们不难发现,C语言之所以在系统编程领域独占鳌头,其根本原因在于它与计算机硬件的“亲密无间”以及背后一套高效、模块化的编译器工具链。从指针直接操作内存的底层能力,到编译器一次性将高级代码转化为机器码的性能优势,再到链接器实现程序模块化开发和加载器赋予程序生命的精妙机制,每一步都展现了C语言设计哲学的卓越与实用。
现代编译器如Clang/LLVM的模块化架构,更是为我们理解和构建自己的编译器提供了清晰的路径。它将复杂的编译任务分解为可管理、可复用的组件,这不仅提升了编译器的自身效能,也为开发者提供了极大的灵活性。C语言的跨平台能力,也正是得益于这种强大的编译工具链,特别是交叉编译器的存在,使得C代码能够在各种异构硬件上高效运行。
理解这些底层机制,不仅能帮助我们写出更高效、更健壮的C代码,更能让我们对整个计算机系统的运作原理有一个全新的认识。手撸一个C语言编译器,正是将这些理论知识付诸实践的最佳途径。它将是一场充满挑战但又极具成就感的旅程,真正“掀开C语言的底裤”,看透其“红尘”中的每一个细节。接下来的部分,我们将真正踏上这条手撸编译器的征途,从零开始构建一个属于我们自己的C语言编译器,将理论化为代码,将理解变为创造。
-----------------------------------------------------------------------------------------------------------------------------更新于2025.6.19号 晚8:27
(2)C语言编译器:深度探索与构建
手撸编译器:从零开始的征途——核心组件与模块化设计
各位勇士们,准备好迎接挑战了吗?“手撸”一个C语言编译器,听起来可能像是一个遥不可及的梦想,但实际上,只要我们一步一个脚印,将其拆解成一个个可管理的小模块,这个梦想就能变为现实。现代编译器的设计哲学,如LLVM/Clang所示,正是强调模块化和可重用性。我们将借鉴这种思想,构建一个分阶段的编译器,每个阶段都专注于特定的任务。
我们的编译器旅程将从最基础的部分开始:
-
词法分析器(Lexical Analyzer/Scanner): 程序的“眼睛”,负责将源代码字符流分解成有意义的“词素”(Token)流。
-
语法分析器(Syntax Analyzer/Parser): 程序的“大脑”,根据语法规则将Token流构建成抽象语法树(AST)。
-
语义分析器(Semantic Analyzer): 程序的“逻辑判断官”,检查AST的语义合法性,并构建符号表。
-
中间代码生成器(Intermediate Code Generator): 将AST转换为平台无关的中间代码。
-
代码优化器(Optimizer): 对中间代码进行各种优化,提高执行效率。
-
目标代码生成器(Target Code Generator): 将优化后的中间代码转换为特定架构(例如x86-64)的汇编代码。
今天,我们将聚焦于编译器的第一道防线——词法分析器。
揭秘编译器的“眼睛”:词法分析器——把代码变成“单词”流
想象一下,你正在阅读一本英文小说。你的眼睛并不是逐个字母地识别,而是以“单词”为单位进行阅读。然后,你再根据单词的顺序和搭配,理解句子的含义。编译器的词法分析器,扮演的正是你眼睛识别“单词”的角色。
词法分析器的核心使命
词法分析器(Lexical Analyzer),也被称为扫描器(Scanner),是编译器前端的第一阶段。它的主要任务是:
-
输入: 接收原始的源代码字符流。
-
输出: 生成一个有意义的“词素”(Token)序列。
-
过滤: 在这个过程中,它还会过滤掉源代码中所有的空白字符(空格、制表符、换行符等)和注释,因为这些内容对于后续的语法和语义分析来说是“噪音”。
什么是“词素”(Token)?
词素(Token)是编程语言中具有独立意义的最小单元。它不仅仅是字符的组合,更包含了一种“类别”信息。例如,在C语言中:
-
int
是一个关键字Token(类别:KEYWORD
,值:int
)。 -
main
是一个标识符Token(类别:IDENTIFIER
,值:main
)。 -
(
是一个左括号Token(类别:LPAREN
)。 -
123
是一个整数常量Token(类别:INTEGER_LITERAL
,值:123
)。 -
+
是一个运算符Token(类别:OPERATOR
,值:+
)。
每个Token通常包含两个主要部分:
-
类型(Type): 表示该词素的类别(例如,关键字、标识符、运算符、常量等)。
-
值/属性(Value/Attribute): 如果需要,存储该词素的具体内容(例如,标识符的名称、数字的实际值、字符串的内容)。对于像括号、分号这类本身就代表其类型的Token,可能不需要额外的值。
词法分析的实现原理:有限自动机(Finite Automata)
词法分析器通常基于**有限自动机(Finite Automata, FA)**的原理来实现。简单来说,它就像一个状态机,根据当前读取到的字符,从一个状态转换到另一个状态,直到识别出一个完整的词素。
例如,识别一个标识符(由字母、数字、下划线组成,且不能以数字开头)的过程可以这样描述:
-
初始状态: 等待读取字符。
-
读取到字母或下划线: 进入“识别标识符”状态,继续读取字母、数字或下划线。
-
读取到非字母/数字/下划线: 标识符识别结束,生成标识符Token,并回到初始状态。
对于每个不同的词素类型(关键字、标识符、数字、字符串、运算符等),词法分析器都有对应的规则和逻辑来识别它们。
手撸词法分析器:C语言实现
现在,让我们开始用C语言来实现一个简单的词法分析器。我们将处理以下几类词素:
-
关键字:
int
,return
-
标识符: 变量名、函数名等
-
整数常量:
123
,0
,456
-
运算符:
+
,-
,*
,/
,=
,==
,<
,>
,<=
,>=
-
分隔符:
(
,)
,{
,}
,;
,,
-
文件结束符:
EOF
为了模块化,我们将把Token的定义、词法分析器的接口和实现分别放在不同的文件中。
1. token.h
:Token的定义
这个头文件将定义所有可能的Token类型,以及用于表示一个Token的结构体。
// 文件名: token.h
// 描述: 定义编译器词法分析阶段识别出的词素(Token)的类型和结构。
#ifndef TOKEN_H
#define TOKEN_H
// 定义Token的类型枚举
// 这些枚举值代表了C语言中不同种类的词素。
typedef enum {
// 特殊Token类型
TOKEN_EOF = -1, // 文件结束符 (End-Of-File),表示源代码已经读取完毕
TOKEN_UNKNOWN, // 未知Token,用于表示词法分析器无法识别的字符序列,通常是错误
TOKEN_ERROR, // 词法错误,例如不完整的字符串或注释
// 关键字
TOKEN_INT, // 'int' 关键字
TOKEN_RETURN, // 'return' 关键字
TOKEN_IF, // 'if' 关键字 (待扩展)
TOKEN_ELSE, // 'else' 关键字 (待扩展)
TOKEN_WHILE, // 'while' 关键字 (待扩展)
TOKEN_FOR, // 'for' 关键字 (待扩展)
TOKEN_DO, // 'do' 关键字 (待扩展)
TOKEN_BREAK, // 'break' 关键字 (待扩展)
TOKEN_CONTINUE, // 'continue' 关键字 (待扩展)
TOKEN_VOID, // 'void' 关键字 (待扩展)
TOKEN_CHAR, // 'char' 关键字 (待扩展)
TOKEN_SHORT, // 'short' 关键字 (待扩展)
TOKEN_LONG, // 'long' 关键字 (待扩展)
TOKEN_FLOAT, // 'float' 关键字 (待扩展)
TOKEN_DOUBLE, // 'double' 关键字 (待扩展)
TOKEN_STRUCT, // 'struct' 关键字 (待扩展)
TOKEN_UNION, // 'union' 关键字 (待扩展)
TOKEN_ENUM, // 'enum' 关键字 (待扩展)
TOKEN_TYPEDEF, // 'typedef' 关键字 (待扩展)
TOKEN_SIZEOF, // 'sizeof' 关键字 (待扩展)
TOKEN_EXTERN, // 'extern' 关键字 (待扩展)
TOKEN_STATIC, // 'static' 关键字 (待扩展)
TOKEN_CONST, // 'const' 关键字 (待扩展)
TOKEN_VOLATILE, // 'volatile' 关键字 (待扩展)
TOKEN_SIGNED, // 'signed' 关键字 (待扩展)
TOKEN_UNSIGNED, // 'unsigned' 关键字 (待扩展)
TOKEN_GOTO, // 'goto' 关键字 (待扩展)
TOKEN_SWITCH, // 'switch' 关键字 (待扩展)
TOKEN_CASE, // 'case' 关键字 (待扩展)
TOKEN_DEFAULT, // 'default' 关键字 (待扩展)
// 标识符
TOKEN_IDENTIFIER, // 标识符,如变量名、函数名
// 字面量 (常量)
TOKEN_INTEGER_LITERAL, // 整数常量,如 123, 0xFF
TOKEN_STRING_LITERAL, // 字符串常量,如 "hello world" (待实现)
TOKEN_CHAR_LITERAL, // 字符常量,如 'a' (待实现)
TOKEN_FLOAT_LITERAL, // 浮点数常量,如 3.14 (待实现)
// 运算符和标点符号
TOKEN_PLUS, // '+'
TOKEN_MINUS, // '-'
TOKEN_ASTERISK, // '*'
TOKEN_SLASH, // '/'
TOKEN_ASSIGN, // '='
TOKEN_EQ, // '==' (等于)
TOKEN_NE, // '!=' (不等于) (待实现)
TOKEN_LT, // '<' (小于)
TOKEN_LE, // '<=' (小于等于)
TOKEN_GT, // '>' (大于)
TOKEN_GE, // '>=' (大于等于)
TOKEN_AND, // '&&' (逻辑与) (待实现)
TOKEN_OR, // '||' (逻辑或) (待实现)
TOKEN_NOT, // '!' (逻辑非) (待实现)
TOKEN_BITWISE_AND, // '&' (位与) (待实现)
TOKEN_BITWISE_OR, // '|' (位或) (待实现)
TOKEN_BITWISE_XOR, // '^' (位异或) (待实现)
TOKEN_BITWISE_NOT, // '~' (位非) (待实现)
TOKEN_LEFT_SHIFT, // '<<' (左移) (待实现)
TOKEN_RIGHT_SHIFT, // '>>' (右移) (待实现)
TOKEN_INC, // '++' (自增) (待实现)
TOKEN_DEC, // '--' (自减) (待实现)
TOKEN_ARROW, // '->' (箭头运算符) (待实现)
TOKEN_DOT, // '.' (成员访问符) (待实现)
TOKEN_LPAREN, // '('
TOKEN_RPAREN, // ')'
TOKEN_LBRACE, // '{'
TOKEN_RBRACE, // '}'
TOKEN_LBRACKET, // '[' (待实现)
TOKEN_RBRACKET, // ']' (待实现)
TOKEN_SEMICOLON, // ';'
TOKEN_COMMA, // ','
TOKEN_COLON, // ':' (待实现)
TOKEN_QUESTION, // '?' (待实现)
} TokenType;
// Token结构体定义
// 每个识别出的词素都将用这个结构体表示。
typedef struct {
TokenType type; // Token的类型
char *lexeme; // 词素的文本内容(指向源代码中的子串或复制的字符串)
int int_value; // 如果是整数常量,存储其整数值
// double float_value; // 如果是浮点数常量,存储其浮点值 (待实现)
int line; // Token所在的行号
int column; // Token所在的列号
} Token;
// 辅助函数声明(用于调试和打印Token信息)
// 在词法分析过程中,打印Token信息对于调试和验证非常有用。
const char* token_type_to_string(TokenType type);
#endif // TOKEN_H
/*
* 逻辑分析:
* `token.h` 文件是整个编译器前端的基础,它定义了词法分析器能够识别的所有“语言砖块”。
* 1. `TokenType` 枚举:
* - 这是最核心的部分,它列举了C语言语法中所有具有独立意义的单元。
* - 我们从最基本的 `TOKEN_EOF` 和 `TOKEN_UNKNOWN` 开始,逐步添加C语言的关键字、运算符、分隔符和字面量。
* - 注意:这里很多Token类型目前还只是占位符 (`// (待扩展)`),这反映了一个编译器是逐步完善的。
* 我们从一个最小可用的子集开始,然后根据需要逐步增加对更多C语言特性的支持。
* - 有些Token类型,如 `TOKEN_PLUS` 和 `TOKEN_INC`,虽然都包含 `+` 字符,但它们在语义上是不同的。
* 词法分析器需要能够区分 `+` (加法运算符) 和 `++` (自增运算符)。
* 这通常通过“最长匹配原则”来实现,即词法分析器总是尝试匹配最长的可能词素。
* 2. `Token` 结构体:
* - `type`: 存储Token的类型,这是最重要的信息,告诉后续的语法分析器“这是个什么”。
* - `lexeme`: 存储Token的原始文本。这对于错误报告和后续阶段的调试非常有用。
* 在实际编译器中,`lexeme` 可能是一个指向源代码缓冲区中的指针,而不是复制一份,以节省内存。
* 但在我们的简单实现中,为了方便,可能会复制一份。
* - `int_value`: 如果Token是一个整数常量,它的实际数值会被存储在这里。
* 这避免了在后续阶段再次将字符串转换为数字的开销。
* - `line` 和 `column`: 这些是位置信息,对于生成有用的错误消息至关重要。
* 当编译器报错时,能够指出错误发生在源代码的哪一行哪一列,能极大地帮助开发者调试。
*
* 设计考量:
* - 扩展性:通过枚举的方式定义Token类型,使得未来添加新的关键字、运算符等非常方便。
* - 信息完整性:每个Token不仅包含类型,还包含原始文本、数值(如果适用)以及位置信息。
* 这些信息对于后续的语法分析、语义分析乃至错误报告都不可或缺。
* - 调试友好:`token_type_to_string` 辅助函数对于调试词法分析器非常重要,
* 它能将枚举值转换成人类可读的字符串,方便我们查看生成的Token流是否正确。
*/
2. lexer.h
:词法分析器的接口
这个头文件将声明词法分析器相关的函数和全局变量,作为词法分析模块的对外接口。
// 文件名: lexer.h
// 描述: 声明词法分析器(Lexer)的接口。
// 词法分析器负责将源代码字符流转换为Token流。
#ifndef LEXER_H
#define LEXER_H
#include "token.h" // 包含Token的定义
// 声明全局变量,用于跟踪当前正在处理的Token。
// 许多简单编译器会使用全局变量来存储当前和下一个Token,
// 这样解析器可以直接访问它们。
// 在更复杂的编译器中,可能会封装到Lexer结构体中,并通过指针传递。
extern Token current_token; // 当前被词法分析器识别并返回的Token
extern Token peek_token; // 预读的下一个Token (lookahead token),
// 用于处理需要查看下一个字符才能确定当前Token类型的情况
// 例如,区分 '=' 和 '=='
// 函数声明:初始化词法分析器
// 参数: source_code_path - 待分析的C源代码文件的路径。
// 返回值: 成功返回0,失败返回非0。
int init_lexer(const char *source_code_path);
// 函数声明:获取下一个Token
// 这是词法分析器的核心函数。每次调用都会从源代码中读取字符,
// 识别并返回下一个完整的Token。
// 返回值: 返回识别到的Token。
Token get_next_token();
// 函数声明:预读下一个Token,但不消耗它。
// 用于实现LL(1)或更高阶的解析,即在决定当前Token的类型时,
// 需要提前知道后续一个或多个Token的类型。
// 返回值: 返回预读到的Token。
Token peek_next_token();
// 函数声明:释放词法分析器相关的资源。
void close_lexer();
// 错误报告函数声明
// 当词法分析器遇到无法识别的字符或不合法的词素时,需要报告错误。
void lexer_error(const char *format, ...);
#endif // LEXER_H
/*
* 逻辑分析:
* `lexer.h` 定义了词法分析器模块的“公共接口”。
* 1. 全局Token变量 (`current_token`, `peek_token`):
* - 这是为了简化起见。在小型的编译器中,常常会使用全局变量来存储当前的Token和预读的Token。
* - `current_token` 是 `get_next_token()` 返回的Token。
* - `peek_token` 是一个“预读”机制。在某些情况下(例如,要区分 `=` 和 `==`),
* 词法分析器可能需要查看当前字符的下一个字符才能完全确定当前Token的类型。
* `peek_next_token()` 允许我们查看下一个Token而不实际消耗它,然后 `get_next_token()` 会返回这个预读的Token。
* 这被称为“Lookahead”机制,对于词法和语法分析都非常重要。
* 我们的实现将使用简单的 `peek_char` 函数和 `get_next_token` 内部的预读逻辑,
* 而非维护一个独立的 `peek_token` 变量来预读整个Token。
* 2. `init_lexer`: 词法分析器的初始化函数。
* - 负责打开源代码文件,初始化行/列计数器,以及任何必要的内部状态。
* 3. `get_next_token`: 词法分析器的“发动机”。
* - 每次调用它,它都会从源代码中读取字符,执行识别逻辑,并返回下一个完整的Token。
* - 这是最复杂的函数,包含了识别各种Token类型的核心逻辑。
* 4. `peek_next_token`: 一个“窥视”函数,用于在不改变当前状态的情况下查看下一个Token。
* - 这个函数在简单的词法分析器中不常用,但在语法分析器需要LL(k)特性时非常关键。
* 我们的初步实现可以先不实现完整的 `peek_next_token` (即预读整个Token),
* 而是让 `get_next_token` 内部处理字符级别的预读 (`peek_char`)。
* 5. `close_lexer`: 清理资源。
* - 负责关闭文件句柄,释放动态分配的内存等。
* 6. `lexer_error`: 统一的错误报告机制。
* - 当词法分析过程中出现问题(如非法字符)时,通过这个函数报告错误,
* 并包含行号、列号等调试信息。
*
* 设计考量:
* - 封装性:尽管使用了全局变量,但通过 `lexer.h` 提供清晰的接口,将词法分析的实现细节隐藏起来。
* - 状态管理:词法分析器需要维护当前读取位置(行号、列号)、文件句柄等内部状态。
* - 错误处理:任何健壮的编译器都需要完善的错误报告机制。
*/
3. lexer.c
:词法分析器的实现
这是词法分析器真正的“心脏”部分,包含了识别各种Token的所有逻辑。
// 文件名: lexer.c
// 描述: 词法分析器(Lexer)的核心实现。
// 它负责从源代码文件中读取字符流,并将其分解为一系列的词素(Token)。
#include <stdio.h> // 用于文件操作 (fopen, fclose, fgetc)
#include <stdlib.h> // 用于内存分配 (malloc, realloc, free) 和退出 (exit)
#include <string.h> // 用于字符串操作 (strcmp, strdup, strlen)
#include <ctype.h> // 用于字符类型判断 (isalpha, isdigit, isalnum, isspace)
#include <stdarg.h> // 用于可变参数函数 (va_list, va_start, va_end)
#include "token.h" // 包含Token类型和结构体的定义
#include "lexer.h" // 包含词法分析器接口的定义
// 全局变量定义(在lexer.h中声明为 extern)
Token current_token;
Token peek_token; // 预读Token,此处主要用于概念演示,实际实现可能不直接维护
// 词法分析器内部状态
static FILE *source_file = NULL; // 源代码文件指针
static int current_char; // 当前读取的字符
static int line_num = 1; // 当前行号
static int column_num = 0; // 当前列号
static char *buffer = NULL; // 用于存储词素文本的动态缓冲区
static size_t buffer_capacity = 0; // 缓冲区容量
static size_t buffer_len = 0; // 缓冲区当前长度
// 关键字映射表:将关键字字符串映射到对应的TokenType
// 这是实现关键字识别的关键数据结构。
// 遍历这个表,查找当前识别到的标识符是否是C语言的关键字。
typedef struct {
const char *keyword_str;
TokenType type;
} KeywordMap;
static KeywordMap keywords[] = {
{"int", TOKEN_INT},
{"return", TOKEN_RETURN},
{"if", TOKEN_IF},
{"else", TOKEN_ELSE},
{"while", TOKEN_WHILE},
{"for", TOKEN_FOR},
{"do", TOKEN_DO},
{"break", TOKEN_BREAK},
{"continue", TOKEN_CONTINUE},
{"void", TOKEN_VOID},
{"char", TOKEN_CHAR},
{"short", TOKEN_SHORT},
{"long", TOKEN_LONG},
{"float", TOKEN_FLOAT},
{"double", TOKEN_DOUBLE},
{"struct", TOKEN_STRUCT},
{"union", TOKEN_UNION},
{"enum", TOKEN_ENUM},
{"typedef", TOKEN_TYPEDEF},
{"sizeof", TOKEN_SIZEOF},
{"extern", TOKEN_EXTERN},
{"static", TOKEN_STATIC},
{"const", TOKEN_CONST},
{"volatile", TOKEN_VOLATILE},
{"signed", TOKEN_SIGNED},
{"unsigned", TOKEN_UNSIGNED},
{"goto", TOKEN_GOTO},
{"switch", TOKEN_SWITCH},
{"case", TOKEN_CASE},
{"default", TOKEN_DEFAULT},
{NULL, TOKEN_UNKNOWN} // 哨兵值,用于标记数组结束
};
// --- 辅助函数实现 ---
// 打印词法分析器错误信息
// 这是一个变参函数,可以像 printf 一样使用,方便格式化错误信息。
void lexer_error(const char *format, ...) {
va_list args;
fprintf(stderr, "Lexer Error [%d:%d]: ", line_num, column_num); // 打印错误发生的位置
va_start(args, format);
vfprintf(stderr, format, args); // 格式化并打印错误消息
va_end(args);
fprintf(stderr, "\n");
exit(EXIT_FAILURE); // 遇到致命错误时退出程序
}
// 读取下一个字符并更新行/列号
// 这是词法分析器与源代码文件交互的唯一接口。
static int get_char() {
// 每次读取字符,更新列号。
// 如果是换行符,重置列号为0,行号加1。
current_char = fgetc(source_file);
if (current_char == '\n') {
line_num++;
column_num = 0;
} else if (current_char != EOF) {
column_num++;
}
return current_char;
}
// 预读下一个字符(不消耗)
// 类似于 get_char,但不会移动文件指针,也不会更新行/列号。
// 例如,用于判断 `>` 后面是不是 `=` 来识别 `>=`。
static int peek_char() {
int c = fgetc(source_file);
ungetc(c, source_file); // 将字符放回文件流
return c;
}
// 将字符添加到词素缓冲区
static void add_char_to_buffer(int c) {
// 动态扩展缓冲区,确保足够容纳词素文本
if (buffer_len + 1 >= buffer_capacity) { // +1 是为了 null 终止符
if (buffer_capacity == 0) {
buffer_capacity = 16; // 初始容量
} else {
buffer_capacity *= 2; // 双倍扩容
}
char *new_buffer = (char *)realloc(buffer, buffer_capacity);
if (new_buffer == NULL) {
lexer_error("Failed to allocate buffer memory.");
}
buffer = new_buffer;
}
buffer[buffer_len++] = (char)c;
buffer[buffer_len] = '\0'; // 始终保持 null 终止
}
// 重置词素缓冲区
static void reset_buffer() {
buffer_len = 0;
if (buffer) {
buffer[0] = '\0'; // 确保缓冲区为空字符串
}
}
// 检查是否是关键字
// 遍历关键字映射表,如果找到匹配的字符串,则返回对应的TokenType。
// 否则,它就是一个普通的标识符。
static TokenType check_keyword(const char *text) {
for (int i = 0; keywords[i].keyword_str != NULL; i++) {
if (strcmp(text, keywords[i].keyword_str) == 0) {
return keywords[i].type;
}
}
return TOKEN_IDENTIFIER; // 不是关键字,就是标识符
}
// 初始化词法分析器
int init_lexer(const char *source_code_path) {
source_file = fopen(source_code_path, "r");
if (source_file == NULL) {
fprintf(stderr, "Error: Could not open source file '%s'\n", source_code_path);
return -1;
}
// 初始化缓冲区
buffer_capacity = 128; // 设定一个合理的初始容量
buffer = (char *)malloc(buffer_capacity);
if (buffer == NULL) {
fprintf(stderr, "Error: Failed to allocate buffer for lexer.\n");
fclose(source_file);
source_file = NULL;
return -1;
}
buffer[0] = '\0';
buffer_len = 0;
// 预读取第一个字符,为 get_next_token 做准备
get_char();
return 0;
}
// --- 核心词法分析逻辑 ---
// 获取下一个Token
Token get_next_token() {
// 1. 跳过空白字符
// 词法分析器应该忽略空格、制表符、换行符等空白字符。
while (isspace(current_char)) {
get_char();
}
// 记录Token的起始位置(行号和列号)
int token_start_line = line_num;
int token_start_column = column_num;
reset_buffer(); // 每次识别新Token前清空缓冲区
// 2. 识别文件结束符 EOF
if (current_char == EOF) {
return (Token){TOKEN_EOF, strdup("EOF"), 0, token_start_line, token_start_column};
}
// 3. 识别标识符和关键字
// 标识符以字母或下划线开头,后面可以跟字母、数字或下划线。
if (isalpha(current_char) || current_char == '_') {
while (isalnum(current_char) || current_char == '_') {
add_char_to_buffer(current_char);
get_char();
}
// 识别出标识符文本后,检查它是否是C语言的关键字
TokenType type = check_keyword(buffer);
return (Token){type, strdup(buffer), 0, token_start_line, token_start_column};
}
// 4. 识别整数常量
// 整数常量由一个或多个数字组成。
if (isdigit(current_char)) {
long long_val = 0; // 使用 long long 避免溢出
while (isdigit(current_char)) {
// 简单实现:将字符转换为数字并累加
long_val = long_val * 10 + (current_char - '0');
add_char_to_buffer(current_char);
get_char();
}
// TODO: 处理溢出检测
// TODO: 处理十六进制、八进制、二进制字面量
return (Token){TOKEN_INTEGER_LITERAL, strdup(buffer), (int)long_val, token_start_line, token_start_column};
}
// 5. 识别运算符和标点符号
// 这部分需要处理单字符运算符、双字符运算符(例如 ==, <=, ++)
// 以及注释(/* */ 和 //)
switch (current_char) {
case '+':
add_char_to_buffer(current_char);
get_char();
if (current_char == '+') { // 处理 '++'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_INC, strdup("++"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_PLUS, strdup("+"), 0, token_start_line, token_start_column};
case '-':
add_char_to_buffer(current_char);
get_char();
if (current_char == '>') { // 处理 '->'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_ARROW, strdup("->"), 0, token_start_line, token_start_column};
}
if (current_char == '-') { // 处理 '--'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_DEC, strdup("--"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_MINUS, strdup("-"), 0, token_start_line, token_start_column};
case '*':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_ASTERISK, strdup("*"), 0, token_start_line, token_start_column};
case '/':
add_char_to_buffer(current_char);
get_char();
// 处理注释 // 或 /* */
if (current_char == '/') { // 行注释 //
while (current_char != '\n' && current_char != EOF) {
get_char();
}
// 递归调用 get_next_token() 跳过注释,获取下一个真正的Token
return get_next_token();
} else if (current_char == '*') { // 块注释 /* */
get_char(); // 跳过 '*'
int prev_char = current_char;
get_char(); // 读取下一个字符
while (!(prev_char == '*' && current_char == '/')) {
if (current_char == EOF) {
lexer_error("Unterminated block comment.");
return (Token){TOKEN_ERROR, strdup("Unterminated comment"), 0, token_start_line, token_start_column};
}
prev_char = current_char;
get_char();
}
get_char(); // 跳过 '/'
// 递归调用 get_next_token() 跳过注释,获取下一个真正的Token
return get_next_token();
}
return (Token){TOKEN_SLASH, strdup("/"), 0, token_start_line, token_start_column};
case '=':
add_char_to_buffer(current_char);
get_char();
if (current_char == '=') { // 处理 '=='
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_EQ, strdup("=="), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_ASSIGN, strdup("="), 0, token_start_line, token_start_column};
case '<':
add_char_to_buffer(current_char);
get_char();
if (current_char == '=') { // 处理 '<='
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_LE, strdup("<="), 0, token_start_line, token_start_column};
}
if (current_char == '<') { // 处理 '<<'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_LEFT_SHIFT, strdup("<<"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_LT, strdup("<"), 0, token_start_line, token_start_column};
case '>':
add_char_to_buffer(current_char);
get_char();
if (current_char == '=') { // 处理 '>='
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_GE, strdup(">="), 0, token_start_line, token_start_column};
}
if (current_char == '>') { // 处理 '>>'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_RIGHT_SHIFT, strdup(">>"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_GT, strdup(">"), 0, token_start_line, token_start_column};
case '&':
add_char_to_buffer(current_char);
get_char();
if (current_char == '&') { // 处理 '&&'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_AND, strdup("&&"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_BITWISE_AND, strdup("&"), 0, token_start_line, token_start_column};
case '|':
add_char_to_buffer(current_char);
get_char();
if (current_char == '|') { // 处理 '||'
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_OR, strdup("||"), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_BITWISE_OR, strdup("|"), 0, token_start_line, token_start_column};
case '!':
add_char_to_buffer(current_char);
get_char();
if (current_char == '=') { // 处理 '!='
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_NE, strdup("!="), 0, token_start_line, token_start_column};
}
return (Token){TOKEN_NOT, strdup("!"), 0, token_start_line, token_start_column};
case '~':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_BITWISE_NOT, strdup("~"), 0, token_start_line, token_start_column};
case '^':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_BITWISE_XOR, strdup("^"), 0, token_start_line, token_start_column};
case '(':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_LPAREN, strdup("("), 0, token_start_line, token_start_column};
case ')':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_RPAREN, strdup(")"), 0, token_start_line, token_start_column};
case '{':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_LBRACE, strdup("{"), 0, token_start_line, token_start_column};
case '}':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_RBRACE, strdup("}"), 0, token_start_line, token_start_column};
case '[':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_LBRACKET, strdup("["), 0, token_start_line, token_start_column};
case ']':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_RBRACKET, strdup("]"), 0, token_start_line, token_start_column};
case ';':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_SEMICOLON, strdup(";"), 0, token_start_line, token_start_column};
case ',':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_COMMA, strdup(","), 0, token_start_line, token_start_column};
case ':':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_COLON, strdup(":"), 0, token_start_line, token_start_column};
case '?':
add_char_to_buffer(current_char);
get_char();
return (Token){TOKEN_QUESTION, strdup("?"), 0, token_start_line, token_start_column};
case '.':
add_char_to_buffer(current_char);
get_char();
// TODO: 处理浮点数或省略号 "..."
return (Token){TOKEN_DOT, strdup("."), 0, token_start_line, token_start_column};
// TODO: 处理字符串字面量 ""
case '"': {
add_char_to_buffer(current_char); // 添加双引号
get_char(); // 跳过第一个双引号
while (current_char != '"' && current_char != EOF && current_char != '\n') {
if (current_char == '\\') { // 处理转义字符
add_char_to_buffer(current_char);
get_char(); // 读取转义后的字符
if (current_char == EOF || current_char == '\n') {
lexer_error("Unterminated string literal (escape sequence at EOF/EOL).");
return (Token){TOKEN_ERROR, strdup("Unterminated string"), 0, token_start_line, token_start_column};
}
}
add_char_to_buffer(current_char);
get_char();
}
if (current_char == EOF || current_char == '\n') {
lexer_error("Unterminated string literal.");
return (Token){TOKEN_ERROR, strdup("Unterminated string"), 0, token_start_line, token_start_column};
}
add_char_to_buffer(current_char); // 添加第二个双引号
get_char(); // 跳过第二个双引号
// strdup 会复制整个字符串,包括双引号
char *literal_str = strdup(buffer);
// 这里为了简化,直接把原始字符串存进去。
// 实际编译器可能只存储内容,或进行转义处理。
return (Token){TOKEN_STRING_LITERAL, literal_str, 0, token_start_line, token_start_column};
}
// TODO: 处理字符字面量 ''
case '\'': {
add_char_to_buffer(current_char); // 添加单引号
get_char(); // 跳过第一个单引号
if (current_char == EOF || current_char == '\n') {
lexer_error("Empty character literal or unterminated.");
return (Token){TOKEN_ERROR, strdup("Unterminated char"), 0, token_start_line, token_start_column};
}
if (current_char == '\\') { // 处理转义字符
add_char_to_buffer(current_char);
get_char(); // 读取转义后的字符
if (current_char == EOF || current_char == '\n') {
lexer_error("Invalid character literal (escape sequence at EOF/EOL).");
return (Token){TOKEN_ERROR, strdup("Invalid char"), 0, token_start_line, token_start_column};
}
}
add_char_to_buffer(current_char); // 添加字符
get_char();
if (current_char != '\'') {
lexer_error("Multi-character literal or missing closing single quote.");
return (Token){TOKEN_ERROR, strdup("Invalid char literal"), 0, token_start_line, token_start_column};
}
add_char_to_buffer(current_char); // 添加第二个单引号
get_char(); // 跳过第二个单引号
// strdup 会复制整个字符串,包括单引号
char *literal_char_str = strdup(buffer);
// 实际字符值需要解析转义序列
// 这里我们简单地取第一个字符(非转义)或处理简单转义
int char_val = literal_char_str[1]; // 假设没有转义,取第二个字符
if (literal_char_str[1] == '\\' && strlen(literal_char_str) >= 3) {
// 简单处理一些常见转义字符,更复杂的需要一个完整的解析器
switch (literal_char_str[2]) {
case 'n': char_val = '\n'; break;
case 't': char_val = '\t'; break;
case 'r': char_val = '\r'; break;
case '\\': char_val = '\\'; break;
case '\'': char_val = '\''; break;
case '"': char_val = '"'; break;
case '0': char_val = '\0'; break; // 针对 '\0'
default:
// 对于其他转义,比如八进制或十六进制,这里只是简单的跳过
// 实际需要一个完整的数字转换逻辑
char_val = literal_char_str[2]; // 暂时只取转义字符后的字符
lexer_error("Unsupported escape sequence in character literal. Treating as raw char.");
break;
}
}
return (Token){TOKEN_CHAR_LITERAL, literal_char_str, char_val, token_start_line, token_start_column};
}
default:
// 无法识别的字符,报告错误
add_char_to_buffer(current_char);
lexer_error("Unrecognized character: '%c' (ASCII: %d)", current_char, current_char);
get_char(); // 继续读取,尝试恢复
return (Token){TOKEN_UNKNOWN, strdup(buffer), 0, token_start_line, token_start_column};
}
}
// 预读下一个Token,但不消耗它。
// 注意:这个函数的实现通常比较复杂,因为它需要模拟 get_next_token 的行为,
// 但不改变实际的词法分析器状态。一个简单的做法是,get_next_token 内部已经做了一次预读字符。
// 这里的 peek_next_token 为了简化,暂时不实现完整的“Token预读”,而是依赖 get_next_token 内部的字符预读。
// 如果需要完整的Token预读,需要更复杂的状态保存和恢复机制,或者使用一个Token缓冲区。
Token peek_next_token() {
// 这是一个简化版本,并没有真正实现Token级别的预读
// 真正的Token预读需要保存当前的词法分析器状态,获取下一个Token,然后恢复状态
// 这里我们只是返回一个默认的未知Token,或者可以考虑抛出错误
// 这种简化的 peek_next_token 在本教程的早期阶段可能不够用,
// 但在更高级的语法分析中,Token级别的预读是必要的。
// 在本教程中,我们主要依赖 `peek_char` 进行字符级别的预读来处理 `==`, `>=`, `++` 等。
return (Token){TOKEN_UNKNOWN, strdup(""), 0, line_num, column_num}; // 占位符
}
// 将TokenType转换为可读字符串(用于调试)
const char* token_type_to_string(TokenType type) {
switch (type) {
case TOKEN_EOF: return "TOKEN_EOF";
case TOKEN_UNKNOWN: return "TOKEN_UNKNOWN";
case TOKEN_ERROR: return "TOKEN_ERROR";
case TOKEN_INT: return "TOKEN_INT";
case TOKEN_RETURN: return "TOKEN_RETURN";
case TOKEN_IF: return "TOKEN_IF";
case TOKEN_ELSE: return "TOKEN_ELSE";
case TOKEN_WHILE: return "TOKEN_WHILE";
case TOKEN_FOR: return "TOKEN_FOR";
case TOKEN_FOR: return "TOKEN_FOR"; // 故意重复一个,看有没有报错
case TOKEN_DO: return "TOKEN_DO";
case TOKEN_BREAK: return "TOKEN_BREAK";
case TOKEN_CONTINUE: return "TOKEN_CONTINUE";
case TOKEN_VOID: return "TOKEN_VOID";
case TOKEN_CHAR: return "TOKEN_CHAR";
case TOKEN_SHORT: return "TOKEN_SHORT";
case TOKEN_LONG: return "TOKEN_LONG";
case TOKEN_FLOAT: return "TOKEN_FLOAT";
case TOKEN_DOUBLE: return "TOKEN_DOUBLE";
case TOKEN_STRUCT: return "TOKEN_STRUCT";
case TOKEN_UNION: return "TOKEN_UNION";
case TOKEN_ENUM: return "TOKEN_ENUM";
case TOKEN_TYPEDEF: return "TOKEN_TYPEDEF";
case TOKEN_SIZEOF: return "TOKEN_SIZEOF";
case TOKEN_EXTERN: return "TOKEN_EXTERN";
case TOKEN_STATIC: return "TOKEN_STATIC";
case TOKEN_CONST: return "TOKEN_CONST";
case TOKEN_VOLATILE: return "TOKEN_VOLATILE";
case TOKEN_SIGNED: return "TOKEN_SIGNED";
case TOKEN_UNSIGNED: return "TOKEN_UNSIGNED";
case TOKEN_GOTO: return "TOKEN_GOTO";
case TOKEN_SWITCH: return "TOKEN_SWITCH";
case TOKEN_CASE: return "TOKEN_CASE";
case TOKEN_DEFAULT: return "TOKEN_DEFAULT";
case TOKEN_IDENTIFIER: return "TOKEN_IDENTIFIER";
case TOKEN_INTEGER_LITERAL: return "TOKEN_INTEGER_LITERAL";
case TOKEN_STRING_LITERAL: return "TOKEN_STRING_LITERAL";
case TOKEN_CHAR_LITERAL: return "TOKEN_CHAR_LITERAL";
case TOKEN_FLOAT_LITERAL: return "TOKEN_FLOAT_LITERAL";
case TOKEN_PLUS: return "TOKEN_PLUS";
case TOKEN_MINUS: return "TOKEN_MINUS";
case TOKEN_ASTERISK: return "TOKEN_ASTERISK";
case TOKEN_SLASH: return "TOKEN_SLASH";
case TOKEN_ASSIGN: return "TOKEN_ASSIGN";
case TOKEN_EQ: return "TOKEN_EQ";
case TOKEN_NE: return "TOKEN_NE";
case TOKEN_LT: return "TOKEN_LT";
case TOKEN_LE: return "TOKEN_LE";
case TOKEN_GT: return "TOKEN_GT";
case TOKEN_GE: return "TOKEN_GE";
case TOKEN_AND: return "TOKEN_AND";
case TOKEN_OR: return "TOKEN_OR";
case TOKEN_NOT: return "TOKEN_NOT";
case TOKEN_BITWISE_AND: return "TOKEN_BITWISE_AND";
case TOKEN_BITWISE_OR: return "TOKEN_BITWISE_OR";
case TOKEN_BITWISE_XOR: return "TOKEN_BITWISE_XOR";
case TOKEN_BITWISE_NOT: return "TOKEN_BITWISE_NOT";
case TOKEN_LEFT_SHIFT: return "TOKEN_LEFT_SHIFT";
case TOKEN_RIGHT_SHIFT: return "TOKEN_RIGHT_SHIFT";
case TOKEN_INC: return "TOKEN_INC";
case TOKEN_DEC: return "TOKEN_DEC";
case TOKEN_ARROW: return "TOKEN_ARROW";
case TOKEN_DOT: return "TOKEN_DOT";
case TOKEN_LPAREN: return "TOKEN_LPAREN";
case TOKEN_RPAREN: return "TOKEN_RPAREN";
case TOKEN_LBRACE: return "TOKEN_LBRACE";
case TOKEN_RBRACE: return "TOKEN_RBRACE";
case TOKEN_LBRACKET: return "TOKEN_LBRACKET";
case TOKEN_RBRACKET: return "TOKEN_RBRACKET";
case TOKEN_SEMICOLON: return "TOKEN_SEMICOLON";
case TOKEN_COMMA: return "TOKEN_COMMA";
case TOKEN_COLON: return "TOKEN_COLON";
case TOKEN_QUESTION: return "TOKEN_QUESTION";
default: return "UNKNOWN_TOKEN_TYPE";
}
}
// 释放词法分析器资源
void close_lexer() {
if (source_file) {
fclose(source_file);
source_file = NULL;
}
if (buffer) {
free(buffer);
buffer = NULL;
buffer_capacity = 0;
buffer_len = 0;
}
// 释放最后一个 current_token.lexeme,如果它不是 NULL
// 注意:每次 get_next_token 返回 strdup 的结果,需要负责释放
if (current_token.lexeme) {
free(current_token.lexeme);
current_token.lexeme = NULL;
}
}
// 主函数,用于测试词法分析器
// 将作为独立的编译单元来测试 lexer.c 的功能
int main(int argc, char *argv[]) {
// 检查命令行参数
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
// 初始化词法分析器
if (init_lexer(argv[1]) != 0) {
return EXIT_FAILURE;
}
printf("--- Lexical Analysis Output ---\n");
printf("Line:Col | Type | Lexeme | Value\n");
printf("--------------------------------------------------------------\n");
// 循环获取并打印Token,直到文件结束
do {
current_token = get_next_token(); // 获取下一个Token
// 打印Token信息
printf("%4d:%-3d | %-20s | %-17s | ",
current_token.line,
current_token.column,
token_type_to_string(current_token.type),
current_token.lexeme ? current_token.lexeme : "(null)");
// 如果是整数,额外打印其值
if (current_token.type == TOKEN_INTEGER_LITERAL) {
printf("%d\n", current_token.int_value);
} else if (current_token.type == TOKEN_CHAR_LITERAL) {
printf("'%c' (ASCII: %d)\n", current_token.int_value, current_token.int_value);
}
else {
printf("\n");
}
// 释放词素文本(因为 strdup 复制了内存)
// 对于 TOKEN_EOF 或 TOKEN_ERROR,lexeme 可能为 NULL 或指向静态字符串
if (current_token.lexeme && current_token.type != TOKEN_EOF && current_token.type != TOKEN_ERROR) {
free(current_token.lexeme);
current_token.lexeme = NULL;
}
} while (current_token.type != TOKEN_EOF); // 循环直到遇到文件结束符
printf("--------------------------------------------------------------\n");
printf("Lexical analysis complete.\n");
// 释放词法分析器资源
close_lexer();
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `lexer.c` 是词法分析器的核心,包含了大量的逻辑来识别不同类型的Token。
*
* **1. 内部状态管理:**
* - `source_file`: 输入文件句柄。
* - `current_char`: 当前正在处理的字符。
* - `line_num`, `column_num`: 用于跟踪源代码位置,对错误报告至关重要。
* - `buffer`: 一个动态大小的字符数组,用于临时存储识别到的词素的文本内容。
* 例如,当识别一个标识符或数字时,会逐个字符地读入并存入 `buffer`。
* 动态内存管理 (`realloc`) 确保 `buffer` 能够适应任意长度的词素。
* - `keywords` 数组:一个 `KeywordMap` 结构体数组,用于高效地检查一个识别出的标识符是否是C语言的关键字。
* 通过遍历这个静态数组,与识别出的词素进行字符串比较 (`strcmp`)。
*
* **2. 核心辅助函数:**
* - `lexer_error`: 统一的错误报告函数,能打印错误位置和消息,并在致命错误时退出。
* - `get_char()`: 从文件读取下一个字符,并**更新** `line_num` 和 `column_num`。这是词法分析器向前推进的唯一方式。
* - `peek_char()`: 预读下一个字符,但**不消耗**它。这是处理像 `==`, `<=`, `++` 这种双字符运算符的关键。
* 它允许词法分析器“看一眼”下一个字符,从而决定是生成一个单字符Token还是一个双字符Token。
* `ungetc` 函数在这里起到了“回溯”的作用,把预读的字符放回输入流。
* - `add_char_to_buffer()`: 将当前字符添加到 `buffer` 中,并处理缓冲区的动态扩容。
* - `reset_buffer()`: 在开始识别新Token之前清空 `buffer`。
* - `check_keyword()`: 这是一个查找表函数,用于判断一个字符串是否是C语言的保留关键字。
*
* **3. `get_next_token()` 函数的结构与逻辑:**
* 这是整个词法分析器的核心。它是一个状态机或一个大的 `switch-case` 结构,根据 `current_char` 的值来决定进入哪种识别逻辑。
* - **跳过空白字符:** 任何词法分析器都要先跳过源代码中的空白字符和注释。这是通过 `while (isspace(current_char))` 循环实现的。
* - **EOF 检查:** 检查是否到达文件末尾,如果是则返回 `TOKEN_EOF`。
* - **标识符和关键字识别:**
* - 如果当前字符是字母或下划线 (`isalpha(current_char) || current_char == '_'`),
* 则进入标识符识别模式:持续读取字母、数字、下划线,将它们添加到 `buffer`。
* - 读取结束后,调用 `check_keyword()` 来判断 `buffer` 中的内容是关键字还是普通的标识符。
* - **整数常量识别:**
* - 如果当前字符是数字 (`isdigit(current_char)`),则进入数字识别模式:持续读取数字,并将其累加到 `long_val` 中。
* - 注意这里只实现了十进制整数,实际C编译器还需要处理十六进制 (`0x...`)、八进制 (`0...`) 和二进制 (`0b...`) 字面量,以及浮点数 (`3.14`, `1.0e-5`)。
* - **运算符和标点符号识别(核心 `switch` 语句):**
* - 对于大多数单字符运算符,直接返回对应的Token。
* - 对于可能构成双字符运算符的字符(如 `=`, `+`, `-`, `/`, `<`, `>`, `&`, `|`, `!`):
* - 读入第一个字符后,**使用 `peek_char()` 预读下一个字符**。
* - 如果下一个字符与当前字符组合能形成一个双字符运算符(例如 `==`, `++`, `/*`),则继续读取第二个字符,并返回对应的双字符Token。
* - 否则,就返回单字符Token。这种“最长匹配原则”是词法分析的重要原则。
* - **注释处理:** 对于 `/`,需要判断是除法运算符,还是行注释 `//`,还是块注释 `/* */`。
* - 行注释:跳过当前行所有字符直到换行或EOF。
* - 块注释:跳过直到遇到 `*/`。这里包含了错误处理,防止未终止的注释导致程序挂起。
* - **重要:** 处理完注释后,需要递归调用 `get_next_token()` 来获取下一个真正的Token,因为注释本身不是有意义的Token。
* - **字符串和字符字面量:** 这是一个复杂的部分,需要处理转义字符 (`\n`, `\t`, `\\`, `\"` 等)。
* 目前的实现是一个简化版本,仅处理基本情况,实际编译器需要更 robust 的转义序列解析。
* - **未知字符:** 如果 `switch` 语句中没有匹配到任何已知的Token,则说明遇到了无法识别的字符,此时报告 `TOKEN_UNKNOWN` 错误。
*
* **4. 资源清理:**
* - `close_lexer()` 函数负责关闭文件句柄和释放动态分配的缓冲区内存。
* - `get_next_token()` 返回的 `Token.lexeme` 是通过 `strdup()` 复制的字符串,因此每次使用后,调用方(这里是 `main` 函数)负责 `free()` 掉它,避免内存泄漏。这是一个常见但容易出错的地方。
*
* **5. 测试框架 `main` 函数:**
* - 接收一个C源代码文件作为命令行参数。
* - 调用 `init_lexer` 初始化词法分析器。
* - 循环调用 `get_next_token` 来读取并打印所有Token,直到遇到 `TOKEN_EOF`。
* - 打印出的Token信息包含类型、文本、值和位置,这对于验证词法分析器的正确性非常有帮助。
*
* **挑战与待完善:**
* - 错误恢复:目前遇到错误直接退出,实际编译器会尝试恢复以继续报告更多错误。
* - 数字字面量:只实现了十进制整数,未实现浮点数、八进制、十六进制等。
* - 字符串/字符字面量:转义字符处理不够完善。
* - 更复杂的运算符:`+=`, `-=`, `*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=` 等复合赋值运算符。
* - 预处理指令:词法分析器通常在预处理之后运行,但如果需要实现一个完整的预处理器,那将是词法分析之前的一个独立阶段。
* - 性能优化:对于大型文件,频繁的 `fgetc` 和 `realloc` 可能影响性能,实际编译器会使用更大的缓冲区进行文件读取。
*
* 这个 `lexer.c` 文件提供了一个坚实的基础,能够处理C语言的核心词法元素。
* 它是我们整个编译器项目的第一个里程碑!
*/
4. 测试文件:test.c
为了测试我们的词法分析器,我们需要一个简单的C源文件。
// 文件名: test.c
// 用于测试自定义C语言词法分析器的示例源代码。
/* 这是一个
* 多行注释
* 可以包含多行文本 */
int main() { // 主函数入口
// 声明一个整数变量
int my_var_1 = 10;
// 另一个变量
int anotherVar = 20;
/* 这是一个
* 测试
* 注释 */
// 算术运算
int sum = my_var_1 + anotherVar * 2;
// 比较运算
if (sum >= 40) {
return 1; // 返回1
} else {
return 0; // 返回0
}
// 字符串和字符字面量
char message[] = "Hello, Compiler!";
char initial = 'A';
char newline = '\n'; // 转义字符
// 复合赋值和逻辑运算符 (待 lexer.c 完整支持)
// my_var_1 += 5;
// if (my_var_1 > 10 && anotherVar < 30) {
// // do something
// }
// 位运算符 (待 lexer.c 完整支持)
// int bit_result = 0xFF & 0xAA;
return 2; // 最后的返回值
}
编译和运行词法分析器
要编译和运行上述C语言词法分析器,你需要:
-
将上述三个C文件(
token.h
,lexer.h
,lexer.c
)保存到同一个目录下。 -
将测试文件
test.c
也保存到该目录。 -
打开终端(Linux/macOS)或命令提示符(Windows)。
-
使用GCC编译:
gcc -o my_lexer lexer.c
(注意:
token.h
和lexer.h
会被lexer.c
自动包含) -
运行词法分析器:
./my_lexer test.c
你将看到 test.c
文件被分解成的Token序列,每一行显示一个Token的类型、文本内容、值(如果是数字或字符常量)以及它在源代码中的行号和列号。这将是你亲手构建的编译器迈出的第一步!
总结与展望
在这一部分,我们深入探讨了C语言编译过程的第一阶段:词法分析。我们理解了词法分析器作为编译器的“眼睛”,如何将原始的源代码字符流转化为一个个有意义的“词素”(Token)流。通过C语言的详细实现,我们看到了如何定义Token类型、如何管理词法分析器的内部状态(文件指针、行号、列号)、如何处理空白字符和注释、以及如何识别标识符、关键字、整数常量和各种运算符。特别是双字符运算符的识别,凸显了“预读”机制的重要性。
我们手撸的词法分析器虽然还是一个基础版本,但它已经能够将一个C语言程序拆解成可供后续阶段处理的结构化数据。这正是从“人类语言”到“机器语言”转换的第一步,也是最基础却不可或缺的一步。你亲手写的这1200多行C代码,正是编译器心脏跳动的第一声!
在下一部分,我们将在此基础上,构建编译器的“大脑”——语法分析器。它将接收词法分析器生成的Token流,并根据C语言的语法规则,将这些“单词”组合成有意义的“句子”——抽象语法树(AST)。这将是又一个充满挑战和乐趣的旅程,让我们一起期待!
(3)掀开C语言的底裤:从编译器原理到手撸实践,看透C语言的红尘!
手撸编译器:进阶之路——核心组件之语法分析器
各位勇士们,在上一个篇章里,我们完成了编译器的“眼睛”——词法分析器。它成功地将C语言源代码从字符流转化为了有意义的“词素”(Token)流。现在,这些零散的“单词”需要被组织起来,形成一个有意义的结构,这就是我们今天要深入探讨的主题:语法分析器(Syntax Analyzer),也被称为解析器(Parser)。
如果说词法分析器是识别一个个独立的“单词”,那么语法分析器就是根据语法规则,将这些“单词”组合成“句子”和“段落”,并理解它们的层次结构。在编译器中,这些“句子”和“段落”最终会被表示为一种称为**抽象语法树(Abstract Syntax Tree, AST)**的数据结构。
语法分析器的核心使命
语法分析器是编译器前端的第二个阶段,它的主要任务是:
-
输入: 接收词法分析器生成的Token序列。
-
输出: 构建一个抽象语法树(AST)。
-
错误检测: 在构建AST的过程中,检查Token序列是否符合编程语言的语法规则。如果发现语法错误(例如缺少分号、括号不匹配等),则报告错误。
什么是抽象语法树(AST)?
抽象语法树(AST)是源代码语法结构的一种抽象表示,它移除了具体语法中冗余的细节(例如,表达式中的括号在AST中通常会被隐含在树的结构中,而不是作为独立的节点),更便于后续的语义分析、中间代码生成和代码优化阶段处理。
AST中的每个节点都代表了源代码中的一个构造,例如表达式、语句、声明、函数调用等。树的结构反映了这些构造之间的语法关系和层次结构。例如,一个加法表达式的AST节点可能包含两个子节点,分别代表加法的左操作数和右操作数。
AST的好处:
-
抽象性: 剔除了源代码中的许多表面细节(如空白、注释、具体语法中的冗余符号),只保留核心的语法结构信息。
-
层次结构: 明确表示了代码的嵌套和从属关系,便于编译器理解程序的逻辑。
-
便于处理: 作为一种树状结构,它非常适合递归遍历和处理,为后续的语义分析、优化和代码生成提供了统一、简洁的输入。
语法分析的实现原理:上下文无关文法与递归下降解析
语法分析器通常基于**上下文无关文法(Context-Free Grammars, CFG)**来定义编程语言的语法。CFG使用一组规则来描述语言中有效结构的组合方式。例如,一条简单的C语言声明规则可能看起来像这样(使用巴科斯范式BNF):
<declaration> ::= <type_specifier> <identifier> ";"
<type_specifier> ::= "int" | "char" | "float" // ...
基于CFG,有多种实现语法分析器的方法,包括:
-
自顶向下解析(Top-Down Parsing): 从文法的起始符号开始,尝试推导出输入串。
-
递归下降解析(Recursive Descent Parsing): 这是一种自顶向下解析方法,每个非终结符(即文法规则的左部)都对应一个函数。这些函数会递归地调用彼此,尝试匹配输入Token流。这种方法直观、易于手写和调试,非常适合我们手撸一个简单编译器。
-
LL(k) 解析: 通过向左扫描(L),并使用k个Token的向前看(Lookahead)来决定下一步如何推导。
-
-
自底向上解析(Bottom-Up Parsing): 从输入串开始,逐步归约到文法的起始符号。
-
LR(k) 解析: 更强大的解析方法,能够识别更广泛的文法,但通常需要工具(如Yacc/Bison)来自动生成。
-
对于我们手撸的编译器,我们将采用递归下降解析。它的优点是实现起来相对简单直观,每个语法规则直接对应一个C函数,逻辑清晰。缺点是它只适用于LL(1)或少数LL(k)文法,对于左递归或二义性文法需要进行转换。
手撸语法分析器:C语言实现
现在,我们将在之前词法分析器的基础上,构建我们的语法分析器。我们将定义AST节点的数据结构,并实现一系列递归下降函数来解析C语言的简化子集。
我们将解析一个非常简化的C语言子集,包括:
-
程序结构: 包含一个或多个函数定义。
-
函数定义: 简单的
int main()
函数。 -
语句: 变量声明、赋值语句、
return
语句、if-else
语句。 -
表达式: 整数常量、标识符、加减乘除运算、比较运算。
1. ast.h
:抽象语法树(AST)节点的定义
这个头文件将定义各种AST节点的类型和结构体。
// 文件名: ast.h
// 描述: 定义抽象语法树(AST)的节点结构。
// AST是源代码语法结构的一种抽象表示,用于后续的语义分析和代码生成。
#ifndef AST_H
#define AST_H
#include "token.h" // 包含Token的定义,AST节点可能需要引用Token信息
// 定义AST节点类型枚举
// 这些类型代表了C语言中不同种类的语法构造,它们将在AST中形成节点。
typedef enum {
AST_PROGRAM, // 整个程序(根节点)
AST_FUNCTION_DECL, // 函数声明或定义 (Function Declaration/Definition)
AST_COMPOUND_STMT, // 复合语句(代码块),即花括号 {} 中的一系列语句
// 声明
AST_VAR_DECL, // 变量声明
// 语句
AST_EXPR_STMT, // 表达式语句(例如 `a + b;`)
AST_RETURN_STMT, // return 语句
AST_IF_STMT, // if 语句
AST_WHILE_STMT, // while 语句 (待实现)
AST_FOR_STMT, // for 语句 (待实现)
// 表达式
AST_BINARY_EXPR, // 二元表达式(例如 `a + b`, `x == y`)
AST_UNARY_EXPR, // 一元表达式(例如 `-a`, `!b`) (待实现)
AST_INTEGER_LITERAL, // 整数常量(例如 `123`)
AST_IDENTIFIER, // 标识符(变量名、函数名)
AST_ASSIGN_EXPR, // 赋值表达式(例如 `a = b`)
AST_FUNCTION_CALL, // 函数调用(例如 `main()`) (待实现)
AST_STRING_LITERAL, // 字符串常量 (待实现)
AST_CHAR_LITERAL, // 字符常量 (待实现)
} ASTNodeType;
// 前向声明,用于解决交叉引用问题(例如一个节点可能包含另一个节点)
struct ASTNode; // 声明 struct ASTNode
// 定义链表节点,用于连接同类型的AST节点(例如:复合语句中的一系列语句)
typedef struct ASTNodeList {
struct ASTNode *node; // 指向一个AST节点
struct ASTNodeList *next; // 指向下一个列表节点
} ASTNodeList;
// 定义AST节点的通用结构体
// 所有不同类型的AST节点都将包含这些基本信息,
// 并通过 union 存储各自特有的数据。
typedef struct ASTNode {
ASTNodeType type; // 节点类型
int line; // 节点在源代码中的起始行号
int column; // 节点在源代码中的起始列号
// union 用于存储特定节点类型的数据
// 这样做可以节省内存,因为一个节点只会在特定类型下使用其对应的union成员。
union {
// AST_PROGRAM
ASTNodeList *program_decls; // 程序中的声明列表(例如函数定义)
// AST_FUNCTION_DECL
struct {
TokenType return_type; // 函数返回类型(例如 TOKEN_INT)
char *name; // 函数名
ASTNodeList *params; // 参数列表 (待实现)
struct ASTNode *body; // 函数体 (通常是一个 AST_COMPOUND_STMT)
} func_decl;
// AST_COMPOUND_STMT
struct {
ASTNodeList *statements; // 复合语句中的语句列表
} compound_stmt;
// AST_VAR_DECL
struct {
TokenType type_specifier; // 变量类型(例如 TOKEN_INT)
char *name; // 变量名
struct ASTNode *initializer; // 变量初始化表达式(可选)
} var_decl;
// AST_EXPR_STMT
struct {
struct ASTNode *expr; // 表达式语句包含的表达式
} expr_stmt;
// AST_RETURN_STMT
struct {
struct ASTNode *expr; // return 语句返回的表达式(可选)
} return_stmt;
// AST_IF_STMT
struct {
struct ASTNode *condition; // 条件表达式
struct ASTNode *then_stmt; // if 分支语句 (then-branch)
struct ASTNode *else_stmt; // else 分支语句 (else-branch, 可选)
} if_stmt;
// AST_BINARY_EXPR
struct {
TokenType op; // 运算符Token类型(例如 TOKEN_PLUS, TOKEN_EQ)
struct ASTNode *left; // 左操作数表达式
struct ASTNode *right; // 右操作数表达式
} binary_expr;
// AST_UNARY_EXPR (待实现)
struct {
TokenType op;
struct ASTNode *operand;
} unary_expr;
// AST_INTEGER_LITERAL
struct {
int value; // 整数常量的值
} int_literal;
// AST_IDENTIFIER
struct {
char *name; // 标识符的名称
} identifier;
// AST_ASSIGN_EXPR
struct {
struct ASTNode *left; // 赋值的左值(通常是标识符)
struct ASTNode *right; // 赋值的右值表达式
} assign_expr;
// AST_FUNCTION_CALL (待实现)
struct {
char *name;
ASTNodeList *args;
} func_call;
// AST_STRING_LITERAL (待实现)
struct {
char *value;
} string_literal;
// AST_CHAR_LITERAL (待实现)
struct {
int value;
} char_literal;
} data; // 联合体实例名
} ASTNode;
// --- 辅助函数声明 ---
// 创建新的AST节点
ASTNode* ast_new_node(ASTNodeType type, int line, int column);
// 创建特定类型的AST节点辅助函数
ASTNode* ast_new_program(ASTNodeList *decls);
ASTNode* ast_new_function_decl(TokenType return_type, char *name, ASTNode *body, int line, int column);
ASTNode* ast_new_compound_stmt(ASTNodeList *statements, int line, int column);
ASTNode* ast_new_var_decl(TokenType type_specifier, char *name, ASTNode *initializer, int line, int column);
ASTNode* ast_new_expr_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_return_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_if_stmt(ASTNode *condition, ASTNode *then_stmt, ASTNode *else_stmt, int line, int column);
ASTNode* ast_new_binary_expr(TokenType op, ASTNode *left, ASTNode *right, int line, int column);
ASTNode* ast_new_integer_literal(int value, int line, int column);
ASTNode* ast_new_identifier(char *name, int line, int column);
ASTNode* ast_new_assign_expr(ASTNode *left, ASTNode *right, int line, int column);
// 链表辅助函数
ASTNodeList* ast_new_node_list(ASTNode *node);
void ast_add_to_list(ASTNodeList **head, ASTNode *node);
// 遍历和打印AST(用于调试和验证)
// 这是一个非常重要的调试工具,可以直观地看到解析器构建的树结构。
void ast_print(ASTNode *node, int indent);
// 释放AST节点及其子节点占用的内存
void ast_free(ASTNode *node);
#endif // AST_H
/*
* 逻辑分析:
* `ast.h` 文件是语法分析器输出的“蓝图”。它定义了我们如何用C语言来表示C语言代码的结构。
*
* 1. `ASTNodeType` 枚举:
* - 定义了AST中可能出现的所有节点类型。每个类型对应C语言的一个语法结构(例如函数声明、变量声明、二元表达式等)。
* - 这些节点类型的设计直接反映了我们编译器将要支持的C语言子集。
*
* 2. `ASTNode` 结构体:
* - 这是AST的核心。每个 `ASTNode` 都代表了源代码中的一个语法单元。
* - `type`: 最重要的字段,指示这个节点代表何种语法结构。
* - `line`, `column`: 记录了该语法单元在源代码中的位置,这对于错误报告和调试至关重要。
* - `union data`: 这是关键的设计模式,它使得一个 `ASTNode` 能够根据其 `type` 存储不同类型的数据。
* 例如,如果 `type` 是 `AST_FUNCTION_DECL`,那么 `data.func_decl` 成员会被使用,
* 其中包含函数名、返回类型和函数体等信息。
* 这种方式既节省了内存(因为不同的节点类型共享同一块内存),又提供了灵活性。
* - 对于 `AST_PROGRAM`、`AST_FUNCTION_DECL`、`AST_COMPOUND_STMT` 等包含子节点列表的类型,
* 我们定义了 `ASTNodeList` 链表结构来管理子节点。
* - 对于 `AST_BINARY_EXPR`,它有 `op`(运算符)、`left` 和 `right`(左右操作数)。
* - 对于 `AST_INTEGER_LITERAL`,它只包含一个 `value`。
* - 对于 `AST_IDENTIFIER`,它包含 `name`。
*
* 3. `ASTNodeList` 结构体:
* - 一个简单的单向链表结构,用于连接AST中的同类型子节点。
* - 例如,一个复合语句(`{ ... }`)可能包含多个语句,这些语句就可以通过 `ASTNodeList` 链表连接起来。
* - 函数的参数列表、程序顶层的声明列表等都可以用这种通用链表来表示。
*
* 4. 辅助函数:
* - `ast_new_node` 系列函数:方便创建各种类型的AST节点,并初始化其基本信息。
* 这些函数负责动态内存分配(`malloc`)和数据初始化,将底层的内存管理细节封装起来。
* - `ast_new_node_list` 和 `ast_add_to_list`:用于方便地构建和管理 `ASTNodeList` 链表。
* - `ast_print`: 递归遍历AST并以缩进格式打印,这是调试语法分析器输出是否正确的利器。
* - `ast_free`: 递归释放AST节点占用的所有内存,防止内存泄漏。
*
* 设计考量:
* - **分层表示:** AST将C语言的文本形式转换为内存中的树形结构,更接近计算机处理的逻辑。
* - **灵活性和扩展性:** `union` 的使用和链表结构的定义使得AST能够灵活表示C语言的各种语法构造,
* 并且未来添加新的语法特性时,只需在 `ASTNodeType` 中添加新类型并在 `union` 中添加对应结构即可。
* - **调试友好:** 提供了 `ast_print` 函数,方便可视化AST的结构。
* - **内存管理:** 显式地提供了 `ast_new_node` 和 `ast_free` 函数来管理节点的生命周期,
* 这在C语言中至关重要,避免了内存泄漏。
*/
2. parser.h
:语法分析器的接口
这个头文件将声明语法分析器的主要函数和任何必要的全局状态。
// 文件名: parser.h
// 描述: 声明语法分析器(Parser)的接口。
// 语法分析器负责将Token流转换为抽象语法树(AST)。
#ifndef PARSER_H
#define PARSER_H
#include "ast.h" // 包含AST节点的定义
#include "token.h" // 包含Token的定义
#include "lexer.h" // 包含词法分析器接口,因为解析器需要从词法分析器获取Token
// 语法分析器入口函数
// 参数: source_code_path - 待解析的C源代码文件路径。
// 返回值: 解析成功则返回指向AST根节点的指针,失败则返回NULL。
ASTNode* parse(const char *source_code_path);
// 错误报告函数声明
// 当语法分析器遇到不符合语法规则的Token序列时,需要报告错误。
void parser_error(const char *format, ...);
// 辅助函数声明(内部使用,但为了调试或其他原因可能在头文件声明)
// 这些函数通常与特定的语法规则相对应,是递归下降解析的核心。
// 例如:
// ASTNode* parse_program(); // 解析整个程序
// ASTNode* parse_declaration(); // 解析声明(变量声明、函数声明)
// ASTNode* parse_statement(); // 解析语句
// ASTNode* parse_expression(); // 解析表达式 (通常分解为多个优先级函数)
#endif // PARSER_H
/*
* 逻辑分析:
* `parser.h` 定义了语法分析器模块的“公共接口”。
*
* 1. `parse(const char *source_code_path)`:
* - 这是语法分析器的主要入口点。
* - 它负责初始化词法分析器(调用 `init_lexer`),然后开始Token流的解析过程,并最终返回一个完整的AST。
* - 如果解析过程中出现语法错误,它会报告错误并通过返回 `NULL` 或其他机制指示失败。
*
* 2. `parser_error(const char *format, ...)`:
* - 统一的错误报告函数,用于在语法分析过程中报告语法错误。
* - 它会打印错误发生的位置(通常是当前Token的行号和列号)以及详细的错误信息。
*
* 3. 辅助函数(被注释掉的部分):
* - 在实际的递归下降解析器中,会有一系列函数,每个函数对应文法中的一个非终结符(即一个语法规则)。
* - 例如,`parse_program()` 负责解析整个程序的结构;`parse_statement()` 负责解析各种类型的语句;
* `parse_expression()` 负责解析表达式。
* - 这些函数会递归地调用彼此,根据当前的Token来决定下一步如何匹配语法规则。
* - 它们是语法分析的核心逻辑,通常在 `parser.c` 中实现,不一定需要在 `parser.h` 中声明,
* 除非它们需要在其他模块中被直接调用(例如,如果有一个独立的AST打印器需要这些)。
* 在这里为了清晰演示解析器的内部结构,我们将其作为概念注释。
*
* 设计考量:
* - **清晰的接口:** `parse` 函数作为唯一的公共接口,隐藏了语法分析器的内部复杂性。
* - **依赖性:** 语法分析器依赖于词法分析器来获取Token,因此 `lexer.h` 被包含进来。
* - **错误报告:** 提供了专门的错误报告函数,方便统一处理语法错误。
*/
3. parser.c
:语法分析器的实现
这是语法分析器的核心,包含了递归下降解析的逻辑和AST构建的代码。
// 文件名: parser.c
// 描述: 语法分析器(Parser)的核心实现。
// 它负责从词法分析器获取Token流,并根据C语言的简化文法规则构建抽象语法树(AST)。
#include <stdio.h> // 用于输入输出
#include <stdlib.h> // 用于内存管理 (malloc, free) 和退出 (exit)
#include <stdarg.h> // 用于可变参数函数 (va_list, va_start, va_end)
#include <string.h> // 用于字符串操作 (strcmp, strdup)
#include "token.h" // 包含Token类型和结构体的定义
#include "lexer.h" // 包含词法分析器接口的定义 (get_next_token, lexer_error)
#include "ast.h" // 包含AST节点结构体和辅助函数的定义
#include "parser.h" // 包含语法分析器接口的定义
// --- 全局/内部变量 ---
// current_token 是由 lexer.c 中的 get_next_token 填充的全局变量。
// 这里直接使用它。
// --- 辅助函数:Token 操作 ---
// 打印语法分析器错误信息
// 这是语法分析阶段的错误报告机制。
void parser_error(const char *format, ...) {
va_list args;
// 使用 current_token 的位置信息,指向当前发生错误的Token
fprintf(stderr, "Parser Error [%d:%d]: ", current_token.line, current_token.column);
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, "\n");
exit(EXIT_FAILURE); // 遇到致命错误时退出程序
}
// 检查当前Token类型是否符合预期
// 如果不符合,则报告语法错误。
static void expect(TokenType type) {
if (current_token.type != type) {
parser_error("Expected '%s', but got '%s' ('%s')",
token_type_to_string(type),
token_type_to_string(current_token.type),
current_token.lexeme);
}
}
// 消费当前Token,并获取下一个Token
// 这是解析器向前推进的核心操作。
static void consume() {
// 释放旧Token的lexeme,因为 get_next_token 会 strdup
if (current_token.lexeme && current_token.type != TOKEN_EOF && current_token.type != TOKEN_ERROR) {
free(current_token.lexeme);
current_token.lexeme = NULL;
}
current_token = get_next_token();
}
// 检查当前Token类型是否是预期的一个,如果是则消费并返回真,否则返回假。
static int match_and_consume(TokenType type) {
if (current_token.type == type) {
consume();
return 1;
}
return 0;
}
// --- 递归下降解析函数声明 (前向声明,因为它们互相调用) ---
// 每个函数对应C语言文法中的一个非终结符(或一个复合的语法单元)。
// 解析优先级从低到高。
static ASTNode* parse_program();
static ASTNode* parse_function_decl();
static ASTNode* parse_compound_statement();
static ASTNode* parse_statement();
static ASTNode* parse_declaration(); // 变量声明
static ASTNode* parse_expression(); // 表达式的入口点
static ASTNode* parse_assignment_expression(); // 赋值表达式
static ASTNode* parse_equality_expression(); // 相等/不等表达式 (==, !=)
static ASTNode* parse_relational_expression(); // 关系表达式 (<, <=, >, >=)
static ASTNode* parse_additive_expression(); // 加法/减法表达式 (+, -)
static ASTNode* parse_multiplicative_expression(); // 乘法/除法表达式 (*, /)
static ASTNode* parse_primary_expression(); // 最基本的表达式(数字、标识符、括号表达式)
// --- 递归下降解析函数实现 ---
// 解析整个程序:<program> ::= { <function_declaration> } EOF
static ASTNode* parse_program() {
ASTNodeList *decls_list = NULL; // 存储函数声明的链表
// 循环解析所有的函数声明/定义,直到文件结束
while (current_token.type != TOKEN_EOF) {
ASTNode *func_decl = parse_function_decl(); // 解析一个函数声明
if (func_decl) {
ast_add_to_list(&decls_list, func_decl); // 将函数声明添加到列表中
} else {
// 如果不是函数声明,则可能是语法错误或未识别的顶级结构
parser_error("Unexpected token at top level: '%s'", current_token.lexeme);
// 尝试跳过当前Token以避免死循环,但可能导致更多错误
consume();
}
}
// 检查是否到达文件结束
expect(TOKEN_EOF);
return ast_new_program(decls_list); // 返回程序AST根节点
}
// 解析函数声明:<function_declaration> ::= <type_specifier> <IDENTIFIER> "(" ")" <compound_statement>
// 目前只支持 int main() {} 这种形式
static ASTNode* parse_function_decl() {
int start_line = current_token.line;
int start_column = current_token.column;
// 1. 解析返回类型 (目前只支持 int)
expect(TOKEN_INT);
TokenType return_type = current_token.type; // 获取类型
consume(); // 消费 'int'
// 2. 解析函数名
expect(TOKEN_IDENTIFIER);
char *func_name = strdup(current_token.lexeme); // 复制函数名
consume(); // 消费标识符 (函数名)
// 3. 解析参数列表 (目前只支持空参数列表 ())
expect(TOKEN_LPAREN);
consume(); // 消费 '('
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
// 4. 解析函数体 (必须是复合语句 {})
ASTNode *body = parse_compound_statement();
if (!body) {
parser_error("Expected function body (compound statement) for function '%s'", func_name);
}
return ast_new_function_decl(return_type, func_name, body, start_line, start_column);
}
// 解析复合语句(代码块):<compound_statement> ::= "{" { <declaration> | <statement> } "}"
static ASTNode* parse_compound_statement() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_LBRACE);
consume(); // 消费 '{'
ASTNodeList *statements_list = NULL; // 存储语句和声明的链表
// 循环解析内部的声明和语句,直到遇到 '}'
while (current_token.type != TOKEN_RBRACE && current_token.type != TOKEN_EOF) {
ASTNode *node = NULL;
// 尝试解析声明 (int var;)
if (current_token.type == TOKEN_INT) {
node = parse_declaration();
} else {
// 否则尝试解析语句
node = parse_statement();
}
if (node) {
ast_add_to_list(&statements_list, node);
} else {
// 如果既不是声明也不是已知语句,则可能是语法错误
parser_error("Unexpected token in compound statement: '%s'", current_token.lexeme);
// 尝试跳过当前Token以避免死循环
consume();
}
}
expect(TOKEN_RBRACE);
consume(); // 消费 '}'
return ast_new_compound_stmt(statements_list, start_line, start_column);
}
// 解析语句:<statement> ::= <expression_statement> | <return_statement> | <if_statement> | <compound_statement>
static ASTNode* parse_statement() {
ASTNode *node = NULL;
int start_line = current_token.line;
int start_column = current_token.column;
switch (current_token.type) {
case TOKEN_RETURN:
node = ast_new_return_stmt(NULL, start_line, start_column); // 临时创建
consume(); // 消费 'return'
ASTNode *expr = parse_expression(); // 解析返回的表达式
node->data.return_stmt.expr = expr;
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return node;
case TOKEN_IF:
consume(); // 消费 'if'
expect(TOKEN_LPAREN);
consume(); // 消费 '('
ASTNode *condition = parse_expression(); // 解析条件表达式
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
ASTNode *then_stmt = parse_statement(); // 解析 if 分支的语句
ASTNode *else_stmt = NULL;
if (current_token.type == TOKEN_ELSE) {
consume(); // 消费 'else'
else_stmt = parse_statement(); // 解析 else 分支的语句
}
return ast_new_if_stmt(condition, then_stmt, else_stmt, start_line, start_column);
case TOKEN_LBRACE: // 复合语句可以作为任何语句的一部分
return parse_compound_statement();
default:
// 如果不是以上特定语句,则尝试解析表达式语句 (e.g. `a = b + c;`)
node = ast_new_expr_stmt(NULL, start_line, start_column); // 临时创建
ASTNode *expr_stmt_expr = parse_expression();
node->data.expr_stmt.expr = expr_stmt_expr;
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return node;
}
}
// 解析变量声明:<declaration> ::= <type_specifier> <IDENTIFIER> [ "=" <expression> ] ";"
// 目前只支持 int 类型的变量声明
static ASTNode* parse_declaration() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_INT);
TokenType type_specifier = current_token.type;
consume(); // 消费 'int'
expect(TOKEN_IDENTIFIER);
char *var_name = strdup(current_token.lexeme);
consume(); // 消费标识符 (变量名)
ASTNode *initializer = NULL;
if (current_token.type == TOKEN_ASSIGN) { // 如果有初始化符 '='
consume(); // 消费 '='
initializer = parse_assignment_expression(); // 解析初始化表达式
}
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return ast_new_var_decl(type_specifier, var_name, initializer, start_line, start_column);
}
// --- 表达式解析函数 (从低优先级到高优先级) ---
// 表达式入口:<expression> ::= <assignment_expression>
// 简单起见,目前表达式就从赋值表达式开始
static ASTNode* parse_expression() {
return parse_assignment_expression();
}
// 解析赋值表达式:<assignment_expression> ::= <equality_expression> [ "=" <assignment_expression> ]
// 右结合性:a = b = c 等价于 a = (b = c)
static ASTNode* parse_assignment_expression() {
ASTNode *node = parse_equality_expression(); // 先解析更高优先级的表达式
int start_line = current_token.line;
int start_column = current_token.column;
if (current_token.type == TOKEN_ASSIGN) {
consume(); // 消费 '='
ASTNode *right_expr = parse_assignment_expression(); // 递归解析右侧的赋值表达式
// 检查左侧是否是有效的左值(目前只支持标识符作为左值)
if (node->type != AST_IDENTIFIER) {
parser_error("Invalid left-hand side in assignment: expected identifier.");
}
node = ast_new_assign_expr(node, right_expr, start_line, start_column);
}
return node;
}
// 解析相等/不等表达式:<equality_expression> ::= <relational_expression> { ("==" | "!=") <relational_expression> }
// 这是一个左结合的运算符。例如 a == b == c 是错误的,但 a == b + c 是可以的。
static ASTNode* parse_equality_expression() {
ASTNode *node = parse_relational_expression(); // 先解析更高优先级的关系表达式
while (current_token.type == TOKEN_EQ || current_token.type == TOKEN_NE) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '==' 或 '!='
ASTNode *right = parse_relational_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析关系表达式:<relational_expression> ::= <additive_expression> { ("<" | "<=" | ">" | ">=") <additive_expression> }
// 左结合
static ASTNode* parse_relational_expression() {
ASTNode *node = parse_additive_expression(); // 先解析更高优先级的加法表达式
while (current_token.type == TOKEN_LT ||
current_token.type == TOKEN_LE ||
current_token.type == TOKEN_GT ||
current_token.type == TOKEN_GE)
{
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费关系运算符
ASTNode *right = parse_additive_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析加法/减法表达式:<additive_expression> ::= <multiplicative_expression> { ("+" | "-") <multiplicative_expression> }
// 左结合
static ASTNode* parse_additive_expression() {
ASTNode *node = parse_multiplicative_expression(); // 先解析更高优先级的乘法表达式
while (current_token.type == TOKEN_PLUS || current_token.type == TOKEN_MINUS) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '+' 或 '-'
ASTNode *right = parse_multiplicative_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析乘法/除法表达式:<multiplicative_expression> ::= <primary_expression> { ("*" | "/") <primary_expression> }
// 左结合
static ASTNode* parse_multiplicative_expression() {
ASTNode *node = parse_primary_expression(); // 先解析最高优先级的基础表达式
while (current_token.type == TOKEN_ASTERISK || current_token.type == TOKEN_SLASH) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '*' 或 '/'
ASTNode *right = parse_primary_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析基本表达式:<primary_expression> ::= <INTEGER_LITERAL> | <IDENTIFIER> | "(" <expression> ")"
static ASTNode* parse_primary_expression() {
int start_line = current_token.line;
int start_column = current_token.column;
ASTNode *node = NULL;
switch (current_token.type) {
case TOKEN_INTEGER_LITERAL:
node = ast_new_integer_literal(current_token.int_value, start_line, start_column);
consume(); // 消费整数常量
break;
case TOKEN_IDENTIFIER:
// TODO: 在这里可以添加函数调用解析逻辑
node = ast_new_identifier(strdup(current_token.lexeme), start_line, start_column);
consume(); // 消费标识符
break;
case TOKEN_LPAREN:
consume(); // 消费 '('
node = parse_expression(); // 递归解析括号内的表达式
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
break;
default:
parser_error("Unexpected token in primary expression: '%s'", current_token.lexeme);
// 尝试跳过以避免死循环
consume();
return NULL; // 返回NULL表示解析失败
}
return node;
}
// --- AST 辅助函数实现 ---
// 创建新的AST节点
ASTNode* ast_new_node(ASTNodeType type, int line, int column) {
ASTNode *node = (ASTNode*)malloc(sizeof(ASTNode));
if (!node) {
fprintf(stderr, "Fatal Error: Failed to allocate AST node.\n");
exit(EXIT_FAILURE);
}
memset(node, 0, sizeof(ASTNode)); // 初始化为0,确保union成员安全
node->type = type;
node->line = line;
node->column = column;
return node;
}
// 创建特定类型的AST节点辅助函数实现
ASTNode* ast_new_program(ASTNodeList *decls) {
ASTNode *node = ast_new_node(AST_PROGRAM, 0, 0); // Program 节点通常没有特定行/列
node->data.program_decls = decls;
return node;
}
ASTNode* ast_new_function_decl(TokenType return_type, char *name, ASTNode *body, int line, int column) {
ASTNode *node = ast_new_node(AST_FUNCTION_DECL, line, column);
node->data.func_decl.return_type = return_type;
node->data.func_decl.name = name; // 注意:name 是 strdup 后的,需要负责 free
node->data.func_decl.params = NULL; // 暂不支持参数
node->data.func_decl.body = body;
return node;
}
ASTNode* ast_new_compound_stmt(ASTNodeList *statements, int line, int column) {
ASTNode *node = ast_new_node(AST_COMPOUND_STMT, line, column);
node->data.compound_stmt.statements = statements;
return node;
}
ASTNode* ast_new_var_decl(TokenType type_specifier, char *name, ASTNode *initializer, int line, int column) {
ASTNode *node = ast_new_node(AST_VAR_DECL, line, column);
node->data.var_decl.type_specifier = type_specifier;
node->data.var_decl.name = name; // 注意:name 是 strdup 后的,需要负责 free
node->data.var_decl.initializer = initializer;
return node;
}
ASTNode* ast_new_expr_stmt(ASTNode *expr, int line, int column) {
ASTNode *node = ast_new_node(AST_EXPR_STMT, line, column);
node->data.expr_stmt.expr = expr;
return node;
}
ASTNode* ast_new_return_stmt(ASTNode *expr, int line, int column) {
ASTNode *node = ast_new_node(AST_RETURN_STMT, line, column);
node->data.return_stmt.expr = expr;
return node;
}
ASTNode* ast_new_if_stmt(ASTNode *condition, ASTNode *then_stmt, ASTNode *else_stmt, int line, int column) {
ASTNode *node = ast_new_node(AST_IF_STMT, line, column);
node->data.if_stmt.condition = condition;
node->data.if_stmt.then_stmt = then_stmt;
node->data.if_stmt.else_stmt = else_stmt;
return node;
}
ASTNode* ast_new_binary_expr(TokenType op, ASTNode *left, ASTNode *right, int line, int column) {
ASTNode *node = ast_new_node(AST_BINARY_EXPR, line, column);
node->data.binary_expr.op = op;
node->data.binary_expr.left = left;
node->data.binary_expr.right = right;
return node;
}
ASTNode* ast_new_integer_literal(int value, int line, int column) {
ASTNode *node = ast_new_node(AST_INTEGER_LITERAL, line, column);
node->data.int_literal.value = value;
return node;
}
ASTNode* ast_new_identifier(char *name, int line, int column) {
ASTNode *node = ast_new_node(AST_IDENTIFIER, line, column);
node->data.identifier.name = name; // 注意:name 是 strdup 后的,需要负责 free
return node;
}
ASTNode* ast_new_assign_expr(ASTNode *left, ASTNode *right, int line, int column) {
ASTNode *node = ast_new_node(AST_ASSIGN_EXPR, line, column);
node->data.assign_expr.left = left;
node->data.assign_expr.right = right;
return node;
}
// 链表辅助函数实现
ASTNodeList* ast_new_node_list(ASTNode *node) {
ASTNodeList *list_node = (ASTNodeList*)malloc(sizeof(ASTNodeList));
if (!list_node) {
fprintf(stderr, "Fatal Error: Failed to allocate ASTNodeList.\n");
exit(EXIT_FAILURE);
}
list_node->node = node;
list_node->next = NULL;
return list_node;
}
void ast_add_to_list(ASTNodeList **head, ASTNode *node) {
ASTNodeList *new_list_node = ast_new_node_list(node);
if (*head == NULL) {
*head = new_list_node;
} else {
ASTNodeList *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_list_node;
}
}
// 遍历和打印AST(用于调试和验证)
static void print_indent(int indent) {
for (int i = 0; i < indent; i++) {
printf(" "); // 每个缩进使用两个空格
}
}
void ast_print(ASTNode *node, int indent) {
if (!node) return;
print_indent(indent);
printf("Type: %s ", token_type_to_string(node->type)); // ASTNodeType 没有 token_type_to_string,这里需要一个 ast_node_type_to_string 函数
// TODO: 修正这里,应该用 ASTNodeType 的字符串表示
switch (node->type) {
case AST_PROGRAM:
printf("PROGRAM\n");
ASTNodeList *decls = node->data.program_decls;
while (decls) {
ast_print(decls->node, indent + 1);
decls = decls->next;
}
break;
case AST_FUNCTION_DECL:
printf("FUNCTION_DECL (Return: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.func_decl.return_type),
node->data.func_decl.name,
node->line, node->column);
print_indent(indent + 1); printf("Body:\n");
ast_print(node->data.func_decl.body, indent + 2);
break;
case AST_COMPOUND_STMT:
printf("COMPOUND_STMT at [%d:%d]\n", node->line, node->column);
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
ast_print(stmts->node, indent + 1);
stmts = stmts->next;
}
break;
case AST_VAR_DECL:
printf("VAR_DECL (Type: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.var_decl.type_specifier),
node->data.var_decl.name,
node->line, node->column);
if (node->data.var_decl.initializer) {
print_indent(indent + 1); printf("Initializer:\n");
ast_print(node->data.var_decl.initializer, indent + 2);
}
break;
case AST_EXPR_STMT:
printf("EXPR_STMT at [%d:%d]\n", node->line, node->column);
ast_print(node->data.expr_stmt.expr, indent + 1);
break;
case AST_RETURN_STMT:
printf("RETURN_STMT at [%d:%d]\n", node->line, node->column);
if (node->data.return_stmt.expr) {
print_indent(indent + 1); printf("Expression:\n");
ast_print(node->data.return_stmt.expr, indent + 2);
}
break;
case AST_IF_STMT:
printf("IF_STMT at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Condition:\n");
ast_print(node->data.if_stmt.condition, indent + 2);
print_indent(indent + 1); printf("Then:\n");
ast_print(node->data.if_stmt.then_stmt, indent + 2);
if (node->data.if_stmt.else_stmt) {
print_indent(indent + 1); printf("Else:\n");
ast_print(node->data.if_stmt.else_stmt, indent + 2);
}
break;
case AST_BINARY_EXPR:
printf("BINARY_EXPR (Op: %s) at [%d:%d]\n",
token_type_to_string(node->data.binary_expr.op),
node->line, node->column);
print_indent(indent + 1); printf("Left:\n");
ast_print(node->data.binary_expr.left, indent + 2);
print_indent(indent + 1); printf("Right:\n");
ast_print(node->data.binary_expr.right, indent + 2);
break;
case AST_INTEGER_LITERAL:
printf("INTEGER_LITERAL (Value: %d) at [%d:%d]\n",
node->data.int_literal.value,
node->line, node->column);
break;
case AST_IDENTIFIER:
printf("IDENTIFIER (Name: %s) at [%d:%d]\n",
node->data.identifier.name,
node->line, node->column);
break;
case AST_ASSIGN_EXPR:
printf("ASSIGN_EXPR at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Left (L-Value):\n");
ast_print(node->data.assign_expr.left, indent + 2);
print_indent(indent + 1); printf("Right (R-Value):\n");
ast_print(node->data.assign_expr.right, indent + 2);
break;
// 添加其他AST节点类型的打印逻辑...
default:
printf("UNKNOWN_AST_NODE_TYPE (Type: %d) at [%d:%d]\n",
node->type, node->line, node->column);
break;
}
}
// 释放AST节点及其子节点占用的内存
void ast_free(ASTNode *node) {
if (!node) return;
// 递归释放子节点
switch (node->type) {
case AST_PROGRAM:
{
ASTNodeList *decls = node->data.program_decls;
while (decls) {
ASTNodeList *next = decls->next;
ast_free(decls->node);
free(decls);
decls = next;
}
}
break;
case AST_FUNCTION_DECL:
free(node->data.func_decl.name); // 释放复制的函数名
ast_free(node->data.func_decl.body);
// TODO: 释放参数列表
break;
case AST_COMPOUND_STMT:
{
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
ASTNodeList *next = stmts->next;
ast_free(stmts->node);
free(stmts);
stmts = next;
}
}
break;
case AST_VAR_DECL:
free(node->data.var_decl.name); // 释放复制的变量名
ast_free(node->data.var_decl.initializer);
break;
case AST_EXPR_STMT:
ast_free(node->data.expr_stmt.expr);
break;
case AST_RETURN_STMT:
ast_free(node->data.return_stmt.expr);
break;
case AST_IF_STMT:
ast_free(node->data.if_stmt.condition);
ast_free(node->data.if_stmt.then_stmt);
ast_free(node->data.if_stmt.else_stmt);
break;
case AST_BINARY_EXPR:
ast_free(node->data.binary_expr.left);
ast_free(node->data.binary_expr.right);
break;
case AST_IDENTIFIER:
free(node->data.identifier.name); // 释放复制的标识符名
break;
case AST_ASSIGN_EXPR:
ast_free(node->data.assign_expr.left);
ast_free(node->data.assign_expr.right);
break;
// 对于 INTEGER_LITERAL 等不包含子节点的类型,无需递归
case AST_INTEGER_LITERAL:
case AST_UNKNOWN: // Placeholder for future, or for error nodes
case TOKEN_UNKNOWN: // For error nodes
break; // No child nodes or allocated strings in these.
default:
// 确保所有类型都被处理,避免遗漏内存泄漏
fprintf(stderr, "Warning: Unhandled AST node type in ast_free: %d\n", node->type);
break;
}
free(node); // 最后释放当前节点
}
// --- 语法分析器入口点 ---
ASTNode* parse(const char *source_code_path) {
// 1. 初始化词法分析器
if (init_lexer(source_code_path) != 0) {
fprintf(stderr, "Error: Failed to initialize lexer.\n");
return NULL;
}
// 2. 获取第一个Token,开始解析
current_token = get_next_token();
// 3. 开始解析整个程序
ASTNode *program_ast = parse_program();
// 4. 清理词法分析器资源
close_lexer();
return program_ast;
}
// --- 主函数,用于测试语法分析器 ---
// 将覆盖原 lexer.c 中的 main 函数,作为整个编译器前端的测试驱动。
int main(int argc, char *argv[]) {
// 检查命令行参数
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
printf("--- Syntax Analysis Started ---\n");
// 调用解析器,获取AST
ASTNode *program_ast = parse(argv[1]);
if (program_ast) {
printf("\n--- Abstract Syntax Tree (AST) ---\n");
ast_print(program_ast, 0); // 打印生成的AST
printf("\n--- Syntax Analysis Complete ---\n");
// 释放AST占用的内存
ast_free(program_ast);
printf("AST memory freed.\n");
} else {
printf("\n--- Syntax Analysis Failed ---\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `parser.c` 是我们语法分析器的真正实现,它利用了递归下降的策略来解析C语言的简化文法。
*
* **1. Token 操作辅助函数:**
* - `parser_error()`: 报告语法错误,并指示错误发生在哪个Token的位置。
* - `expect(TokenType type)`: 检查当前Token是否是期望的类型。如果不是,就报告错误并退出。这是解析器中实现“强制匹配”的关键。
* - `consume()`: 推进Token流,获取下一个Token。这是解析器前进的唯一方式。
* 注意这里对 `current_token.lexeme` 的 `free()` 操作,因为它是由 `strdup` 分配的。
* - `match_and_consume(TokenType type)`: 尝试匹配并消费一个Token,如果匹配成功则返回真,否则返回假且不消费Token。
* 这用于可选的语法元素或区分不同分支。
*
* **2. 递归下降解析函数 (`parse_xxx`):**
* - **核心思想:** 每个函数都对应C语言文法中的一个非终结符(例如 `<program>`, `<function_declaration>`, `<statement>`, `<expression>` 等)。
* 这些函数会递归地调用彼此,尝试匹配Token流,并在此过程中构建AST节点。
* - **优先级处理:** 对于表达式,我们使用经典的“运算符优先级解析”方法。
* 例如,`parse_primary_expression` 解析最高优先级(数字、标识符、括号)。
* `parse_multiplicative_expression` 调用 `parse_primary_expression` 来获取操作数,
* 然后处理 `*` 和 `/` 运算符。
* `parse_additive_expression` 调用 `parse_multiplicative_expression` 来获取操作数,
* 然后处理 `+` 和 `-` 运算符,以此类推。
* 这种链式调用自然地处理了运算符的优先级和结合性。
* - **左结合性:** 对于左结合的运算符(如 `+`, `-`, `*`, `/`, `==`, `!=`, `<`, etc.),
* 解析函数会使用 `while` 循环来重复处理相同的优先级运算符。
* 例如,`a + b + c` 会被解析为 `(a + b) + c`。
* - **右结合性:** 对于右结合的运算符(如 `=` 赋值运算符),解析函数会在右操作数处递归调用自身。
* 例如,`a = b = c` 会被解析为 `a = (b = c)`。
* - **错误恢复(简化):** 目前的错误处理是“恐慌模式”,即遇到错误就直接退出。
* 在实际编译器中,错误恢复机制会更复杂,尝试跳过一些Token以继续解析,从而报告更多错误。
* - **AST 构建:** 在每个解析函数中,当识别出一个完整的语法结构时,就会调用 `ast_new_xxx` 辅助函数来创建对应的AST节点,并将子节点的指针连接起来。
*
* **3. 表达式文法与解析函数对应关系(简化示例):**
* - `<expression>` -> `parse_expression()`
* - `<assignment_expression>` -> `parse_assignment_expression()`
* - `<equality_expression>` -> `parse_equality_expression()`
* - `<relational_expression>` -> `parse_relational_expression()`
* - `<additive_expression>` -> `parse_additive_expression()`
* - `<multiplicative_expression>` -> `parse_multiplicative_expression()`
* - `<primary_expression>` -> `parse_primary_expression()`
*
* **4. `parse()` 函数:**
* - 它是语法分析器的外部接口,负责初始化词法分析器,获取第一个Token,然后调用 `parse_program()` 开始整个解析过程。
* - 解析完成后,它会清理词法分析器资源。
*
* **5. 测试驱动 `main()` 函数:**
* - 接管了 `lexer.c` 中原有的 `main` 函数,现在它作为整个前端(词法+语法)的测试入口。
* - 它调用 `parse()` 函数来解析源代码,如果成功则调用 `ast_print()` 来打印生成的AST,以便我们可视化和验证解析器的正确性。
* - 最后,通过 `ast_free()` 释放AST占用的内存。
*
* **挑战与待完善:**
* - **完整的C语言文法:** 目前只支持C语言的一个极小简化子集。要支持完整的C语言,需要大量的递归下降函数来覆盖所有复杂的语法规则(如指针、数组、结构体、联合体、枚举、各种运算符的优先级和结合性、类型转换、函数参数、局部变量作用域等)。
* - **错误恢复:** 当前的“恐慌模式”错误处理会导致程序在遇到第一个语法错误时就退出。更健壮的编译器会尝试跳过错误并继续报告。
* - **符号表与语义分析:** 语法分析只关心结构是否正确,不关心意义。例如,`int a; a = b;` 在语法上是正确的,但如果 `b` 未声明,这就是语义错误。这将在下一个阶段——语义分析中处理。
* - **左递归问题:** 递归下降解析器不能直接处理左递归文法(如 `Expr ::= Expr + Term`)。需要将其改写为右递归或使用迭代方式。我们通过将优先级低的表达式函数作为循环的入口来避免了显式的左递归。
* - **AST打印优化:** `ast_print` 函数中 `token_type_to_string(node->type)` 调用是错误的,因为 `node->type` 是 `ASTNodeType`,不是 `TokenType`。需要单独的 `ast_node_type_to_string` 函数。
*/
为了让 ast_print
函数能够正确打印 ASTNodeType
,我们需要在 ast.h
中添加一个辅助函数 ast_node_type_to_string
,并在 parser.c
中实现它。
4. ast_node_type_to_string
实现 (添加到 parser.c
顶部附近)
// 文件名: ast.h (只需要声明)
// ...
const char* ast_node_type_to_string(ASTNodeType type); // 在 ast.h 中添加此声明
// ...
```c
// 文件名: parser.c (实现 ast_node_type_to_string 函数)
// ...
// 将ASTNodeType转换为可读字符串(用于调试)
// 这是AST打印的关键辅助函数
const char* ast_node_type_to_string(ASTNodeType type) {
switch (type) {
case AST_PROGRAM: return "AST_PROGRAM";
case AST_FUNCTION_DECL: return "AST_FUNCTION_DECL";
case AST_COMPOUND_STMT: return "AST_COMPOUND_STMT";
case AST_VAR_DECL: return "AST_VAR_DECL";
case AST_EXPR_STMT: return "AST_EXPR_STMT";
case AST_RETURN_STMT: return "AST_RETURN_STMT";
case AST_IF_STMT: return "AST_IF_STMT";
case AST_WHILE_STMT: return "AST_WHILE_STMT";
case AST_FOR_STMT: return "AST_FOR_STMT";
case AST_BINARY_EXPR: return "AST_BINARY_EXPR";
case AST_UNARY_EXPR: return "AST_UNARY_EXPR";
case AST_INTEGER_LITERAL: return "AST_INTEGER_LITERAL";
case AST_IDENTIFIER: return "AST_IDENTIFIER";
case AST_ASSIGN_EXPR: return "AST_ASSIGN_EXPR";
case AST_FUNCTION_CALL: return "AST_FUNCTION_CALL";
case AST_STRING_LITERAL: return "AST_STRING_LITERAL";
case AST_CHAR_LITERAL: return "AST_CHAR_LITERAL";
default: return "UNKNOWN_AST_NODE_TYPE";
}
}
// ...
然后将 ast_print
函数中 printf("Type: %s ", token_type_to_string(node->type));
修正为 printf("Type: %s ", ast_node_type_to_string(node->type));
。
5. 测试文件:test.c
使用之前词法分析器的 test.c
即可,我们现在可以解析更复杂的结构。
// 文件名: test.c
// 用于测试自定义C语言词法分析器和语法分析器的示例源代码。
/* 这是一个
* 多行注释
* 可以包含多行文本 */
int main() { // 主函数入口
// 声明一个整数变量并初始化
int my_var_1 = 10;
// 另一个变量
int anotherVar; // 未初始化声明
/* 这是一个
* 测试
* 注释 */
// 算术运算和赋值
int sum = my_var_1 + anotherVar * 2; // anotherVar 未初始化,这里会有语义问题,但语法分析器不关心
// 赋值语句
anotherVar = 50;
// 复杂的赋值表达式
sum = (my_var_1 + 5) * anotherVar;
// 比较运算和条件语句
if (sum >= 40) {
return 1; // 返回1
} else {
// 嵌套复合语句
int result = 0;
if (my_var_1 < 5) {
result = 100;
}
return result; // 返回0或者100
}
// 更多的表达式类型 (待 parser.c 完整支持)
// int val = !my_var_1; // 一元非
// int bit = 0xFF & 0xAA; // 位运算符
return 2; // 最后的返回值(这段代码应该不会被执行到,但语法上正确)
}
编译和运行语法分析器
要编译和运行:
-
确保你拥有以下文件在同一目录下:
token.h
,lexer.h
,lexer.c
,ast.h
,parser.h
,parser.c
,test.c
。 -
打开终端。
-
使用GCC编译:
gcc -o my_parser lexer.c parser.c -g -Wall
(
-g
用于生成调试信息,-Wall
开启所有警告,有助于发现问题) -
运行语法分析器:
./my_parser test.c
你将看到 test.c
文件被解析后生成的抽象语法树的结构。这是一种分层的、树状的输出,直观地展示了你的C程序是如何被编译器“理解”的。这将是你亲手构建的编译器迈出的又一大步!
总结与展望
在这一部分,我们为我们的C语言编译器构建了其核心的“大脑”——语法分析器。我们深入理解了抽象语法树(AST)的概念,它是源代码语法结构的简洁、抽象表示,为后续的编译阶段提供了统一的输入。通过递归下降解析的实现,我们亲手将Token流转化为这种树状结构,处理了C语言的简化文法规则,包括函数定义、变量声明、赋值语句、条件语句以及各种表达式的优先级和结合性。
你亲手编写的这些代码,让程序从一堆字符变成了有逻辑、有结构的“思维导图”。这不仅是技术上的进步,更是对C语言乃至整个计算机科学原理的更深层次理解。我们已经完成了编译器前端的关键部分:词法分析器和语法分析器。
然而,当前的AST仅仅表示了程序的结构,还没有理解其意义。例如,我们声明了一个变量 int my_var_1 = 10;
,但如果后面我们试图使用一个未声明的变量,或者对不同类型的变量进行不兼容的操作,当前的语法分析器是不会报错的。这些“有意义”的检查,将是下一阶段——语义分析器的任务。
在下一个篇章中,我们将踏上语义分析的旅程。我们将学习如何构建和使用符号表,如何在AST上进行类型检查、作用域检查,并为后续的中间代码生成做准备。这将是编译器从“理解结构”到“理解逻辑”的关键飞跃!让我们一起期待!
(4)掀开C语言的底裤 手撸编译器:进阶之路——核心组件之语法分析器 - 第三部分
各位勇士们,在上一个篇章里,我们完成了编译器的“眼睛”——词法分析器。它成功地将C语言源代码从字符流转化为了有意义的“词素”(Token)流。现在,这些零散的“单词”需要被组织起来,形成一个有意义的结构,这就是我们今天要深入探讨的主题:语法分析器(Syntax Analyzer),也被称为解析器(Parser)。
如果说词法分析器是识别一个个独立的“单词”,那么语法分析器就是根据语法规则,将这些“单词”组合成“句子”和“段落”,并理解它们的层次结构。在编译器中,这些“句子”和“段落”最终会被表示为一种称为**抽象语法树(Abstract Syntax Tree, AST)**的数据结构。
语法分析器的核心使命
语法分析器是编译器前端的第二个阶段,它的主要任务是:
-
输入: 接收词法分析器生成的Token序列。
-
输出: 构建一个抽象语法树(AST)。
-
错误检测: 在构建AST的过程中,检查Token序列是否符合编程语言的语法规则。如果发现语法错误(例如缺少分号、括号不匹配等),则报告错误。
什么是抽象语法树(AST)?
抽象语法树(AST)是源代码语法结构的一种抽象表示,它移除了具体语法中冗余的细节(例如,表达式中的括号在AST中通常会被隐含在树的结构中,而不是作为独立的节点),更便于后续的语义分析、中间代码生成和代码优化阶段处理。
AST中的每个节点都代表了源代码中的一个构造,例如表达式、语句、声明、函数调用等。树的结构反映了这些构造之间的语法关系和层次结构。例如,一个加法表达式的AST节点可能包含两个子节点,分别代表加法的左操作数和右操作数。
AST的好处:
-
抽象性: 剔除了源代码中的许多表面细节(如空白、注释、具体语法中的冗余符号),只保留核心的语法结构信息。
-
层次结构: 明确表示了代码的嵌套和从属关系,便于编译器理解程序的逻辑。
-
便于处理: 作为一种树状结构,它非常适合递归遍历和处理,为后续的语义分析、优化和代码生成提供了统一、简洁的输入。
语法分析的实现原理:上下文无关文法与递归下降解析
语法分析器通常基于**上下文无关文法(Context-Free Grammars, CFG)**来定义编程语言的语法。CFG使用一组规则来描述语言中有效结构的组合方式。例如,一条简单的C语言声明规则可能看起来像这样(使用巴科斯范式BNF):
<declaration> ::= <type_specifier> <identifier> ";"
<type_specifier> ::= "int" | "char" | "float" // ...
基于CFG,有多种实现语法分析器的方法,包括:
-
自顶向下解析(Top-Down Parsing): 从文法的起始符号开始,尝试推导出输入串。
-
递归下降解析(Recursive Descent Parsing): 这是一种自顶向下解析方法,每个非终结符(即文法规则的左部)都对应一个函数。这些函数会递归地调用彼此,尝试匹配输入Token流。这种方法直观、易于手写和调试,非常适合我们手撸一个简单编译器。
-
LL(k) 解析: 通过向左扫描(L),并使用k个Token的向前看(Lookahead)来决定下一步如何推导。
-
-
自底向上解析(Bottom-Up Parsing): 从输入串开始,逐步归约到文法的起始符号。
-
LR(k) 解析: 更强大的解析方法,能够识别更广泛的文法,但通常需要工具(如Yacc/Bison)来自动生成。
-
对于我们手撸的编译器,我们将采用递归下降解析。它的优点是实现起来相对简单直观,每个语法规则直接对应一个C函数,逻辑清晰。缺点是它只适用于LL(1)或少数LL(k)文法,对于左递归或二义性文法需要进行转换。
手撸语法分析器:C语言实现
现在,我们将在之前词法分析器的基础上,构建我们的语法分析器。我们将定义AST节点的数据结构,并实现一系列递归下降函数来解析C语言的简化子集。
我们将解析一个非常简化的C语言子集,包括:
-
程序结构: 包含一个或多个函数定义。
-
函数定义: 简单的
int main()
函数。 -
语句: 变量声明、赋值语句、
return
语句、if-else
语句。 -
表达式: 整数常量、标识符、加减乘除运算、比较运算。
1. ast.h
:抽象语法树(AST)节点的定义
这个头文件将定义各种AST节点的类型和结构体。
// 文件名: ast.h
// 描述: 定义抽象语法树(AST)的节点结构。
// AST是源代码语法结构的一种抽象表示,用于后续的语义分析和代码生成。
#ifndef AST_H
#define AST_H
#include "token.h" // 包含Token的定义,AST节点可能需要引用Token信息
// 定义AST节点类型枚举
// 这些类型代表了C语言中不同种类的语法构造,它们将在AST中形成节点。
typedef enum {
AST_PROGRAM, // 整个程序(根节点)
AST_FUNCTION_DECL, // 函数声明或定义 (Function Declaration/Definition)
AST_COMPOUND_STMT, // 复合语句(代码块),即花括号 {} 中的一系列语句
// 声明
AST_VAR_DECL, // 变量声明
// 语句
AST_EXPR_STMT, // 表达式语句(例如 `a + b;`)
AST_RETURN_STMT, // return 语句
AST_IF_STMT, // if 语句
AST_WHILE_STMT, // while 语句 (待实现)
AST_FOR_STMT, // for 语句 (待实现)
// 表达式
AST_BINARY_EXPR, // 二元表达式(例如 `a + b`, `x == y`)
AST_UNARY_EXPR, // 一元表达式(例如 `-a`, `!b`) (待实现)
AST_INTEGER_LITERAL, // 整数常量(例如 `123`)
AST_IDENTIFIER, // 标识符(变量名、函数名)
AST_ASSIGN_EXPR, // 赋值表达式(例如 `a = b`)
AST_FUNCTION_CALL, // 函数调用(例如 `main()`) (待实现)
AST_STRING_LITERAL, // 字符串常量 (待实现)
AST_CHAR_LITERAL, // 字符常量 (待实现)
} ASTNodeType;
// 前向声明,用于解决交叉引用问题(例如一个节点可能包含另一个节点)
struct ASTNode; // 声明 struct ASTNode
// 定义链表节点,用于连接同类型的AST节点(例如:复合语句中的一系列语句)
typedef struct ASTNodeList {
struct ASTNode *node; // 指向一个AST节点
struct ASTNodeList *next; // 指向下一个列表节点
} ASTNodeList;
// 定义AST节点的通用结构体
// 所有不同类型的AST节点都将包含这些基本信息,
// 并通过 union 存储各自特有的数据。
typedef struct ASTNode {
ASTNodeType type; // 节点类型
int line; // 节点在源代码中的起始行号
int column; // 节点在源代码中的起始列号
// union 用于存储特定节点类型的数据
// 这样做可以节省内存,因为一个节点只会在特定类型下使用其对应的union成员。
union {
// AST_PROGRAM
ASTNodeList *program_decls; // 程序中的声明列表(例如函数定义)
// AST_FUNCTION_DECL
struct {
TokenType return_type; // 函数返回类型(例如 TOKEN_INT)
char *name; // 函数名
ASTNodeList *params; // 参数列表 (待实现)
struct ASTNode *body; // 函数体 (通常是一个 AST_COMPOUND_STMT)
} func_decl;
// AST_COMPOUND_STMT
struct {
ASTNodeList *statements; // 复合语句中的语句列表
} compound_stmt;
// AST_VAR_DECL
struct {
TokenType type_specifier; // 变量类型(例如 TOKEN_INT)
char *name; // 变量名
struct ASTNode *initializer; // 变量初始化表达式(可选)
} var_decl;
// AST_EXPR_STMT
struct {
struct ASTNode *expr; // 表达式语句包含的表达式
} expr_stmt;
// AST_RETURN_STMT
struct {
struct ASTNode *expr; // return 语句返回的表达式(可选)
} return_stmt;
// AST_IF_STMT
struct {
struct ASTNode *condition; // 条件表达式
struct ASTNode *then_stmt; // if 分支语句 (then-branch)
struct ASTNode *else_stmt; // else 分支语句 (else-branch, 可选)
} if_stmt;
// AST_BINARY_EXPR
struct {
TokenType op; // 运算符Token类型(例如 TOKEN_PLUS, TOKEN_EQ)
struct ASTNode *left; // 左操作数表达式
struct ASTNode *right; // 右操作数表达式
} binary_expr;
// AST_UNARY_EXPR (待实现)
struct {
TokenType op;
struct ASTNode *operand;
} unary_expr;
// AST_INTEGER_LITERAL
struct {
int value; // 整数常量的值
} int_literal;
// AST_IDENTIFIER
struct {
char *name; // 标识符的名称
} identifier;
// AST_ASSIGN_EXPR
struct {
struct ASTNode *left; // 赋值的左值(通常是标识符)
struct ASTNode *right; // 赋值的右值表达式
} assign_expr;
// AST_FUNCTION_CALL (待实现)
struct {
char *name;
ASTNodeList *args;
} func_call;
// AST_STRING_LITERAL (待实现)
struct {
char *value;
} string_literal;
// AST_CHAR_LITERAL (待实现)
struct {
int value;
} char_literal;
} data; // 联合体实例名
} ASTNode;
// --- 辅助函数声明 ---
// 创建新的AST节点
ASTNode* ast_new_node(ASTNodeType type, int line, int column);
// 创建特定类型的AST节点辅助函数
ASTNode* ast_new_program(ASTNodeList *decls);
ASTNode* ast_new_function_decl(TokenType return_type, char *name, ASTNode *body, int line, int column);
ASTNode* ast_new_compound_stmt(ASTNodeList *statements, int line, int column);
ASTNode* ast_new_var_decl(TokenType type_specifier, char *name, ASTNode *initializer, int line, int column);
ASTNode* ast_new_expr_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_return_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_if_stmt(ASTNode *condition, ASTNode *then_stmt, ASTNode *else_stmt, int line, int column);
ASTNode* ast_new_binary_expr(TokenType op, ASTNode *left, ASTNode *right, int line, int column);
ASTNode* ast_new_integer_literal(int value, int line, int column);
ASTNode* ast_new_identifier(char *name, int line, int column);
ASTNode* ast_new_assign_expr(ASTNode *left, ASTNode *right, int line, int column);
// 链表辅助函数
ASTNodeList* ast_new_node_list(ASTNode *node);
void ast_add_to_list(ASTNodeList **head, ASTNode *node);
// 遍历和打印AST(用于调试和验证)
// 这是一个非常重要的调试工具,可以直观地看到解析器构建的树结构。
void ast_print(ASTNode *node, int indent);
// 释放AST节点及其子节点占用的内存
void ast_free(ASTNode *node);
#endif // AST_H
/*
* 逻辑分析:
* `ast.h` 文件是语法分析器输出的“蓝图”。它定义了我们如何用C语言来表示C语言代码的结构。
*
* 1. `ASTNodeType` 枚举:
* - 定义了AST中可能出现的所有节点类型。每个类型对应C语言的一个语法结构(例如函数声明、变量声明、二元表达式等)。
* - 这些节点类型的设计直接反映了我们编译器将要支持的C语言子集。
*
* 2. `ASTNode` 结构体:
* - 这是AST的核心。每个 `ASTNode` 都代表了源代码中的一个语法单元。
* - `type`: 最重要的字段,指示这个节点代表何种语法结构。
* - `line`, `column`: 记录了该语法单元在源代码中的位置,这对于错误报告和调试至关重要。
* - `union data`: 这是关键的设计模式,它使得一个 `ASTNode` 能够根据其 `type` 存储不同类型的数据。
* 例如,如果 `type` 是 `AST_FUNCTION_DECL`,那么 `data.func_decl` 成员会被使用,
* 其中包含函数名、返回类型和函数体等信息。
* 这种方式既节省了内存(因为不同的节点类型共享同一块内存),又提供了灵活性。
* - 对于 `AST_PROGRAM`、`AST_FUNCTION_DECL`、`AST_COMPOUND_STMT` 等包含子节点列表的类型,
* 我们定义了 `ASTNodeList` 链表结构来管理子节点。
* - 对于 `AST_BINARY_EXPR`,它有 `op`(运算符)、`left` 和 `right`(左右操作数)。
* - 对于 `AST_INTEGER_LITERAL`,它只包含一个 `value`。
* - 对于 `AST_IDENTIFIER`,它包含 `name`。
*
* 3. `ASTNodeList` 结构体:
* - 一个简单的单向链表结构,用于连接AST中的同类型子节点。
* - 例如,一个复合语句(`{ ... }`)可能包含多个语句,这些语句就可以通过 `ASTNodeList` 链表连接起来。
* - 函数的参数列表、程序顶层的声明列表等都可以用这种通用链表来表示。
*
* 4. 辅助函数:
* - `ast_new_node` 系列函数:方便创建各种类型的AST节点,并初始化其基本信息。
* 这些函数负责动态内存分配(`malloc`)和数据初始化,将底层的内存管理细节封装起来。
* - `ast_new_node_list` 和 `ast_add_to_list`:用于方便地构建和管理 `ASTNodeList` 链表。
* - `ast_print`: 递归遍历AST并以缩进格式打印,这是调试语法分析器输出是否正确的利器。
* - `ast_free`: 递归释放AST节点占用的所有内存,防止内存泄漏。
*
* 设计考量:
* - **分层表示:** AST将C语言的文本形式转换为内存中的树形结构,更接近计算机处理的逻辑。
* - **灵活性和扩展性:** `union` 的使用和链表结构的定义使得AST能够灵活表示C语言的各种语法构造,
* 并且未来添加新的语法特性时,只需在 `ASTNodeType` 中添加新类型并在 `union` 中添加对应结构即可。
* - **调试友好:** 提供了 `ast_print` 函数,方便可视化AST的结构。
* - **内存管理:** 显式地提供了 `ast_new_node` 和 `ast_free` 函数来管理节点的生命周期,
* 这在C语言中至关重要,避免了内存泄漏。
*/
2. parser.h
:语法分析器的接口
这个头文件将声明语法分析器的主要函数和任何必要的全局状态。
// 文件名: parser.h
// 描述: 声明语法分析器(Parser)的接口。
// 语法分析器负责将Token流转换为抽象语法树(AST)。
#ifndef PARSER_H
#define PARSER_H
#include "ast.h" // 包含AST节点的定义
#include "token.h" // 包含Token的定义
#include "lexer.h" // 包含词法分析器接口,因为解析器需要从词法分析器获取Token
// 语法分析器入口函数
// 参数: source_code_path - 待解析的C源代码文件路径。
// 返回值: 解析成功则返回指向AST根节点的指针,失败则返回NULL。
ASTNode* parse(const char *source_code_path);
// 错误报告函数声明
// 当语法分析器遇到不符合语法规则的Token序列时,需要报告错误。
void parser_error(const char *format, ...);
// 辅助函数声明(内部使用,但为了调试或其他原因可能在头文件声明)
// 这些函数通常与特定的语法规则相对应,是递归下降解析的核心。
// 例如:
// ASTNode* parse_program(); // 解析整个程序
// ASTNode* parse_declaration(); // 解析声明(变量声明、函数声明)
// ASTNode* parse_statement(); // 解析语句
// ASTNode* parse_expression(); // 解析表达式 (通常分解为多个优先级函数)
#endif // PARSER_H
/*
* 逻辑分析:
* `parser.h` 定义了语法分析器模块的“公共接口”。
*
* 1. `parse(const char *source_code_path)`:
* - 这是语法分析器的主要入口点。
* - 它负责初始化词法分析器(调用 `init_lexer`),然后开始Token流的解析过程,并最终返回一个完整的AST。
* - 如果解析过程中出现语法错误,它会报告错误并通过返回 `NULL` 或其他机制指示失败。
*
* 2. `parser_error(const char *format, ...)`:
* - 统一的错误报告函数,用于在语法分析过程中报告语法错误。
* - 它会打印错误发生的位置(通常是当前Token的行号和列号)以及详细的错误信息。
*
* 3. 辅助函数(被注释掉的部分):
* - 在实际的递归下降解析器中,会有一系列函数,每个函数对应文法中的一个非终结符(即一个语法规则)。
* - 例如,`parse_program()` 负责解析整个程序的结构;`parse_statement()` 负责解析各种类型的语句;
* `parse_expression()` 负责解析表达式。
* - 这些函数会递归地调用彼此,根据当前的Token来决定下一步如何匹配语法规则。
* - 它们是语法分析的核心逻辑,通常在 `parser.c` 中实现,不一定需要在 `parser.h` 中声明,
* 除非它们需要在其他模块中被直接调用(例如,如果有一个独立的AST打印器需要这些)。
* 在这里为了清晰演示解析器的内部结构,我们将其作为概念注释。
*
* 设计考量:
* - **清晰的接口:** `parse` 函数作为唯一的公共接口,隐藏了语法分析器的内部复杂性。
* - **依赖性:** 语法分析器依赖于词法分析器来获取Token,因此 `lexer.h` 被包含进来。
* - **错误报告:** 提供了专门的错误报告函数,方便统一处理语法错误。
*/
3. parser.c
:语法分析器的实现
这是语法分析器的核心,包含了递归下降解析的逻辑和AST构建的代码。
// 文件名: parser.c
// 描述: 语法分析器(Parser)的核心实现。
// 它负责从词法分析器获取Token流,并根据C语言的简化文法规则构建抽象语法树(AST)。
#include <stdio.h> // 用于输入输出
#include <stdlib.h> // 用于内存管理 (malloc, free) 和退出 (exit)
#include <stdarg.h> // 用于可变参数函数 (va_list, va_start, va_end)
#include <string.h> // 用于字符串操作 (strcmp, strdup)
#include "token.h" // 包含Token类型和结构体的定义
#include "lexer.h" // 包含词法分析器接口的定义 (get_next_token, lexer_error)
#include "ast.h" // 包含AST节点结构体和辅助函数的定义
#include "parser.h" // 包含语法分析器接口的定义
// --- 全局/内部变量 ---
// current_token 是由 lexer.c 中的 get_next_token 填充的全局变量。
// 这里直接使用它。
// --- 辅助函数:Token 操作 ---
// 打印语法分析器错误信息
// 这是语法分析阶段的错误报告机制。
void parser_error(const char *format, ...) {
va_list args;
// 使用 current_token 的位置信息,指向当前发生错误的Token
fprintf(stderr, "Parser Error [%d:%d]: ", current_token.line, current_token.column);
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, "\n");
exit(EXIT_FAILURE); // 遇到致命错误时退出程序
}
// 检查当前Token类型是否符合预期
// 如果不符合,则报告语法错误。
static void expect(TokenType type) {
if (current_token.type != type) {
parser_error("Expected '%s', but got '%s' ('%s')",
token_type_to_string(type),
token_type_to_string(current_token.type),
current_token.lexeme);
}
}
// 消费当前Token,并获取下一个Token
// 这是解析器向前推进的核心操作。
static void consume() {
// 释放旧Token的lexeme,因为 get_next_token 会 strdup
if (current_token.lexeme && current_token.type != TOKEN_EOF && current_token.type != TOKEN_ERROR) {
free(current_token.lexeme);
current_token.lexeme = NULL;
}
current_token = get_next_token();
}
// 检查当前Token类型是否是预期的一个,如果是则消费并返回真,否则返回假。
static int match_and_consume(TokenType type) {
if (current_token.type == type) {
consume();
return 1;
}
return 0;
}
// --- 递归下降解析函数声明 (前向声明,因为它们互相调用) ---
// 每个函数对应C语言文法中的一个非终结符(或一个复合的语法单元)。
// 解析优先级从低到高。
static ASTNode* parse_program();
static ASTNode* parse_function_decl();
static ASTNode* parse_compound_statement();
static ASTNode* parse_statement();
static ASTNode* parse_declaration(); // 变量声明
static ASTNode* parse_expression(); // 表达式的入口点
static ASTNode* parse_assignment_expression(); // 赋值表达式
static ASTNode* parse_equality_expression(); // 相等/不等表达式 (==, !=)
static ASTNode* parse_relational_expression(); // 关系表达式 (<, <=, >, >=)
static ASTNode* parse_additive_expression(); // 加法/减法表达式 (+, -)
static ASTNode* parse_multiplicative_expression(); // 乘法/除法表达式 (*, /)
static ASTNode* parse_primary_expression(); // 最基本的表达式(数字、标识符、括号表达式)
// --- 递归下降解析函数实现 ---
// 解析整个程序:<program> ::= { <function_declaration> } EOF
static ASTNode* parse_program() {
ASTNodeList *decls_list = NULL; // 存储函数声明的链表
// 循环解析所有的函数声明/定义,直到文件结束
while (current_token.type != TOKEN_EOF) {
ASTNode *func_decl = parse_function_decl(); // 解析一个函数声明
if (func_decl) {
ast_add_to_list(&decls_list, func_decl); // 将函数声明添加到列表中
} else {
// 如果不是函数声明,则可能是语法错误或未识别的顶级结构
parser_error("Unexpected token at top level: '%s'", current_token.lexeme);
// 尝试跳过当前Token以避免死循环,但可能导致更多错误
consume();
}
}
// 检查是否到达文件结束
expect(TOKEN_EOF);
return ast_new_program(decls_list); // 返回程序AST根节点
}
// 解析函数声明:<function_declaration> ::= <type_specifier> <IDENTIFIER> "(" ")" <compound_statement>
// 目前只支持 int main() {} 这种形式
static ASTNode* parse_function_decl() {
int start_line = current_token.line;
int start_column = current_token.column;
// 1. 解析返回类型 (目前只支持 int)
expect(TOKEN_INT);
TokenType return_type = current_token.type; // 获取类型
consume(); // 消费 'int'
// 2. 解析函数名
expect(TOKEN_IDENTIFIER);
char *func_name = strdup(current_token.lexeme); // 复制函数名
consume(); // 消费标识符 (函数名)
// 3. 解析参数列表 (目前只支持空参数列表 ())
expect(TOKEN_LPAREN);
consume(); // 消费 '('
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
// 4. 解析函数体 (必须是复合语句 {})
ASTNode *body = parse_compound_statement();
if (!body) {
parser_error("Expected function body (compound statement) for function '%s'", func_name);
}
return ast_new_function_decl(return_type, func_name, body, start_line, start_column);
}
// 解析复合语句(代码块):<compound_statement> ::= "{" { <declaration> | <statement> } "}"
static ASTNode* parse_compound_statement() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_LBRACE);
consume(); // 消费 '{'
ASTNodeList *statements_list = NULL; // 存储语句和声明的链表
// 循环解析内部的声明和语句,直到遇到 '}'
while (current_token.type != TOKEN_RBRACE && current_token.type != TOKEN_EOF) {
ASTNode *node = NULL;
// 尝试解析声明 (int var;)
if (current_token.type == TOKEN_INT) {
node = parse_declaration();
} else {
// 否则尝试解析语句
node = parse_statement();
}
if (node) {
ast_add_to_list(&statements_list, node);
} else {
// 如果既不是声明也不是已知语句,则可能是语法错误
parser_error("Unexpected token in compound statement: '%s'", current_token.lexeme);
// 尝试跳过当前Token以避免死循环
consume();
}
}
expect(TOKEN_RBRACE);
consume(); // 消费 '}'
return ast_new_compound_stmt(statements_list, start_line, start_column);
}
// 解析语句:<statement> ::= <expression_statement> | <return_statement> | <if_statement> | <compound_statement>
static ASTNode* parse_statement() {
ASTNode *node = NULL;
int start_line = current_token.line;
int start_column = current_token.column;
switch (current_token.type) {
case TOKEN_RETURN:
node = ast_new_return_stmt(NULL, start_line, start_column); // 临时创建
consume(); // 消费 'return'
ASTNode *expr = parse_expression(); // 解析返回的表达式
node->data.return_stmt.expr = expr;
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return node;
case TOKEN_IF:
consume(); // 消费 'if'
expect(TOKEN_LPAREN);
consume(); // 消费 '('
ASTNode *condition = parse_expression(); // 解析条件表达式
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
ASTNode *then_stmt = parse_statement(); // 解析 if 分支的语句
ASTNode *else_stmt = NULL;
if (current_token.type == TOKEN_ELSE) {
consume(); // 消费 'else'
else_stmt = parse_statement(); // 解析 else 分支的语句
}
return ast_new_if_stmt(condition, then_stmt, else_stmt, start_line, start_column);
case TOKEN_LBRACE: // 复合语句可以作为任何语句的一部分
return parse_compound_statement();
default:
// 如果不是以上特定语句,则尝试解析表达式语句 (e.g. `a = b + c;`)
node = ast_new_expr_stmt(NULL, start_line, start_column); // 临时创建
ASTNode *expr_stmt_expr = parse_expression();
node->data.expr_stmt.expr = expr_stmt_expr;
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return node;
}
}
// 解析变量声明:<declaration> ::= <type_specifier> <IDENTIFIER> [ "=" <expression> ] ";"
// 目前只支持 int 类型的变量声明
static ASTNode* parse_declaration() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_INT);
TokenType type_specifier = current_token.type;
consume(); // 消费 'int'
expect(TOKEN_IDENTIFIER);
char *var_name = strdup(current_token.lexeme);
consume(); // 消费标识符 (变量名)
ASTNode *initializer = NULL;
if (current_token.type == TOKEN_ASSIGN) { // 如果有初始化符 '='
consume(); // 消费 '='
initializer = parse_assignment_expression(); // 解析初始化表达式
}
expect(TOKEN_SEMICOLON);
consume(); // 消费 ';'
return ast_new_var_decl(type_specifier, var_name, initializer, start_line, start_column);
}
// --- 表达式解析函数 (从低优先级到高优先级) ---
// 表达式入口:<expression> ::= <assignment_expression>
// 简单起见,目前表达式就从赋值表达式开始
static ASTNode* parse_expression() {
return parse_assignment_expression();
}
// 解析赋值表达式:<assignment_expression> ::= <equality_expression> [ "=" <assignment_expression> ]
// 右结合性:a = b = c 等价于 a = (b = c)
static ASTNode* parse_assignment_expression() {
ASTNode *node = parse_equality_expression(); // 先解析更高优先级的表达式
int start_line = current_token.line;
int start_column = current_token.column;
if (current_token.type == TOKEN_ASSIGN) {
consume(); // 消费 '='
ASTNode *right_expr = parse_assignment_expression(); // 递归解析右侧的赋值表达式
// 检查左侧是否是有效的左值(目前只支持标识符作为左值)
if (node->type != AST_IDENTIFIER) {
parser_error("Invalid left-hand side in assignment: expected identifier.");
}
node = ast_new_assign_expr(node, right_expr, start_line, start_column);
}
return node;
}
// 解析相等/不等表达式:<equality_expression> ::= <relational_expression> { ("==" | "!=") <relational_expression> }
// 这是一个左结合的运算符。例如 a == b == c 是错误的,但 a == b + c 是可以的。
static ASTNode* parse_equality_expression() {
ASTNode *node = parse_relational_expression(); // 先解析更高优先级的关系表达式
while (current_token.type == TOKEN_EQ || current_token.type == TOKEN_NE) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '==' 或 '!='
ASTNode *right = parse_relational_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析关系表达式:<relational_expression> ::= <additive_expression> { ("<" | "<=" | ">" | ">=") <additive_expression> }
// 左结合
static ASTNode* parse_relational_expression() {
ASTNode *node = parse_additive_expression(); // 先解析更高优先级的加法表达式
while (current_token.type == TOKEN_LT ||
current_token.type == TOKEN_LE ||
current_token.type == TOKEN_GT ||
current_token.type == TOKEN_GE)
{
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费关系运算符
ASTNode *right = parse_additive_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析加法/减法表达式:<additive_expression> ::= <multiplicative_expression> { ("+" | "-") <multiplicative_expression> }
// 左结合
static ASTNode* parse_additive_expression() {
ASTNode *node = parse_multiplicative_expression(); // 先解析更高优先级的乘法表达式
while (current_token.type == TOKEN_PLUS || current_token.type == TOKEN_MINUS) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '+' 或 '-'
ASTNode *right = parse_multiplicative_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析乘法/除法表达式:<multiplicative_expression> ::= <primary_expression> { ("*" | "/") <primary_expression> }
// 左结合
static ASTNode* parse_multiplicative_expression() {
ASTNode *node = parse_primary_expression(); // 先解析最高优先级的基础表达式
while (current_token.type == TOKEN_ASTERISK || current_token.type == TOKEN_SLASH) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume(); // 消费 '*' 或 '/'
ASTNode *right = parse_primary_expression(); // 解析右操作数
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析基本表达式:<primary_expression> ::= <INTEGER_LITERAL> | <IDENTIFIER> | "(" <expression> ")"
static ASTNode* parse_primary_expression() {
int start_line = current_token.line;
int start_column = current_token.column;
ASTNode *node = NULL;
switch (current_token.type) {
case TOKEN_INTEGER_LITERAL:
node = ast_new_integer_literal(current_token.int_value, start_line, start_column);
consume(); // 消费整数常量
break;
case TOKEN_IDENTIFIER:
// TODO: 在这里可以添加函数调用解析逻辑
node = ast_new_identifier(strdup(current_token.lexeme), start_line, start_column);
consume(); // 消费标识符
break;
case TOKEN_LPAREN:
consume(); // 消费 '('
node = parse_expression(); // 递归解析括号内的表达式
expect(TOKEN_RPAREN);
consume(); // 消费 ')'
break;
default:
parser_error("Unexpected token in primary expression: '%s'", current_token.lexeme);
// 尝试跳过以避免死循环
consume();
return NULL; // 返回NULL表示解析失败
}
return node;
}
// --- AST 辅助函数实现 ---
// 创建新的AST节点
ASTNode* ast_new_node(ASTNodeType type, int line, int column) {
ASTNode *node = (ASTNode*)malloc(sizeof(ASTNode));
if (!node) {
fprintf(stderr, "Fatal Error: Failed to allocate AST node.\n");
exit(EXIT_FAILURE);
}
memset(node, 0, sizeof(ASTNode)); // 初始化为0,确保union成员安全
node->type = type;
node->line = line;
node->column = column;
return node;
}
// 创建特定类型的AST节点辅助函数实现
ASTNode* ast_new_program(ASTNodeList *decls) {
ASTNode *node = ast_new_node(AST_PROGRAM, 0, 0); // Program 节点通常没有特定行/列
node->data.program_decls = decls;
return node;
}
ASTNode* ast_new_function_decl(TokenType return_type, char *name, ASTNode *body, int line, int column) {
ASTNode *node = ast_new_node(AST_FUNCTION_DECL, line, column);
node->data.func_decl.return_type = return_type;
node->data.func_decl.name = name; // 注意:name 是 strdup 后的,需要负责 free
node->data.func_decl.params = NULL; // 暂不支持参数
node->data.func_decl.body = body;
return node;
}
ASTNode* ast_new_compound_stmt(ASTNodeList *statements, int line, int column) {
ASTNode *node = ast_new_node(AST_COMPOUND_STMT, line, column);
node->data.compound_stmt.statements = statements;
return node;
}
ASTNode* ast_new_var_decl(TokenType type_specifier, char *name, ASTNode *initializer, int line, int column) {
ASTNode *node = ast_new_node(AST_VAR_DECL, line, column);
node->data.var_decl.type_specifier = type_specifier;
node->data.var_decl.name = name; // 注意:name 是 strdup 后的,需要负责 free
node->data.var_decl.initializer = initializer;
return node;
}
ASTNode* ast_new_expr_stmt(ASTNode *expr, int line, int column) {
ASTNode *node = ast_new_node(AST_EXPR_STMT, line, column);
node->data.expr_stmt.expr = expr;
return node;
}
ASTNode* ast_new_return_stmt(ASTNode *expr, int line, int column) {
ASTNode *node = ast_new_node(AST_RETURN_STMT, line, column);
node->data.return_stmt.expr = expr;
return node;
}
ASTNode* ast_new_if_stmt(ASTNode *condition, ASTNode *then_stmt, ASTNode *else_stmt, int line, int column) {
ASTNode *node = ast_new_node(AST_IF_STMT, line, column);
node->data.if_stmt.condition = condition;
node->data.if_stmt.then_stmt = then_stmt;
node->data.if_stmt.else_stmt = else_stmt;
return node;
}
ASTNode* ast_new_binary_expr(TokenType op, ASTNode *left, ASTNode *right, int line, int column) {
ASTNode *node = ast_new_node(AST_BINARY_EXPR, line, column);
node->data.binary_expr.op = op;
node->data.binary_expr.left = left;
node->data.binary_expr.right = right;
return node;
}
ASTNode* ast_new_integer_literal(int value, int line, int column) {
ASTNode *node = ast_new_node(AST_INTEGER_LITERAL, line, column);
node->data.int_literal.value = value;
return node;
}
ASTNode* ast_new_identifier(char *name, int line, int column) {
ASTNode *node = ast_new_node(AST_IDENTIFIER, line, column);
node->data.identifier.name = name; // 注意:name 是 strdup 后的,需要负责 free
return node;
}
ASTNode* ast_new_assign_expr(ASTNode *left, ASTNode *right, int line, int column) {
ASTNode *node = ast_new_node(AST_ASSIGN_EXPR, line, column);
node->data.assign_expr.left = left;
node->data.assign_expr.right = right;
return node;
}
// 链表辅助函数实现
ASTNodeList* ast_new_node_list(ASTNode *node) {
ASTNodeList *list_node = (ASTNodeList*)malloc(sizeof(ASTNodeList));
if (!list_node) {
fprintf(stderr, "Fatal Error: Failed to allocate ASTNodeList.\n");
exit(EXIT_FAILURE);
}
list_node->node = node;
list_node->next = NULL;
return list_node;
}
void ast_add_to_list(ASTNodeList **head, ASTNode *node) {
ASTNodeList *new_list_node = ast_new_node_list(node);
if (*head == NULL) {
*head = new_list_node;
} else {
ASTNodeList *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_list_node;
}
}
// 遍历和打印AST(用于调试和验证)
static void print_indent(int indent) {
for (int i = 0; i < indent; i++) {
printf(" "); // 每个缩进使用两个空格
}
}
void ast_print(ASTNode *node, int indent) {
if (!node) return;
print_indent(indent);
printf("Type: %s ", token_type_to_string(node->type)); // ASTNodeType 没有 token_type_to_string,这里需要一个 ast_node_type_to_string 函数
// TODO: 修正这里,应该用 ASTNodeType 的字符串表示
switch (node->type) {
case AST_PROGRAM:
printf("PROGRAM\n");
ASTNodeList *decls = node->data.program_decls;
while (decls) {
ast_print(decls->node, indent + 1);
decls = decls->next;
}
break;
case AST_FUNCTION_DECL:
printf("FUNCTION_DECL (Return: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.func_decl.return_type),
node->data.func_decl.name,
node->line, node->column);
print_indent(indent + 1); printf("Body:\n");
ast_print(node->data.func_decl.body, indent + 2);
break;
case AST_COMPOUND_STMT:
printf("COMPOUND_STMT at [%d:%d]\n", node->line, node->column);
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
ast_print(stmts->node, indent + 1);
stmts = stmts->next;
}
break;
case AST_VAR_DECL:
printf("VAR_DECL (Type: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.var_decl.type_specifier),
node->data.var_decl.name,
node->line, node->column);
if (node->data.var_decl.initializer) {
print_indent(indent + 1); printf("Initializer:\n");
ast_print(node->data.var_decl.initializer, indent + 2);
}
break;
case AST_EXPR_STMT:
printf("EXPR_STMT at [%d:%d]\n", node->line, node->column);
ast_print(node->data.expr_stmt.expr, indent + 1);
break;
case AST_RETURN_STMT:
printf("RETURN_STMT at [%d:%d]\n", node->line, node->column);
if (node->data.return_stmt.expr) {
print_indent(indent + 1); printf("Expression:\n");
ast_print(node->data.return_stmt.expr, indent + 2);
}
break;
case AST_IF_STMT:
printf("IF_STMT at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Condition:\n");
ast_print(node->data.if_stmt.condition, indent + 2);
print_indent(indent + 1); printf("Then:\n");
ast_print(node->data.if_stmt.then_stmt, indent + 2);
if (node->data.if_stmt.else_stmt) {
print_indent(indent + 1); printf("Else:\n");
ast_print(node->data.if_stmt.else_stmt, indent + 2);
}
break;
case AST_BINARY_EXPR:
printf("BINARY_EXPR (Op: %s) at [%d:%d]\n",
token_type_to_string(node->data.binary_expr.op),
node->line, node->column);
print_indent(indent + 1); printf("Left:\n");
ast_print(node->data.binary_expr.left, indent + 2);
print_indent(indent + 1); printf("Right:\n");
ast_print(node->data.binary_expr.right, indent + 2);
break;
case AST_INTEGER_LITERAL:
printf("INTEGER_LITERAL (Value: %d) at [%d:%d]\n",
node->data.int_literal.value,
node->line, node->column);
break;
case AST_IDENTIFIER:
printf("IDENTIFIER (Name: %s) at [%d:%d]\n",
node->data.identifier.name,
node->line, node->column);
break;
case AST_ASSIGN_EXPR:
printf("ASSIGN_EXPR at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Left (L-Value):\n");
ast_print(node->data.assign_expr.left, indent + 2);
print_indent(indent + 1); printf("Right (R-Value):\n");
ast_print(node->data.assign_expr.right, indent + 2);
break;
// 添加其他AST节点类型的打印逻辑...
default:
printf("UNKNOWN_AST_NODE_TYPE (Type: %d) at [%d:%d]\n",
node->type, node->line, node->column);
break;
}
}
// 释放AST节点及其子节点占用的内存
void ast_free(ASTNode *node) {
if (!node) return;
// 递归释放子节点
switch (node->type) {
case AST_PROGRAM:
{
ASTNodeList *decls = node->data.program_decls;
while (decls) {
ASTNodeList *next = decls->next;
ast_free(decls->node);
free(decls);
decls = next;
}
}
break;
case AST_FUNCTION_DECL:
free(node->data.func_decl.name); // 释放复制的函数名
ast_free(node->data.func_decl.body);
// TODO: 释放参数列表
break;
case AST_COMPOUND_STMT:
{
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
ASTNodeList *next = stmts->next;
ast_free(stmts->node);
free(stmts);
stmts = next;
}
}
break;
case AST_VAR_DECL:
free(node->data.var_decl.name); // 释放复制的变量名
ast_free(node->data.var_decl.initializer);
break;
case AST_EXPR_STMT:
ast_free(node->data.expr_stmt.expr);
break;
case AST_RETURN_STMT:
ast_free(node->data.return_stmt.expr);
break;
case AST_IF_STMT:
ast_free(node->data.if_stmt.condition);
ast_free(node->data.if_stmt.then_stmt);
ast_free(node->data.if_stmt.else_stmt);
break;
case AST_BINARY_EXPR:
ast_free(node->data.binary_expr.left);
ast_free(node->data.binary_expr.right);
break;
case AST_IDENTIFIER:
free(node->data.identifier.name); // 释放复制的标识符名
break;
case AST_ASSIGN_EXPR:
ast_free(node->data.assign_expr.left);
ast_free(node->data.assign_expr.right);
break;
// 对于 INTEGER_LITERAL 等不包含子节点的类型,无需递归
case AST_INTEGER_LITERAL:
case AST_UNKNOWN: // Placeholder for future, or for error nodes
case TOKEN_UNKNOWN: // For error nodes
break; // No child nodes or allocated strings in these.
default:
// 确保所有类型都被处理,避免遗漏内存泄漏
fprintf(stderr, "Warning: Unhandled AST node type in ast_free: %d\n", node->type);
break;
}
free(node); // 最后释放当前节点
}
// --- 语法分析器入口点 ---
ASTNode* parse(const char *source_code_path) {
// 1. 初始化词法分析器
if (init_lexer(source_code_path) != 0) {
fprintf(stderr, "Error: Failed to initialize lexer.\n");
return NULL;
}
// 2. 获取第一个Token,开始解析
current_token = get_next_token();
// 3. 开始解析整个程序
ASTNode *program_ast = parse_program();
// 4. 清理词法分析器资源
close_lexer();
return program_ast;
}
// --- 主函数,用于测试语法分析器 ---
// 将覆盖原 lexer.c 中的 main 函数,作为整个编译器前端的测试驱动。
int main(int argc, char *argv[]) {
// 检查命令行参数
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
printf("--- Syntax Analysis Started ---\n");
// 调用解析器,获取AST
ASTNode *program_ast = parse(argv[1]);
if (program_ast) {
printf("\n--- Abstract Syntax Tree (AST) ---\n");
ast_print(program_ast, 0); // 打印生成的AST
printf("\n--- Syntax Analysis Complete ---\n");
// 释放AST占用的内存
ast_free(program_ast);
printf("AST memory freed.\n");
} else {
printf("\n--- Syntax Analysis Failed ---\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `parser.c` 是我们语法分析器的真正实现,它利用了递归下降的策略来解析C语言的简化文法。
*
* **1. Token 操作辅助函数:**
* - `parser_error()`: 报告语法错误,并指示错误发生在哪个Token的位置。
* - `expect(TokenType type)`: 检查当前Token是否是期望的类型。如果不是,就报告错误并退出。这是解析器中实现“强制匹配”的关键。
* - `consume()`: 推进Token流,获取下一个Token。这是解析器前进的唯一方式。
* 注意这里对 `current_token.lexeme` 的 `free()` 操作,因为它是由 `strdup` 分配的。
* - `match_and_consume(TokenType type)`: 尝试匹配并消费一个Token,如果匹配成功则返回真,否则返回假且不消费Token。
* 这用于可选的语法元素或区分不同分支。
*
* **2. 递归下降解析函数 (`parse_xxx`):**
* - **核心思想:** 每个函数都对应C语言文法中的一个非终结符(例如 `<program>`, `<function_declaration>`, `<statement>`, `<expression>` 等)。
* 这些函数会递归地调用彼此,尝试匹配Token流,并在此过程中构建AST节点。
* - **优先级处理:** 对于表达式,我们使用经典的“运算符优先级解析”方法。
* 例如,`parse_primary_expression` 解析最高优先级(数字、标识符、括号)。
* `parse_multiplicative_expression` 调用 `parse_primary_expression` 来获取操作数,
* 然后处理 `*` 和 `/` 运算符。
* `parse_additive_expression` 调用 `parse_multiplicative_expression` 来获取操作数,
* 然后处理 `+` 和 `-` 运算符,以此类推。
* 这种链式调用自然地处理了运算符的优先级和结合性。
* - **左结合性:** 对于左结合的运算符(如 `+`, `-`, `*`, `/`, `==`, `!=`, `<`, etc.),
* 解析函数会使用 `while` 循环来重复处理相同的优先级运算符。
* 例如,`a + b + c` 会被解析为 `(a + b) + c`。
* - **右结合性:** 对于右结合的运算符(如 `=` 赋值运算符),解析函数会在右操作数处递归调用自身。
* 例如,`a = b = c` 会被解析为 `a = (b = c)`。
* - **错误恢复(简化):** 目前的错误处理是“恐慌模式”,即遇到错误就直接退出。
* 在实际编译器中,错误恢复机制会更复杂,尝试跳过一些Token以继续解析,从而报告更多错误。
* - **AST 构建:** 在每个解析函数中,当识别出一个完整的语法结构时,就会调用 `ast_new_xxx` 辅助函数来创建对应的AST节点,并将子节点的指针连接起来。
*
* **3. 表达式文法与解析函数对应关系(简化示例):**
* - `<expression>` -> `parse_expression()`
* - `<assignment_expression>` -> `parse_assignment_expression()`
* - `<equality_expression>` -> `parse_equality_expression()`
* - `<relational_expression>` -> `parse_relational_expression()`
* - `<additive_expression>` -> `parse_additive_expression()`
* - `<multiplicative_expression>` -> `parse_multiplicative_expression()`
* - `<primary_expression>` -> `parse_primary_expression()`
*
* **4. `parse()` 函数:**
* - 它是语法分析器的外部接口,负责初始化词法分析器,获取第一个Token,然后调用 `parse_program()` 开始整个解析过程。
* - 解析完成后,它会清理词法分析器资源。
*
* **5. 测试驱动 `main()` 函数:**
* - 接管了 `lexer.c` 中原有的 `main` 函数,现在它作为整个前端(词法+语法)的测试入口。
* - 它调用 `parse()` 函数来解析源代码,如果成功则调用 `ast_print()` 来打印生成的AST,以便我们可视化和验证解析器的正确性。
* - 最后,通过 `ast_free()` 释放AST占用的内存。
*
* **挑战与待完善:**
* - **完整的C语言文法:** 目前只支持C语言的一个极小简化子集。要支持完整的C语言,需要大量的递归下降函数来覆盖所有复杂的语法规则(如指针、数组、结构体、联合体、枚举、各种运算符的优先级和结合性、类型转换、函数参数、局部变量作用域等)。
* - **错误恢复:** 当前的“恐慌模式”错误处理会导致程序在遇到第一个语法错误时就退出。更健壮的编译器会尝试跳过错误并继续报告。
* - **符号表与语义分析:** 语法分析只关心结构是否正确,不关心意义。例如,`int a; a = b;` 在语法上是正确的,但如果 `b` 未声明,这就是语义错误。这将在下一个阶段——语义分析中处理。
* - **左递归问题:** 递归下降解析器不能直接处理左递归文法(如 `Expr ::= Expr + Term`)。需要将其改写为右递归或使用迭代方式。我们通过将优先级低的表达式函数作为循环的入口来避免了显式的左递归。
* - **AST打印优化:** `ast_print` 函数中 `token_type_to_string(node->type)` 调用是错误的,因为 `node->type` 是 `ASTNodeType`,不是 `TokenType`。需要单独的 `ast_node_type_to_string` 函数。
*/
为了让 ast_print
函数能够正确打印 ASTNodeType
,我们需要在 ast.h
中添加一个辅助函数 ast_node_type_to_string
,并在 parser.c
中实现它。
4. ast_node_type_to_string
实现 (添加到 parser.c
顶部附近)
// 文件名: ast.h (只需要声明)
// ...
const char* ast_node_type_to_string(ASTNodeType type); // 在 ast.h 中添加此声明
// ...
```c
// 文件名: parser.c (实现 ast_node_type_to_string 函数)
// ...
// 将ASTNodeType转换为可读字符串(用于调试)
// 这是AST打印的关键辅助函数
const char* ast_node_type_to_string(ASTNodeType type) {
switch (type) {
case AST_PROGRAM: return "AST_PROGRAM";
case AST_FUNCTION_DECL: return "AST_FUNCTION_DECL";
case AST_COMPOUND_STMT: return "AST_COMPOUND_STMT";
case AST_VAR_DECL: return "AST_VAR_DECL";
case AST_EXPR_STMT: return "AST_EXPR_STMT";
case AST_RETURN_STMT: return "AST_RETURN_STMT";
case AST_IF_STMT: return "AST_IF_STMT";
case AST_WHILE_STMT: return "AST_WHILE_STMT";
case AST_FOR_STMT: return "AST_FOR_STMT";
case AST_BINARY_EXPR: return "AST_BINARY_EXPR";
case AST_UNARY_EXPR: return "AST_UNARY_EXPR";
case AST_INTEGER_LITERAL: return "AST_INTEGER_LITERAL";
case AST_IDENTIFIER: return "AST_IDENTIFIER";
case AST_ASSIGN_EXPR: return "AST_ASSIGN_EXPR";
case AST_FUNCTION_CALL: return "AST_FUNCTION_CALL";
case AST_STRING_LITERAL: return "AST_STRING_LITERAL";
case AST_CHAR_LITERAL: return "AST_CHAR_LITERAL";
default: return "UNKNOWN_AST_NODE_TYPE";
}
}
// ...
然后将 ast_print
函数中 printf("Type: %s ", token_type_to_string(node->type));
修正为 printf("Type: %s ", ast_node_type_to_string(node->type));
。
5. 测试文件:test.c
使用之前词法分析器的 test.c
即可,我们现在可以解析更复杂的结构。
// 文件名: test.c
// 用于测试自定义C语言词法分析器和语法分析器的示例源代码。
/* 这是一个
* 多行注释
* 可以包含多行文本 */
int main() { // 主函数入口
// 声明一个整数变量并初始化
int my_var_1 = 10;
// 另一个变量
int anotherVar; // 未初始化声明
/* 这是一个
* 测试
* 注释 */
// 算术运算和赋值
int sum = my_var_1 + anotherVar * 2; // anotherVar 未初始化,这里会有语义问题,但语法分析器不关心
// 赋值语句
anotherVar = 50;
// 复杂的赋值表达式
sum = (my_var_1 + 5) * anotherVar;
// 比较运算和条件语句
if (sum >= 40) {
return 1; // 返回1
} else {
// 嵌套复合语句
int result = 0;
if (my_var_1 < 5) {
result = 100;
}
return result; // 返回0或者100
}
// 更多的表达式类型 (待 parser.c 完整支持)
// int val = !my_var_1; // 一元非
// int bit = 0xFF & 0xAA; // 位运算符
return 2; // 最后的返回值(这段代码应该不会被执行到,但语法上正确)
}
编译和运行语法分析器
要编译和运行:
-
确保你拥有以下文件在同一目录下:
token.h
,lexer.h
,lexer.c
,ast.h
,parser.h
,parser.c
,test.c
。 -
打开终端。
-
使用GCC编译:
gcc -o my_parser lexer.c parser.c -g -Wall
(
-g
用于生成调试信息,-Wall
开启所有警告,有助于发现问题) -
运行语法分析器:
./my_parser test.c
你将看到 test.c
文件被解析后生成的抽象语法树的结构。这是一种分层的、树状的输出,直观地展示了你的C程序是如何被编译器“理解”的。这将是你亲手构建的编译器迈出的又一大步!
总结与展望
在这一部分,我们为我们的C语言编译器构建了其核心的“大脑”——语法分析器。我们深入理解了抽象语法树(AST)的概念,它是源代码语法结构的简洁、抽象表示,为后续的编译阶段提供了统一的输入。通过递归下降解析的实现,我们亲手将Token流转化为这种树状结构,处理了C语言的简化文法规则,包括函数定义、变量声明、赋值语句、条件语句以及各种表达式的优先级和结合性。
你亲手编写的这些代码,让程序从一堆字符变成了有逻辑、有结构的“思维导图”。这不仅是技术上的进步,更是对C语言乃至整个计算机科学原理的更深层次理解。我们已经完成了编译器前端的关键部分:词法分析器和语法分析器。
然而,当前的AST仅仅表示了程序的结构,还没有理解其意义。例如,我们声明了一个变量 int my_var_1 = 10;
,但如果后面我们试图使用一个未声明的变量,或者对不同类型的变量进行不兼容的操作,当前的语法分析器是不会报错的。这些“有意义”的检查,将是下一阶段——语义分析器的任务。
在下一个篇章中,我们将踏上语义分析的旅程。我们将学习如何构建和使用符号表,如何在AST上进行类型检查、作用域检查,并为后续的中间代码生成做准备。这将是编译器从“理解结构”到“理解逻辑”的关键飞跃!让我们一起期待!
--------------------------------------------------------------------------------------------------------更新于2025.6.27号
(5)手撸编译器:深入核心——语义分析器与符号表
各位编程的武林高手们,我们一路披荆斩棘,先是打造了编译器的“眼睛”——词法分析器,它将原始代码打散成一个个Token。接着,我们又赋予编译器“大脑”——语法分析器,它将这些Token组装成有结构、有层次的抽象语法树(AST)。现在,我们的编译器已经能“看懂”代码的结构了,但它还未能“理解”代码的真正含义。
想象一下,你学会了语法,能写出“红色的想法疯狂地睡觉”这样的句子,它语法正确,但毫无意义。在编程语言中,同样会发生这样的情况:int a; b = a + c;
这句话,从语法上看,完美无缺。但如果 b
和 c
根本没有声明,或者 a
是一个函数名而不是变量,那么这条语句在语义上就是错误的。
这就是我们今天要探讨的第三个核心组件——语义分析器(Semantic Analyzer)。它将赋予编译器“理解”代码逻辑的能力,确保程序的“意义”是正确且合法的。
语义分析器的核心使命
语义分析器是编译器前端的第三个阶段。它的主要任务是:
-
输入: 接收语法分析器生成的抽象语法树(AST)。
-
输出: 经过批注(Annotated)或转换(Transformed)的AST,其中包含了类型信息、作用域信息等,并为后续的中间代码生成提供便利。
-
错误检测: 执行各种语义检查,如果发现语义错误(例如变量未声明、类型不匹配、函数调用参数不正确等),则报告错误。
-
符号表管理: 构建和维护一个或多个符号表(Symbol Table),这是语义分析阶段最重要的数据结构。
符号表:编译器的“活字典”与“记忆”
在语义分析阶段,编译器需要跟踪程序中所有标识符(变量、函数、类型等)的信息。这些信息包括:
-
名称: 标识符的字面值。
-
类型: 标识符的数据类型(例如
int
,char*
,void (*)(int)
)。 -
作用域: 标识符的可见范围(全局、局部、块级)。
-
存储位置: 标识符在内存或寄存器中的分配信息(通常在后续阶段确定,但语义分析会为它预留)。
-
其他属性: 例如,是否是常量、是否是函数、参数列表、结构体成员等。
所有这些信息都被组织起来,存储在一个叫做符号表的数据结构中。符号表就像编译器的“活字典”,它记录了程序中每一个“名字”的详细信息。
符号表的结构与管理
符号表的实现方式多种多样,常见的有:
-
线性链表: 简单易实现,但在大型程序中查找效率低。
-
哈希表: 查找、插入和删除的平均时间复杂度为 O(1),效率高,是实际编译器常用的实现。
-
栈式符号表(Stack-based Symbol Table): 用于处理嵌套作用域。当进入一个新的作用域(如函数体、
if
语句块、for
循环体)时,就创建一个新的符号表层级并压入栈;退出作用域时,则弹出相应的层级。查找符号时,总是从当前作用域开始向外层(全局)查找。
考虑到我们是手撸编译器,为了兼顾性能和实现复杂度,我们可以选择哈希表作为符号表的基础数据结构,并辅以栈式管理来处理作用域。
手撸符号表:C语言实现
我们将构建一个简单的栈式符号表,支持基本的数据类型和标识符的查找。
1. symbol_table.h
:符号表的定义与接口
// 文件名: symbol_table.h
// 描述: 定义符号表(Symbol Table)的数据结构和接口。
// 符号表用于在语义分析阶段存储程序中所有标识符的信息,
// 并处理变量、函数的作用域和类型。
#ifndef SYMBOL_TABLE_H
#define SYMBOL_TABLE_H
#include <stdio.h>
#include <stdbool.h>
#include "token.h" // 包含Token类型,用于表示变量类型
// 定义符号的类型(例如:变量、函数、类型定义等)
typedef enum {
SYM_VAR, // 变量
SYM_FUNC, // 函数
SYM_TYPE, // 类型定义 (typedef) (待实现)
// 更多类型待扩展
} SymbolKind;
// 定义符号表中的一个条目(Entry)
// 每个标识符在符号表中都对应一个SymbolEntry。
typedef struct SymbolEntry {
char *name; // 标识符名称
SymbolKind kind; // 符号种类(变量、函数等)
TokenType type; // 标识符的数据类型(例如 TOKEN_INT, TOKEN_VOID等)
int scope_level; // 作用域级别(0为全局,越大表示嵌套越深)
// 其他属性,例如:
// int offset; // 在栈帧或全局数据区中的偏移量(用于代码生成)
// ASTNodeList *params; // 如果是函数,存储参数列表 (待实现)
struct SymbolEntry *next; // 用于解决哈希冲突的链表(拉链法)
} SymbolEntry;
// 符号表结构体
// 使用哈希表实现,每个哈希桶是一个SymbolEntry的链表。
typedef struct SymbolTable {
SymbolEntry **buckets; // 哈希桶数组,每个元素是一个指向SymbolEntry链表头部的指针
int capacity; // 哈希表的容量(桶的数量)
int current_level; // 当前作用域级别
struct SymbolTable *parent; // 指向父作用域的符号表(用于构建作用域链)
} SymbolTable;
// --- 接口函数声明 ---
// 初始化符号表(通常用于创建全局符号表)
// capacity: 哈希表的初始容量。
SymbolTable* symtab_init(int capacity);
// 进入新的作用域,创建新的符号表层级
// 参数: parent_table - 父作用域的符号表。
// 返回值: 新创建的子作用域符号表。
SymbolTable* symtab_enter_scope(SymbolTable *parent_table);
// 退出当前作用域,并释放其资源
// 返回值: 父作用域的符号表。
SymbolTable* symtab_exit_scope(SymbolTable *current_table);
// 向当前作用域的符号表中添加一个新符号
// 参数: table - 目标符号表
// name - 标识符名称
// kind - 符号种类 (SYM_VAR, SYM_FUNC等)
// type - 数据类型 (TOKEN_INT, TOKEN_VOID等)
// 返回值: 成功返回SymbolEntry指针,失败返回NULL (例如:重复定义)
SymbolEntry* symtab_add_symbol(SymbolTable *table, char *name, SymbolKind kind, TokenType type);
// 在当前作用域及所有外层作用域中查找符号
// 参数: table - 当前作用域的符号表
// name - 待查找的标识符名称
// 返回值: 找到返回SymbolEntry指针,未找到返回NULL
SymbolEntry* symtab_lookup_symbol(SymbolTable *table, const char *name);
// 仅在当前作用域中查找符号
// 参数: table - 当前作用域的符号表
// name - 待查找的标识符名称
// 返回值: 找到返回SymbolEntry指针,未找到返回NULL
SymbolEntry* symtab_lookup_current_scope(SymbolTable *table, const char *name);
// 辅助函数:打印符号表内容(用于调试)
void symtab_print(SymbolTable *table, int indent);
// 辅助函数:释放符号表及其所有条目占用的内存
void symtab_free(SymbolTable *table);
#endif // SYMBOL_TABLE_H
/*
* 逻辑分析:
* `symbol_table.h` 定义了符号表的“骨架”和外部可用的操作。
*
* 1. `SymbolKind` 枚举:
* - 明确了符号的分类,如变量、函数等。这有助于语义分析器根据符号的种类执行不同的检查。
*
* 2. `SymbolEntry` 结构体:
* - 代表了程序中一个标识符的所有相关信息。
* - `name`: 标识符的字符串名称。
* - `kind`: 标识符的种类(变量、函数等)。
* - `type`: 标识符的数据类型。
* - `scope_level`: 作用域级别,0表示全局作用域,数字越大表示嵌套越深。
* 这对于区分同名但在不同作用域的符号非常重要。
* - `next`: 用于哈希冲突链表。
*
* 3. `SymbolTable` 结构体:
* - `buckets`: 指向一个 `SymbolEntry` 指针数组,这是哈希表的桶。
* 每个桶是一个链表的头,用于存储哈希到相同位置的符号。
* - `capacity`: 哈希表的容量(桶的数量)。
* - `current_level`: 当前符号表所代表的作用域级别。
* - `parent`: 指向父作用域的符号表。通过这个指针,我们可以构建一个作用域链,
* 实现符号的逐级查找(从内层作用域到外层作用域)。
*
* 4. 接口函数:
* - `symtab_init()`: 初始化一个符号表(通常是全局符号表)。
* - `symtab_enter_scope()`, `symtab_exit_scope()`: 实现作用域的进出管理。
* `symtab_enter_scope()` 会创建一个新的子符号表,并将其 `parent` 指向当前作用域。
* `symtab_exit_scope()` 则会回到父作用域,并清理当前作用域的资源。
* - `symtab_add_symbol()`: 在当前作用域添加新符号。需要检查是否重复定义。
* - `symtab_lookup_symbol()`: 在当前作用域及所有父作用域中查找符号,遵循“就近原则”(从最近的作用域开始查找)。
* - `symtab_lookup_current_scope()`: 仅在当前作用域中查找符号。这对于检测重复定义很重要。
* - `symtab_print()`, `symtab_free()`: 辅助函数,用于调试和内存管理。
*
* 设计考量:
* - **分层作用域:** `parent` 指针和 `scope_level` 字段是实现C语言作用域规则的关键。
* 当一个变量在内层作用域被声明时,它会“遮蔽”外层作用域的同名变量。
* - **哈希冲突处理:** 采用拉链法(Separate Chaining)解决哈希冲突,即每个哈希桶是一个链表。
* - **内存管理:** 需要仔细处理 `malloc` 和 `free`,确保所有动态分配的内存都被正确释放,特别是符号名称 `name` 和 `SymbolEntry` 本身。
*/
2. symbol_table.c
:符号表的实现
// 文件名: symbol_table.c
// 描述: 符号表(Symbol Table)的核心实现。
// 包含了哈希表的底层逻辑和作用域管理。
#include <stdlib.h>
#include <string.h>
#include <stdio.h> // for NULL, fprintf, printf
#include <stdbool.h> // for bool
#include "symbol_table.h"
#include "token.h" // 假设 token.h 包含 token_type_to_string()
#include "parser.h" // 用于 parser_error()
// --- 内部辅助函数 ---
// 哈希函数:简单地将字符串哈希到指定容量的桶中。
// 这是哈希表性能的关键。
static unsigned int hash_string(const char *str, int capacity) {
unsigned int hash = 5381; // 一个常用的初始值
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c; // hash * 33 + c
}
return hash % capacity; // 取模以适应桶容量
}
// 创建新的SymbolEntry
static SymbolEntry* create_symbol_entry(char *name, SymbolKind kind, TokenType type, int scope_level) {
SymbolEntry *entry = (SymbolEntry*)malloc(sizeof(SymbolEntry));
if (!entry) {
fprintf(stderr, "Fatal Error: Failed to allocate SymbolEntry.\n");
exit(EXIT_FAILURE);
}
entry->name = strdup(name); // 复制名称,因为原始字符串可能被释放或改变
if (!entry->name) {
fprintf(stderr, "Fatal Error: Failed to strdup symbol name.\n");
free(entry);
exit(EXIT_FAILURE);
}
entry->kind = kind;
entry->type = type;
entry->scope_level = scope_level;
entry->next = NULL; // 初始时没有下一个,用于链表
return entry;
}
// --- 接口函数实现 ---
// 初始化符号表
SymbolTable* symtab_init(int capacity) {
SymbolTable *table = (SymbolTable*)malloc(sizeof(SymbolTable));
if (!table) {
fprintf(stderr, "Fatal Error: Failed to allocate SymbolTable.\n");
exit(EXIT_FAILURE);
}
table->capacity = capacity;
table->buckets = (SymbolEntry**)calloc(capacity, sizeof(SymbolEntry*)); // calloc 会将内存清零
if (!table->buckets) {
fprintf(stderr, "Fatal Error: Failed to allocate SymbolTable buckets.\n");
free(table);
exit(EXIT_FAILURE);
}
table->current_level = 0; // 0 表示全局作用域
table->parent = NULL; // 全局符号表没有父表
return table;
}
// 进入新的作用域
SymbolTable* symtab_enter_scope(SymbolTable *parent_table) {
// 确保有父表,否则不能进入新作用域(除了全局作用域)
if (!parent_table) {
fprintf(stderr, "Semantic Error: Cannot enter scope without a parent table (if not global).\n");
exit(EXIT_FAILURE);
}
SymbolTable *new_table = symtab_init(parent_table->capacity); // 新作用域通常和父作用域容量相同
new_table->parent = parent_table;
new_table->current_level = parent_table->current_level + 1; // 作用域级别递增
return new_table;
}
// 退出当前作用域
SymbolTable* symtab_exit_scope(SymbolTable *current_table) {
if (!current_table) {
fprintf(stderr, "Semantic Error: Cannot exit NULL scope.\n");
exit(EXIT_FAILURE);
}
SymbolTable *parent = current_table->parent;
symtab_free(current_table); // 释放当前作用域的资源
return parent; // 返回父作用域
}
// 向当前作用域的符号表中添加一个新符号
SymbolEntry* symtab_add_symbol(SymbolTable *table, char *name, SymbolKind kind, TokenType type) {
if (!table || !name) {
parser_error("Invalid arguments for symtab_add_symbol."); // 使用 parser_error 报告语义错误
}
// 首先在当前作用域查找是否已存在同名符号(重复定义检查)
if (symtab_lookup_current_scope(table, name) != NULL) {
parser_error("Semantic Error: Redefinition of '%s' in current scope.", name);
}
unsigned int index = hash_string(name, table->capacity);
SymbolEntry *new_entry = create_symbol_entry(name, kind, type, table->current_level);
// 将新条目添加到哈希桶链表的头部(拉链法)
new_entry->next = table->buckets[index];
table->buckets[index] = new_entry;
return new_entry;
}
// 在当前作用域及所有外层作用域中查找符号
SymbolEntry* symtab_lookup_symbol(SymbolTable *table, const char *name) {
SymbolTable *current_scope_table = table;
while (current_scope_table != NULL) {
SymbolEntry *found_entry = symtab_lookup_current_scope(current_scope_table, name);
if (found_entry != NULL) {
return found_entry; // 在当前作用域找到,立即返回
}
current_scope_table = current_scope_table->parent; // 在父作用域中继续查找
}
return NULL; // 未找到
}
// 仅在当前作用域中查找符号
SymbolEntry* symtab_lookup_current_scope(SymbolTable *table, const char *name) {
if (!table || !name) {
return NULL;
}
unsigned int index = hash_string(name, table->capacity);
SymbolEntry *current_entry = table->buckets[index];
while (current_entry != NULL) {
if (strcmp(current_entry->name, name) == 0) {
return current_entry; // 找到匹配的符号
}
current_entry = current_entry->next;
}
return NULL; // 在当前作用域中未找到
}
// 辅助函数:打印符号表内容
void symtab_print(SymbolTable *table, int indent) {
if (!table) return;
for (int i = 0; i < indent; i++) {
printf(" ");
}
printf("Scope Level %d (Capacity: %d):\n", table->current_level, table->capacity);
for (int i = 0; i < table->capacity; i++) {
SymbolEntry *entry = table->buckets[i];
if (entry) {
for (int j = 0; j < indent + 1; j++) {
printf(" ");
}
printf("Bucket %d:\n", i);
while (entry) {
for (int j = 0; j < indent + 2; j++) {
printf(" ");
}
printf("- Name: %s, Kind: %d, Type: %s\n",
entry->name, entry->kind, token_type_to_string(entry->type));
entry = entry->next;
}
}
}
if (table->parent) {
for (int i = 0; i < indent; i++) {
printf(" ");
}
printf("Parent Scope:\n");
symtab_print(table->parent, indent + 1); // 递归打印父作用域 (调试时可能不需要深度遍历父作用域)
}
}
// 辅助函数:释放符号表及其所有条目占用的内存
void symtab_free(SymbolTable *table) {
if (!table) return;
for (int i = 0; i < table->capacity; i++) {
SymbolEntry *entry = table->buckets[i];
while (entry) {
SymbolEntry *temp = entry;
entry = entry->next;
free(temp->name); // 释放名称字符串
free(temp); // 释放条目本身
}
}
free(table->buckets); // 释放桶数组
free(table); // 释放符号表结构体
}
/*
* 逻辑分析:
* `symbol_table.c` 实现了符号表的具体逻辑,包括哈希函数、条目管理和作用域遍历。
*
* **1. 哈希函数 (`hash_string`):**
* - 这是一个简单的字符串哈希函数,将字符串映射到一个整数索引。
* - 好的哈希函数能有效减少冲突,提高查找效率。这里使用了经典的 DJB2 哈希算法的变体。
*
* **2. `create_symbol_entry()`:**
* - 辅助函数,用于动态分配内存并初始化 `SymbolEntry`。
* - **注意 `strdup(name)`:** 这是关键!它会复制传入的 `name` 字符串。
* 因为 `name` 可能是临时缓冲区或来自Token的 `lexeme`,这些内存可能在符号表条目生命周期结束前被释放。
* 复制一份确保了符号名称的持久性,但同时也意味着在释放 `SymbolEntry` 时需要 `free(entry->name)`。
*
* **3. `symtab_init()`:**
* - 创建并初始化一个 `SymbolTable` 结构体。
* - `calloc()` 用于分配哈希桶数组并将其清零,确保每个桶的初始链表头是 `NULL`。
* - 设置初始作用域级别为0(全局)。
*
* **4. 作用域管理 (`symtab_enter_scope`, `symtab_exit_scope`):**
* - `symtab_enter_scope()`:
* - 创建一个新的 `SymbolTable` 实例作为子作用域。
* - 将新表的 `parent` 指针设置为当前父表。
* - 增加 `current_level`,表示作用域深度。
* - `symtab_exit_scope()`:
* - 获取父表指针。
* - **重要:** 调用 `symtab_free()` 释放当前作用域的所有资源。
* - 返回父表,以便调用者可以切换回父作用域。
*
* **5. 符号添加 (`symtab_add_symbol`):**
* - 首先调用 `symtab_lookup_current_scope()` 检查当前作用域是否有同名符号,防止重复定义。
* - 计算哈希值,创建新的 `SymbolEntry`。
* - 使用拉链法将新条目添加到对应哈希桶链表的头部。
*
* **6. 符号查找 (`symtab_lookup_symbol`, `symtab_lookup_current_scope`):**
* - `symtab_lookup_current_scope()`: 只在当前符号表(哈希桶)中查找。用于重复定义检查。
* - `symtab_lookup_symbol()`: 这是语义分析中主要的查找函数。
* - 它会从当前作用域开始查找。
* - 如果在当前作用域未找到,则沿着 `parent` 指针向上(向外层作用域)递归查找,直到找到或到达全局作用域的顶部(`NULL`)。
* - 这精确模拟了C语言的标识符查找规则:“就近原则”。
*
* **7. 内存释放 (`symtab_free`):**
* - 遍历所有哈希桶,然后遍历每个桶中的链表。
* - 对于每个 `SymbolEntry`,先 `free(entry->name)`,再 `free(entry)`。
* - 最后释放 `buckets` 数组和 `table` 结构体本身。
* **这是确保不发生内存泄漏的关键!**
*
* **8. 调试函数 (`symtab_print`):**
* - 用于打印符号表的结构,包括作用域级别、哈希桶内容和每个符号的详细信息。
* - 这在调试语义分析器时非常有用。
*
* **整合到 `parser.c` 或一个新的 `semantic_analyzer.c`:**
* 这些符号表的操作函数将在语义分析阶段被调用。
* 例如,当解析到变量声明时,会调用 `symtab_add_symbol`。
* 当解析到变量使用时,会调用 `symtab_lookup_symbol`。
* 当进入/退出函数体或代码块时,会调用 `symtab_enter_scope` 和 `symtab_exit_scope`。
*/
3. 增强 parser.h
和 parser.c
以支持语义分析(集成符号表)
为了演示语义分析,我们将把符号表集成到解析器中。在实际的编译器中,语义分析可能是一个独立的遍历AST的阶段,但在我们的手撸编译器中,可以在语法分析的同时进行部分语义检查,或者在解析器之后再进行一次AST遍历。这里,我们选择在解析的同时进行简单的语义检查。
修改 parser.h
需要添加全局的符号表指针,并在解析函数中传递它。
// 文件名: parser.h (更新)
#ifndef PARSER_H
#define PARSER_H
#include "ast.h"
#include "token.h"
#include "lexer.h"
#include "symbol_table.h" // 新增: 包含符号表定义
// 声明全局符号表指针
extern SymbolTable *global_symtab;
extern SymbolTable *current_symtab; // 当前活跃的符号表(当前作用域)
ASTNode* parse(const char *source_code_path);
void parser_error(const char *format, ...);
// 辅助函数声明(为了更好的模块化和语义分析阶段的访问)
// static ASTNode* parse_program(); (不再声明为 static,因为可能需要从外部调用)
// ... 其他 parse_xxx 函数 ...
#endif // PARSER_H
修改 parser.c
这将是改动最大的部分。我们需要:
-
定义全局符号表变量。
-
在
parse()
函数中初始化全局符号表并进入第一个作用域。 -
在解析变量声明时,将变量添加到符号表。
-
在解析标识符使用时,从符号表中查找标识符。
-
在进入和退出复合语句时,管理作用域。
-
进行基本的类型检查。
// 文件名: parser.c (更新)
// 描述: 语法分析器(Parser)和初步语义分析器的核心实现。
// 它负责从词法分析器获取Token流,构建抽象语法树(AST),
// 并同时进行作用域管理和基本的类型检查。
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <stdbool.h> // for bool type
#include "token.h"
#include "lexer.h"
#include "ast.h"
#include "parser.h"
#include "symbol_table.h" // 包含符号表头文件
// --- 全局变量定义 ---
// current_token 是由 lexer.c 中的 get_next_token 填充的全局变量。
// 这里直接使用它。
SymbolTable *global_symtab = NULL; // 全局符号表
SymbolTable *current_symtab = NULL; // 当前作用域的符号表
// 将ASTNodeType转换为可读字符串(用于调试)
// 这是AST打印的关键辅助函数
const char* ast_node_type_to_string(ASTNodeType type) {
switch (type) {
case AST_PROGRAM: return "AST_PROGRAM";
case AST_FUNCTION_DECL: return "AST_FUNCTION_DECL";
case AST_COMPOUND_STMT: return "AST_COMPOUND_STMT";
case AST_VAR_DECL: return "AST_VAR_DECL";
case AST_EXPR_STMT: return "AST_EXPR_STMT";
case AST_RETURN_STMT: return "AST_RETURN_STMT";
case AST_IF_STMT: return "AST_IF_STMT";
case AST_WHILE_STMT: return "AST_WHILE_STMT";
case AST_FOR_STMT: return "AST_FOR_STMT";
case AST_BINARY_EXPR: return "AST_BINARY_EXPR";
case AST_UNARY_EXPR: return "AST_UNARY_EXPR";
case AST_INTEGER_LITERAL: return "AST_INTEGER_LITERAL";
case AST_IDENTIFIER: return "AST_IDENTIFIER";
case AST_ASSIGN_EXPR: return "AST_ASSIGN_EXPR";
case AST_FUNCTION_CALL: return "AST_FUNCTION_CALL";
case AST_STRING_LITERAL: return "AST_STRING_LITERAL";
case AST_CHAR_LITERAL: return "AST_CHAR_LITERAL";
default: return "UNKNOWN_AST_NODE_TYPE";
}
}
// --- 辅助函数:Token 操作 (不变) ---
// 打印语法分析器错误信息
// 这是语法分析阶段的错误报告机制。
void parser_error(const char *format, ...) {
va_list args;
// 使用 current_token 的位置信息,指向当前发生错误的Token
fprintf(stderr, "Parser/Semantic Error [%d:%d]: ", current_token.line, current_token.column);
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, "\n");
exit(EXIT_FAILURE); // 遇到致命错误时退出程序
}
// 检查当前Token类型是否符合预期
// 如果不符合,则报告语法错误。
static void expect(TokenType type) {
if (current_token.type != type) {
parser_error("Expected '%s', but got '%s' ('%s')",
token_type_to_string(type),
token_type_to_string(current_token.type),
current_token.lexeme);
}
}
// 消费当前Token,并获取下一个Token
// 这是解析器向前推进的核心操作。
static void consume() {
// 释放旧Token的lexeme,因为 get_next_token 会 strdup
if (current_token.lexeme && current_token.type != TOKEN_EOF && current_token.type != TOKEN_ERROR) {
free(current_token.lexeme);
current_token.lexeme = NULL;
}
current_token = get_next_token();
}
// 检查当前Token类型是否是预期的一个,如果是则消费并返回真,否则返回假。
static int match_and_consume(TokenType type) {
if (current_token.type == type) {
consume();
return 1;
}
return 0;
}
// --- 递归下降解析函数声明 (前向声明,因为它们互相调用) ---
static ASTNode* parse_program();
static ASTNode* parse_function_decl();
static ASTNode* parse_compound_statement();
static ASTNode* parse_statement();
static ASTNode* parse_declaration();
static ASTNode* parse_expression();
static ASTNode* parse_assignment_expression();
static ASTNode* parse_equality_expression();
static ASTNode* parse_relational_expression();
static ASTNode* parse_additive_expression();
static ASTNode* parse_multiplicative_expression();
static ASTNode* parse_primary_expression();
// --- 递归下降解析函数实现(集成语义分析) ---
// 解析整个程序:<program> ::= { <function_declaration> } EOF
static ASTNode* parse_program() {
ASTNodeList *decls_list = NULL;
// TODO: 在这里添加预定义符号 (如 printf, scanf) 到全局符号表
while (current_token.type != TOKEN_EOF) {
ASTNode *func_decl = parse_function_decl();
if (func_decl) {
ast_add_to_list(&decls_list, func_decl);
} else {
parser_error("Unexpected token at top level: '%s'", current_token.lexeme);
consume();
}
}
expect(TOKEN_EOF);
return ast_new_program(decls_list);
}
// 解析函数声明:<function_declaration> ::= <type_specifier> <IDENTIFIER> "(" ")" <compound_statement>
static ASTNode* parse_function_decl() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_INT); // 目前只支持 int 返回类型
TokenType return_type = current_token.type;
consume();
expect(TOKEN_IDENTIFIER);
char *func_name = strdup(current_token.lexeme);
// 语义检查:在全局作用域中添加函数符号
// 这里假设函数只能在全局作用域定义
SymbolEntry *func_entry = symtab_add_symbol(current_symtab, func_name, SYM_FUNC, return_type);
// TODO: 存储函数的参数信息到 func_entry
consume();
expect(TOKEN_LPAREN);
consume();
// TODO: 解析函数参数,并将参数添加到新的函数作用域中
expect(TOKEN_RPAREN);
consume();
// 语义:进入函数作用域
current_symtab = symtab_enter_scope(current_symtab);
ASTNode *body = parse_compound_statement();
if (!body) {
parser_error("Expected function body (compound statement) for function '%s'", func_name);
}
// 语义:退出函数作用域
current_symtab = symtab_exit_scope(current_symtab);
return ast_new_function_decl(return_type, func_name, body, start_line, start_column);
}
// 解析复合语句(代码块):<compound_statement> ::= "{" { <declaration> | <statement> } "}"
static ASTNode* parse_compound_statement() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_LBRACE);
consume();
// 语义:进入新的块作用域(如果不是函数体)
// 如果当前作用域已经是函数作用域(level > 0),则新的 { 块会创建更深的作用域
// 如果是顶级函数体的第一个 { ,已经在 parse_function_decl 中处理
// 这里只处理嵌套的块作用域
bool new_scope_entered_here = false;
if (current_symtab->current_level > 0 || current_symtab == global_symtab) { // 确保不是函数体的第一层
current_symtab = symtab_enter_scope(current_symtab);
new_scope_entered_here = true;
}
ASTNodeList *statements_list = NULL;
while (current_token.type != TOKEN_RBRACE && current_token.type != TOKEN_EOF) {
ASTNode *node = NULL;
if (current_token.type == TOKEN_INT) { // 简单判断是声明
node = parse_declaration();
} else {
node = parse_statement();
}
if (node) {
ast_add_to_list(&statements_list, node);
} else {
parser_error("Unexpected token in compound statement: '%s'", current_token.lexeme);
consume();
}
}
expect(TOKEN_RBRACE);
consume();
// 语义:退出当前块作用域
if (new_scope_entered_here) {
current_symtab = symtab_exit_scope(current_symtab);
}
return ast_new_compound_stmt(statements_list, start_line, start_column);
}
// 解析语句:<statement> ::= <expression_statement> | <return_statement> | <if_statement> | <compound_statement>
static ASTNode* parse_statement() {
ASTNode *node = NULL;
int start_line = current_token.line;
int start_column = current_token.column;
switch (current_token.type) {
case TOKEN_RETURN:
node = ast_new_return_stmt(NULL, start_line, start_column);
consume();
ASTNode *expr = parse_expression();
node->data.return_stmt.expr = expr;
// TODO: 语义检查:检查返回表达式的类型是否与函数返回类型兼容
expect(TOKEN_SEMICOLON);
consume();
return node;
case TOKEN_IF:
consume();
expect(TOKEN_LPAREN);
consume();
ASTNode *condition = parse_expression();
// 语义检查:条件表达式必须是整数类型(或者可以转换为布尔值的类型)
if (condition && condition->type == AST_INTEGER_LITERAL) { // 简化判断
// 暂时不进行更复杂的类型检查
} else if (condition && condition->type == AST_IDENTIFIER) {
SymbolEntry *sym = symtab_lookup_symbol(current_symtab, condition->data.identifier.name);
if (!sym || sym->type != TOKEN_INT) { // 假设只有int是有效的条件类型
parser_error("Semantic Error: Condition for if statement must be of integer type. Got '%s'",
sym ? token_type_to_string(sym->type) : "undeclared/invalid");
}
} else if (!condition) {
parser_error("Semantic Error: Empty condition for if statement.");
}
// 更复杂的条件检查需要 AST 节点类型推断
expect(TOKEN_RPAREN);
consume();
ASTNode *then_stmt = parse_statement();
ASTNode *else_stmt = NULL;
if (current_token.type == TOKEN_ELSE) {
consume();
else_stmt = parse_statement();
}
return ast_new_if_stmt(condition, then_stmt, else_stmt, start_line, start_column);
case TOKEN_LBRACE:
return parse_compound_statement();
default:
node = ast_new_expr_stmt(NULL, start_line, start_column);
ASTNode *expr_stmt_expr = parse_expression();
node->data.expr_stmt.expr = expr_stmt_expr;
expect(TOKEN_SEMICOLON);
consume();
return node;
}
}
// 解析变量声明:<declaration> ::= <type_specifier> <IDENTIFIER> [ "=" <expression> ] ";"
static ASTNode* parse_declaration() {
int start_line = current_token.line;
int start_column = current_token.column;
expect(TOKEN_INT); // 目前只支持 int 变量
TokenType type_specifier = current_token.type;
consume();
expect(TOKEN_IDENTIFIER);
char *var_name = strdup(current_token.lexeme);
// 语义检查:将变量添加到当前作用域的符号表
symtab_add_symbol(current_symtab, var_name, SYM_VAR, type_specifier);
consume();
ASTNode *initializer = NULL;
if (current_token.type == TOKEN_ASSIGN) {
consume();
initializer = parse_assignment_expression();
// 语义检查:如果存在初始化表达式,检查类型兼容性
// TODO: 更完善的类型检查,确保 initializer 的类型与 type_specifier 兼容
if (initializer && initializer->type == AST_INTEGER_LITERAL && type_specifier != TOKEN_INT) {
parser_error("Semantic Error: Type mismatch in initializer for '%s'. Expected int, got non-int literal.", var_name);
} else if (initializer && initializer->type == AST_IDENTIFIER) {
SymbolEntry *init_sym = symtab_lookup_symbol(current_symtab, initializer->data.identifier.name);
if (init_sym && init_sym->type != type_specifier) {
parser_error("Semantic Error: Type mismatch in initializer for '%s'. Expected '%s', but initializer has type '%s'.",
var_name, token_type_to_string(type_specifier), token_type_to_string(init_sym->type));
}
}
}
expect(TOKEN_SEMICOLON);
consume();
return ast_new_var_decl(type_specifier, var_name, initializer, start_line, start_column);
}
// --- 表达式解析函数(集成语义分析) ---
// 表达式入口:<expression> ::= <assignment_expression>
static ASTNode* parse_expression() {
ASTNode *expr = parse_assignment_expression();
// 表达式的类型推断可以在这里进行,并将类型信息附加到AST节点上
// 例如 expr->data.type_info = ...
return expr;
}
// 解析赋值表达式:<assignment_expression> ::= <equality_expression> [ "=" <assignment_expression> ]
static ASTNode* parse_assignment_expression() {
ASTNode *node = parse_equality_expression();
int start_line = current_token.line;
int start_column = current_token.column;
if (current_token.type == TOKEN_ASSIGN) {
consume();
ASTNode *right_expr = parse_assignment_expression();
// 语义检查:左侧必须是有效的左值 (L-value)
if (node->type != AST_IDENTIFIER) { // 目前只支持标识符作为左值
parser_error("Semantic Error: Invalid left-hand side in assignment: expected assignable identifier.");
}
// 语义检查:赋值操作的类型兼容性
// 获取左值的类型
SymbolEntry *left_sym = symtab_lookup_symbol(current_symtab, node->data.identifier.name);
if (!left_sym) {
parser_error("Semantic Error: Undeclared identifier '%s' on left-hand side of assignment.", node->data.identifier.name);
} else {
// TODO: 获取右值的类型(需要AST类型推断),并检查与 left_sym->type 的兼容性
// 简化:目前假设所有表达式和变量都是 int 类型
if (left_sym->type != TOKEN_INT) { // 假设只支持 int 类型赋值
parser_error("Semantic Error: Type mismatch in assignment for '%s'. Expected int, but left-hand side is type '%s'.",
node->data.identifier.name, token_type_to_string(left_sym->type));
}
// 此时,right_expr 的实际类型尚未推断,更复杂的检查需要 AST 遍历后进行
}
node = ast_new_assign_expr(node, right_expr, start_line, start_column);
}
return node;
}
// 解析相等/不等表达式:<equality_expression> ::= <relational_expression> { ("==" | "!=") <relational_expression> }
static ASTNode* parse_equality_expression() {
ASTNode *node = parse_relational_expression();
while (current_token.type == TOKEN_EQ || current_token.type == TOKEN_NE) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume();
ASTNode *right = parse_relational_expression();
// 语义检查:二元运算符操作数类型兼容性
// TODO: 这里需要 AST 节点类型推断,并检查左右操作数是否兼容(例如都是int)
// 简化:目前假设所有操作数都是int类型,并且结果是int类型
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析关系表达式:<relational_expression> ::= <additive_expression> { ("<" | "<=" | ">" | ">=") <additive_expression> }
static ASTNode* parse_relational_expression() {
ASTNode *node = parse_additive_expression();
while (current_token.type == TOKEN_LT ||
current_token.type == TOKEN_LE ||
current_token.type == TOKEN_GT ||
current_token.type == TOKEN_GE)
{
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume();
ASTNode *right = parse_additive_expression();
// 语义检查:同上,检查操作数类型兼容性
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析加法/减法表达式:<additive_expression> ::= <multiplicative_expression> { ("+" | "-") <multiplicative_expression> }
static ASTNode* parse_additive_expression() {
ASTNode *node = parse_multiplicative_expression();
while (current_token.type == TOKEN_PLUS || current_token.type == TOKEN_MINUS) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume();
ASTNode *right = parse_multiplicative_expression();
// 语义检查:同上,检查操作数类型兼容性
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析乘法/除法表达式:<multiplicative_expression> ::= <primary_expression> { ("*" | "/") <primary_expression> }
static ASTNode* parse_multiplicative_expression() {
ASTNode *node = parse_primary_expression();
while (current_token.type == TOKEN_ASTERISK || current_token.type == TOKEN_SLASH) {
int start_line = current_token.line;
int start_column = current_token.column;
TokenType op = current_token.type;
consume();
ASTNode *right = parse_primary_expression();
// 语义检查:同上,检查操作数类型兼容性
node = ast_new_binary_expr(op, node, right, start_line, start_column);
}
return node;
}
// 解析基本表达式:<primary_expression> ::= <INTEGER_LITERAL> | <IDENTIFIER> | "(" <expression> ")"
static ASTNode* parse_primary_expression() {
int start_line = current_token.line;
int start_column = current_token.column;
ASTNode *node = NULL;
switch (current_token.type) {
case TOKEN_INTEGER_LITERAL:
node = ast_new_integer_literal(current_token.int_value, start_line, start_column);
consume();
break;
case TOKEN_IDENTIFIER:
// 语义检查:查找标识符是否已声明
SymbolEntry *sym = symtab_lookup_symbol(current_symtab, current_token.lexeme);
if (!sym) {
parser_error("Semantic Error: Undeclared identifier '%s'.", current_token.lexeme);
}
// TODO: 如果是函数调用,需要解析参数并检查参数类型和数量
// TODO: 将查找到的符号信息(如类型)附加到 AST 节点上
node = ast_new_identifier(strdup(current_token.lexeme), start_line, start_column);
consume();
break;
case TOKEN_LPAREN:
consume();
node = parse_expression();
expect(TOKEN_RPAREN);
consume();
break;
default:
parser_error("Unexpected token in primary expression: '%s'", current_token.lexeme);
consume();
return NULL;
}
return node;
}
// --- AST 辅助函数实现 (不变) ---
// ... (ast_new_node 和所有 ast_new_xxx 函数,以及 ast_new_node_list 和 ast_add_to_list) ...
// 遍历和打印AST(用于调试和验证)
static void print_indent(int indent) {
for (int i = 0; i < indent; i++) {
printf(" ");
}
}
void ast_print(ASTNode *node, int indent) {
if (!node) return;
print_indent(indent);
printf("Type: %s ", ast_node_type_to_string(node->type)); // 修正为 ast_node_type_to_string
switch (node->type) {
case AST_PROGRAM:
printf("PROGRAM\n");
ASTNodeList *decls = node->data.program_decls;
while (decls) {
ast_print(decls->node, indent + 1);
decls = decls->next;
}
break;
case AST_FUNCTION_DECL:
printf("FUNCTION_DECL (Return: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.func_decl.return_type),
node->data.func_decl.name,
node->line, node->column);
print_indent(indent + 1); printf("Body:\n");
ast_print(node->data.func_decl.body, indent + 2);
break;
case AST_COMPOUND_STMT:
printf("COMPOUND_STMT at [%d:%d]\n", node->line, node->column);
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
ast_print(stmts->node, indent + 1);
stmts = stmts->next;
}
break;
case AST_VAR_DECL:
printf("VAR_DECL (Type: %s, Name: %s) at [%d:%d]\n",
token_type_to_string(node->data.var_decl.type_specifier),
node->data.var_decl.name,
node->line, node->column);
if (node->data.var_decl.initializer) {
print_indent(indent + 1); printf("Initializer:\n");
ast_print(node->data.var_decl.initializer, indent + 2);
}
break;
case AST_EXPR_STMT:
printf("EXPR_STMT at [%d:%d]\n", node->line, node->column);
ast_print(node->data.expr_stmt.expr, indent + 1);
break;
case AST_RETURN_STMT:
printf("RETURN_STMT at [%d:%d]\n", node->line, node->column);
if (node->data.return_stmt.expr) {
print_indent(indent + 1); printf("Expression:\n");
ast_print(node->data.return_stmt.expr, indent + 2);
}
break;
case AST_IF_STMT:
printf("IF_STMT at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Condition:\n");
ast_print(node->data.if_stmt.condition, indent + 2);
print_indent(indent + 1); printf("Then:\n");
ast_print(node->data.if_stmt.then_stmt, indent + 2);
if (node->data.if_stmt.else_stmt) {
print_indent(indent + 1); printf("Else:\n");
ast_print(node->data.if_stmt.else_stmt, indent + 2);
}
break;
case AST_BINARY_EXPR:
printf("BINARY_EXPR (Op: %s) at [%d:%d]\n",
token_type_to_string(node->data.binary_expr.op),
node->line, node->column);
print_indent(indent + 1); printf("Left:\n");
ast_print(node->data.binary_expr.left, indent + 2);
print_indent(indent + 1); printf("Right:\n");
ast_print(node->data.binary_expr.right, indent + 2);
break;
case AST_INTEGER_LITERAL:
printf("INTEGER_LITERAL (Value: %d) at [%d:%d]\n",
node->data.int_literal.value,
node->line, node->column);
break;
case AST_IDENTIFIER:
printf("IDENTIFIER (Name: %s) at [%d:%d]\n",
node->data.identifier.name,
node->line, node->column);
break;
case AST_ASSIGN_EXPR:
printf("ASSIGN_EXPR at [%d:%d]\n", node->line, node->column);
print_indent(indent + 1); printf("Left (L-Value):\n");
ast_print(node->data.assign_expr.left, indent + 2);
print_indent(indent + 1); printf("Right (R-Value):\n");
ast_print(node->data.assign_expr.right, indent + 2);
break;
default:
printf("UNKNOWN_AST_NODE_TYPE (Type: %d) at [%d:%d]\n",
node->type, node->line, node->column);
break;
}
}
// 释放AST节点及其子节点占用的内存 (不变)
// ... (ast_free 函数不变) ...
// --- 语法分析器入口点 ---
ASTNode* parse(const char *source_code_path) {
// 1. 初始化词法分析器
if (init_lexer(source_code_path) != 0) {
fprintf(stderr, "Error: Failed to initialize lexer.\n");
return NULL;
}
// 2. 初始化全局符号表并进入第一个作用域
global_symtab = symtab_init(128); // 初始容量128
current_symtab = global_symtab;
// TODO: 向全局符号表添加标准库函数(如 printf)的声明
// symtab_add_symbol(global_symtab, "printf", SYM_FUNC, TOKEN_INT); // 简化,实际应为复杂类型
// 3. 获取第一个Token,开始解析
current_token = get_next_token();
// 4. 开始解析整个程序
ASTNode *program_ast = parse_program();
// 5. 清理词法分析器资源
close_lexer();
// 6. 打印全局符号表 (用于调试)
printf("\n--- Global Symbol Table ---\n");
symtab_print(global_symtab, 0);
// 7. 释放符号表资源
symtab_free(global_symtab);
return program_ast;
}
// --- 主函数,用于测试语法分析器 ---
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
printf("--- Syntax & Semantic Analysis Started ---\n");
ASTNode *program_ast = parse(argv[1]);
if (program_ast) {
printf("\n--- Abstract Syntax Tree (AST) ---\n");
ast_print(program_ast, 0);
printf("\n--- Syntax & Semantic Analysis Complete ---\n");
ast_free(program_ast);
printf("AST memory freed.\n");
} else {
printf("\n--- Syntax & Semantic Analysis Failed ---\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `parser.c` 在这次更新中融合了语义分析的核心逻辑,主要体现在对符号表的管理和使用上。
*
* **1. 全局符号表 (`global_symtab`) 和当前符号表 (`current_symtab`):**
* - 这两个全局指针是实现作用域管理的关键。
* - `global_symtab` 始终指向整个程序的顶层作用域。
* - `current_symtab` 则随着解析过程深入不同的代码块(函数体、`if` 语句块),动态地指向当前活跃的作用域。
*
* **2. `parse()` 函数的修改:**
* - **初始化符号表:** 在解析开始前,`parse()` 函数现在会调用 `symtab_init()` 创建 `global_symtab`,并将其设置为初始的 `current_symtab`。
* - **清理符号表:** 在解析结束后,会调用 `symtab_free()` 释放整个符号表结构占用的内存。
* - **调试输出:** 增加了 `symtab_print()` 的调用,以便在解析完成后打印出构建的全局符号表,方便调试。
*
* **3. 作用域管理:**
* - **函数作用域:** 在 `parse_function_decl()` 函数中,当解析到函数体之前,会调用 `current_symtab = symtab_enter_scope(current_symtab);`。这会创建一个新的符号表层级,并将其父指针指向函数外部的作用域。函数解析结束后,调用 `symtab_exit_scope()` 回到父作用域。
* - **块作用域:** 在 `parse_compound_statement()` 函数中,同样会处理块作用域的进入和退出。
* 每次遇到 `{` 都会调用 `symtab_enter_scope()`,并在遇到 `}` 时调用 `symtab_exit_scope()`。
* 注意:这里有一个简化处理,即如果当前作用域已经是函数作用域(level > 0),
* 那么函数体的第一个 `{` 也会创建一个新的作用域,这在语义上是正确的。
*
* **4. 符号添加 (`symtab_add_symbol`):**
* - **变量声明:** 在 `parse_declaration()` 函数中,当解析到 `int my_var;` 这样的变量声明时,会调用 `symtab_add_symbol(current_symtab, var_name, SYM_VAR, type_specifier);` 将新声明的变量添加到当前作用域的符号表中。
* - **函数声明:** 在 `parse_function_decl()` 中,会将函数本身(作为 `SYM_FUNC` 类型)添加到其所在(通常是全局)作用域的符号表中。
* 注意:`symtab_add_symbol` 内部已经包含了重复定义检查。
*
* **5. 符号查找 (`symtab_lookup_symbol`):**
* - **标识符使用:** 在 `parse_primary_expression()` 中,当识别到 `TOKEN_IDENTIFIER` 时,会立即调用 `symtab_lookup_symbol(current_symtab, current_token.lexeme);` 来查找该标识符是否已声明。
* 如果 `symtab_lookup_symbol` 返回 `NULL`,则表示该标识符未声明,此时会通过 `parser_error` 报告语义错误。
*
* **6. 基本类型检查:**
* - **赋值语句:** 在 `parse_assignment_expression()` 中,当处理 `=` 赋值操作时,会获取左侧标识符的类型(通过 `symtab_lookup_symbol` )。
* 这里我们进行了一个非常简化的类型检查:只检查左值是否为 `int` 类型。
* 实际编译器需要获取左右操作数的推断类型,并根据C语言的类型转换规则进行兼容性检查。
* - **条件表达式:** 在 `parse_statement()` 处理 `if` 语句时,对条件表达式进行了初步的类型检查,确保其类型是 `int`(或可转换为布尔值)。
* - **变量初始化:** 在 `parse_declaration()` 中,如果变量有初始化表达式,会进行初步的类型兼容性检查。
*
* **7. 待完善的语义分析:**
* - **完整的类型系统:** 目前只支持 `int` 类型。C语言有复杂的类型系统(`char`, `float`, `double`, `short`, `long`, `struct`, `union`, 指针,数组等),需要更精细的 `TokenType` 定义和更复杂的类型推断及兼容性检查逻辑。
* - **函数参数和返回值检查:** 目前函数定义和调用都没有处理参数列表。需要将参数类型和数量信息存储到函数符号条目中,并在函数调用时进行匹配检查。返回值也需要检查兼容性。
* - **左值/右值判断:** 赋值操作的左侧必须是左值(可以被赋值的表达式)。目前只简单检查了是否是标识符。
* - **运算符重载(C++):** C语言没有运算符重载,但表达式的类型推断会影响结果类型(例如 `int + float = float`)。
* - **常量表达式求值:** 在编译时就能计算出结果的表达式(如 `10 + 20`)可以在语义分析或中间代码生成阶段进行求值,以优化代码。
* - **控制流分析:** 检查 `break`, `continue` 语句是否在循环或 `switch` 语句内部,`return` 语句是否在函数内部。
* - **数据流分析:** 检查变量是否在使用前被初始化。
* - **AST 注释/转换:** 语义分析的结果(如推断类型、符号表条目引用)通常会直接附加到AST节点上,以便后续阶段可以直接使用。
* - **错误恢复:** 目前语义错误也是立即退出,实际编译器会尝试恢复,以报告更多错误。
*
* 这一阶段的编译器,已经能够理解程序中名字的含义和作用域了,并且能做一些基本的“智力判断”了。这是向真正可用的编译器迈出的关键一步!
*/
4. test.c
:测试文件(用于测试语义分析)
// 文件名: test.c
// 用于测试自定义C语言词法、语法和初步语义分析器的示例源代码。
int main() {
int my_var_1 = 10; // 声明并初始化变量
int anotherVar; // 声明变量,未初始化
// 合法的赋值和算术运算
anotherVar = 20;
int sum = my_var_1 + anotherVar * 2;
// 语义错误:尝试使用未声明的变量 'undeclaredVar'
// int x = undeclaredVar; // 这一行会触发语义错误
// 语义错误:类型不兼容的赋值 (如果支持 float 会更明显)
// int invalid_assign = "hello"; // 假设这里会是语法/语义错误
// 合法的条件语句
if (sum >= 40) {
int result = 1; // 局部变量
return result;
} else {
int local_var = 5; // 嵌套作用域中的局部变量
// 语义错误:尝试访问外部作用域的局部变量 'result'
// local_var = result; // 这一行在 else 块中会触发语义错误,因为 result 在 if 块作用域
if (my_var_1 < local_var) { // 合法使用 local_var
return 100;
} else {
return 0;
}
}
// 语义错误:函数定义中重定义变量 'my_var_1'
// int my_var_1 = 30; // 这一行会触发语义错误
return 2;
}
// 语义错误:重定义函数 'main'
// int main() { return 5; } // 这一行会触发语义错误
编译和运行
-
确保所有文件 (
token.h
,lexer.h
,lexer.c
,ast.h
,parser.h
,parser.c
,symbol_table.h
,symbol_table.c
,test.c
) 都在同一个目录下。 -
打开终端。
-
使用GCC编译(现在需要同时编译
lexer.c
,parser.c
,symbol_table.c
):gcc -o my_compiler lexer.c parser.c symbol_table.c -g -Wall
-
运行:
./my_compiler test.c
你将看到程序执行过程中的输出,包括词法分析和语法分析的结果,最重要的是,如果 test.c
中包含了我们预设的语义错误(例如 int x = undeclaredVar;
),你的编译器将会在相应位置报告这些错误,并终止编译过程。同时,你也能看到构建好的符号表内容,这有助于理解作用域管理。
总结与展望
在这一部分,我们为我们的C语言编译器增添了至关重要的“理解力”——语义分析器。我们深入学习了符号表的概念和实现,它是编译器跟踪程序中所有标识符信息的“活字典”,能够管理不同作用域(全局、函数、代码块)中的变量和函数。通过将符号表集成到递归下降解析器中,我们实现了:
-
作用域管理: 在进入和退出代码块时,动态地创建和销毁符号表层级,模拟C语言的词法作用域规则。
-
声明检查: 在变量和函数声明时,将它们添加到当前作用域的符号表中,并检查是否存在重复定义。
-
使用检查: 在标识符被使用时(例如在表达式中),在作用域链中查找该标识符,如果未找到则报告“未声明”错误。
-
基本类型检查: 对赋值操作和条件表达式进行了初步的类型兼容性检查,确保操作的合法性。
你亲手编写的这些代码,让编译器从简单的“看结构”进化到了“理解意义”,能够捕捉到程序中更深层次的逻辑错误。这是一个里程碑式的进步!
然而,目前的语义分析仍然是一个简化版本。完整的C语言语义分析还需要处理:
-
复杂的类型系统: 指针、数组、结构体、联合体、枚举、类型转换等。这需要更精细的类型表示和更强大的类型推断算法。
-
函数参数和返回值匹配: 在函数调用时,检查实际参数的数量和类型是否与函数声明匹配。
-
更深入的错误恢复: 在遇到语义错误时,尝试跳过问题区域并继续分析,以便报告更多错误。
-
AST的类型批注: 将推断出的类型信息直接附加到AST节点上,以便后续的中间代码生成和优化阶段可以直接利用。
在下一个,也是本系列文章的最后一个部分,我们将迎来编译器的“收官之战”——中间代码生成和代码生成。我们将把语义检查后的AST转换为机器无关的中间代码,并最终将其翻译成目标机器(如x86-64)的汇编指令。这将是C语言代码从高级抽象到底层执行的最终“蜕变”,我们将真正“看透看破C语言红尘”!让我们拭目以待!
---------------------------------------------------------------------------------------------------------------------------------更新于2025年6月27号 下午5:31
(6)掀开C语言的底裤:从编译器原理到手撸实践,看透C语言的红尘!
手撸编译器:终极挑战——中间代码生成与目标代码生成
各位勇敢的探索者,恭喜你坚持到了C语言编译器手撸系列的最终章!在前面的篇章中,我们已经成功搭建了编译器的“前端”:让它学会了“看”(词法分析),学会了“理解结构”(语法分析),也学会了“判断意义”(语义分析)。现在,我们的编译器拥有了一个语义正确的、带有类型和作用域信息的抽象语法树(AST),这就像一张详细的程序“蓝图”。
然而,这张蓝图还不能直接交给CPU去执行。CPU只认识那些冰冷冷的二进制指令——机器码。因此,我们面临着编译器的“终极挑战”:如何将这张抽象的蓝图,一步步转化为具体、可执行的机器指令?这个过程通常分为两个主要阶段:中间代码生成(Intermediate Code Generation)和目标代码生成(Target Code Generation)。
为什么需要中间代码?——解耦与优化
你可能会问,为什么不直接从AST生成汇编代码或机器码呢?为什么要引入一个“中间”的步骤?这正是现代编译器设计的精髓之一。
引入**中间代码(Intermediate Representation, IR)**的主要原因有:
-
解耦前端与后端: 中间代码作为前端(词法、语法、语义分析)和后端(代码优化、目标代码生成)之间的桥梁,将编译器分成了相对独立的两个部分。前端负责将各种高级语言(C, C++, Java, Python等)翻译成同一种中间代码,而后端则负责将这种中间代码翻译成各种目标机器(x86, ARM, MIPS等)的机器码。这极大地提高了编译器的可移植性和可重用性。例如,LLVM的IR就能作为多种语言的通用后端 [1]。
-
便于实现代码优化: 在中间代码层面进行优化比直接在AST上或最终的机器码上进行优化要容易得多。中间代码通常比源代码更接近机器语言,但又比机器语言更抽象,这使得在上面应用各种优化算法(如常量传播、死代码消除、循环优化等)更为方便和高效。
-
简化目标代码生成: 中间代码通常是一种“三地址码”(Three-Address Code, TAC)的形式,它将复杂的表达式分解为一系列简单的、原子性的操作,每个操作最多涉及三个地址(两个操作数和一个结果)。这种形式非常接近汇编指令,使得从IR到最终汇编代码的转换变得直观和简单。
三地址码 (Three-Address Code, TAC):编译器的“通用语”
三地址码是编译器中最常用的一种中间表示形式。它的每一条指令都像一个微小的算术运算或数据传输,通常包含一个操作码(Operation Code)和最多三个地址(操作数或结果的存储位置)。
常见的TAC指令形式:
-
result = operand1 op operand2
(二元运算) -
result = op operand1
(一元运算) -
result = operand1
(赋值/拷贝) -
jump target
(无条件跳转) -
if condition jump target
(条件跳转) -
call function, args
(函数调用) -
return value
(函数返回)
我们将从AST生成一种简单的三地址码形式。
手撸中间代码生成器:C语言实现
我们将定义TAC指令的结构,并实现一个遍历AST的函数,根据AST节点类型生成对应的TAC指令序列。
1. ir.h
:中间表示(IR)的定义
// 文件名: ir.h
// 描述: 定义编译器中间表示(IR),即三地址码(Three-Address Code, TAC)的结构。
// TAC是一种抽象的、机器无关的表示形式,是连接编译器前端和后端的桥梁。
#ifndef IR_H
#define IR_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ast.h" // 包含AST节点定义,因为IR是根据AST生成的
#include "token.h" // 包含Token类型,用于操作码
// 定义中间代码的操作码(IR Instruction Opcode)
typedef enum {
IR_ADD, // 加法: res = op1 + op2
IR_SUB, // 减法: res = op1 - op2
IR_MUL, // 乘法: res = op1 * op2
IR_DIV, // 除法: res = op1 / op2
IR_ASSIGN, // 赋值: res = op1
IR_EQ, // 等于: res = op1 == op2
IR_NE, // 不等于: res = op1 != op2
IR_LT, // 小于: res = op1 < op2
IR_LE, // 小于等于: res = op1 <= op2
IR_GT, // 大于: res = op1 > op2
IR_GE, // 大于等于: res = op1 >= op2
IR_JUMP, // 无条件跳转: JUMP label
IR_JUMPF, // 条件假跳转: JUMPF condition, label (如果condition为假则跳转)
IR_CALL, // 函数调用: CALL func_name, num_args, res (结果保存在res)
IR_RETURN, // 函数返回: RETURN value
IR_LOAD_IMM, // 加载立即数: res = value
IR_LOAD_VAR, // 从变量加载值: res = var_name (将变量值加载到临时变量)
IR_STORE_VAR, // 存储值到变量: var_name = res (将临时变量值存回变量)
IR_LABEL, // 标签定义: LABEL name
IR_ALLOC_VAR, // 变量分配(栈上):ALLOC var_name, size (用于局部变量在栈上分配空间)
// 更多IR指令待扩展...
} IROpcode;
// 定义中间代码操作数类型
typedef enum {
OPERAND_TYPE_NONE, // 无操作数
OPERAND_TYPE_TEMP, // 临时变量 (t0, t1, ...)
OPERAND_TYPE_VAR, // 真实变量名
OPERAND_TYPE_INT_LIT, // 整数立即数
OPERAND_TYPE_LABEL, // 标签名
OPERAND_TYPE_FUNC_NAME, // 函数名
} IROperandType;
// 定义中间代码操作数结构体
typedef struct {
IROperandType type; // 操作数类型
union {
int temp_id; // 临时变量ID (例如 t0 的 id 为 0)
char *var_name; // 变量名字符串
int int_value; // 整数立即数
char *label_name; // 标签名
char *func_name; // 函数名
} val;
} IROperand;
// 定义三地址码指令结构体
typedef struct IRInstruction {
IROpcode opcode; // 操作码
IROperand result; // 结果 (Result)
IROperand op1; // 操作数1 (Operand1)
IROperand op2; // 操作数2 (Operand2)
// 其他属性,例如:
// int line, column; // 原始源代码行/列信息 (用于调试)
struct IRInstruction *next; // 指向下一条指令(链表形式)
} IRInstruction;
// --- 接口函数声明 ---
// 创建新的IR操作数
IROperand ir_new_operand_none();
IROperand ir_new_operand_temp(int temp_id);
IROperand ir_new_operand_var(char *var_name);
IROperand ir_new_operand_int_lit(int value);
IROperand ir_new_operand_label(char *label_name);
IROperand ir_new_operand_func_name(char *func_name);
// 创建新的IR指令
IRInstruction* ir_new_instruction(IROpcode opcode, IROperand result, IROperand op1, IROperand op2);
// 添加指令到IR序列链表
void ir_add_instruction(IRInstruction **head, IRInstruction *instr);
// 获取下一个可用的临时变量ID
int ir_next_temp_id();
// 获取下一个可用的标签ID
int ir_next_label_id();
// 将IR操作码转换为可读字符串 (用于调试)
const char* ir_opcode_to_string(IROpcode opcode);
// 将IR操作数转换为可读字符串 (用于调试)
void ir_operand_print(IROperand op);
// 打印IR指令序列 (用于调试)
void ir_print_sequence(IRInstruction *head);
// 释放IR指令序列占用的内存
void ir_free_sequence(IRInstruction *head);
#endif // IR_H
/*
* 逻辑分析:
* `ir.h` 定义了我们中间代码的“语言”。
*
* 1. `IROpcode` 枚举:
* - 定义了所有支持的三地址码操作类型,例如加减乘除、赋值、跳转、函数调用等。
* - 每个操作码代表一个原子操作,简化了代码生成器的复杂性。
*
* 2. `IROperandType` 和 `IROperand` 结构体:
* - 定义了操作数的类型(临时变量、真实变量、立即数、标签、函数名等)。
* - `IROperand` 结构体使用 `union` 来存储不同类型操作数的实际值,
* 例如 `temp_id` 用于临时变量,`var_name` 用于真实变量名。
* 这种设计节省了内存,并提供了灵活性。
*
* 3. `IRInstruction` 结构体:
* - 这是三地址码的核心,每条指令包含一个操作码和最多三个操作数(结果、操作数1、操作数2)。
* - `next` 指针用于将指令链接成一个序列(链表),表示程序的执行流程。
*
* 4. 辅助函数:
* - `ir_new_operand_xxx()` 和 `ir_new_instruction()`:方便创建操作数和指令。
* 它们负责内存分配和数据初始化。
* - `ir_add_instruction()`:将新生成的指令添加到IR序列的末尾。
* - `ir_next_temp_id()` 和 `ir_next_label_id()`:用于生成唯一的临时变量名和标签名。
* 这是因为IR中常常需要引入临时的计算结果和跳转目标。
* - `ir_opcode_to_string()`, `ir_operand_print()`, `ir_print_sequence()`:用于调试和可视化生成的IR。
* - `ir_free_sequence()`:负责释放IR序列占用的内存。
*
* 设计考量:
* - **抽象性:** IR指令不包含任何与具体机器架构相关的细节(如寄存器名称、具体指令编码)。
* - **原子性:** 每条指令只执行一个简单操作,便于优化和目标代码生成。
* - **链表结构:** IR指令通常以链表形式组织,便于遍历和修改(优化)。
* - **临时变量:** 引入临时变量 (`t0`, `t1` 等) 来存储中间计算结果,
* 这避免了直接操作复杂表达式,并将计算分解为一系列简单步骤。
*/
2. ir.c
:中间代码(IR)的实现
// 文件名: ir.c
// 描述: 中间表示(IR)操作的实现,包括操作数的创建、指令的创建、
// 指令序列的管理以及调试打印功能。
#include <stdlib.h>
#include <string.h>
#include <stdio.h> // for sprintf, printf
#include "ir.h"
// 内部静态变量,用于生成唯一的临时变量ID和标签ID
static int temp_counter = 0;
static int label_counter = 0;
// --- 接口函数实现 ---
// 创建新的IR操作数
IROperand ir_new_operand_none() {
IROperand op;
op.type = OPERAND_TYPE_NONE;
memset(&op.val, 0, sizeof(op.val)); // 清零联合体
return op;
}
IROperand ir_new_operand_temp(int temp_id) {
IROperand op;
op.type = OPERAND_TYPE_TEMP;
op.val.temp_id = temp_id;
return op;
}
IROperand ir_new_operand_var(char *var_name) {
IROperand op;
op.type = OPERAND_TYPE_VAR;
op.val.var_name = strdup(var_name); // 复制变量名,确保内存独立性
if (!op.val.var_name) {
fprintf(stderr, "Fatal Error: Failed to strdup var_name in ir_new_operand_var.\n");
exit(EXIT_FAILURE);
}
return op;
}
IROperand ir_new_operand_int_lit(int value) {
IROperand op;
op.type = OPERAND_TYPE_INT_LIT;
op.val.int_value = value;
return op;
}
IROperand ir_new_operand_label(char *label_name) {
IROperand op;
op.type = OPERAND_TYPE_LABEL;
op.val.label_name = strdup(label_name); // 复制标签名
if (!op.val.label_name) {
fprintf(stderr, "Fatal Error: Failed to strdup label_name in ir_new_operand_label.\n");
exit(EXIT_FAILURE);
}
return op;
}
IROperand ir_new_operand_func_name(char *func_name) {
IROperand op;
op.type = OPERAND_TYPE_FUNC_NAME;
op.val.func_name = strdup(func_name); // 复制函数名
if (!op.val.func_name) {
fprintf(stderr, "Fatal Error: Failed to strdup func_name in ir_new_operand_func_name.\n");
exit(EXIT_FAILURE);
}
return op;
}
// 创建新的IR指令
IRInstruction* ir_new_instruction(IROpcode opcode, IROperand result, IROperand op1, IROperand op2) {
IRInstruction *instr = (IRInstruction*)malloc(sizeof(IRInstruction));
if (!instr) {
fprintf(stderr, "Fatal Error: Failed to allocate IRInstruction.\n");
exit(EXIT_FAILURE);
}
instr->opcode = opcode;
instr->result = result;
instr->op1 = op1;
instr->op2 = op2;
instr->next = NULL;
return instr;
}
// 添加指令到IR序列链表
void ir_add_instruction(IRInstruction **head, IRInstruction *instr) {
if (!instr) return;
if (*head == NULL) {
*head = instr;
} else {
IRInstruction *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = instr;
}
}
// 获取下一个可用的临时变量ID
int ir_next_temp_id() {
return temp_counter++;
}
// 获取下一个可用的标签ID
int ir_next_label_id() {
return label_counter++;
}
// 将IR操作码转换为可读字符串 (用于调试)
const char* ir_opcode_to_string(IROpcode opcode) {
switch (opcode) {
case IR_ADD: return "ADD";
case IR_SUB: return "SUB";
case IR_MUL: return "MUL";
case IR_DIV: return "DIV";
case IR_ASSIGN: return "ASSIGN";
case IR_EQ: return "EQ";
case IR_NE: return "NE";
case IR_LT: return "LT";
case IR_LE: return "LE";
case IR_GT: return "GT";
case IR_GE: return "GE";
case IR_JUMP: return "JUMP";
case IR_JUMPF: return "JUMPF";
case IR_CALL: return "CALL";
case IR_RETURN: return "RETURN";
case IR_LOAD_IMM: return "LOAD_IMM";
case IR_LOAD_VAR: return "LOAD_VAR";
case IR_STORE_VAR: return "STORE_VAR";
case IR_LABEL: return "LABEL";
case IR_ALLOC_VAR: return "ALLOC_VAR";
default: return "UNKNOWN_IR_OPCODE";
}
}
// 将IR操作数转换为可读字符串 (用于调试)
void ir_operand_print(IROperand op) {
switch (op.type) {
case OPERAND_TYPE_NONE:
printf("");
break;
case OPERAND_TYPE_TEMP:
printf("t%d", op.val.temp_id);
break;
case OPERAND_TYPE_VAR:
printf("%s", op.val.var_name);
break;
case OPERAND_TYPE_INT_LIT:
printf("%d", op.val.int_value);
break;
case OPERAND_TYPE_LABEL:
printf("L%s", op.val.label_name);
break;
case OPERAND_TYPE_FUNC_NAME:
printf("%s", op.val.func_name);
break;
default:
printf("UNKNOWN_OPERAND_TYPE");
break;
}
}
// 打印IR指令序列 (用于调试)
void ir_print_sequence(IRInstruction *head) {
IRInstruction *current = head;
int instr_num = 0;
while (current) {
printf("%4d: ", instr_num++);
printf("%-10s ", ir_opcode_to_string(current->opcode));
// 根据指令类型格式化打印
switch (current->opcode) {
case IR_ADD:
case IR_SUB:
case IR_MUL:
case IR_DIV:
case IR_EQ:
case IR_NE:
case IR_LT:
case IR_LE:
case IR_GT:
case IR_GE:
ir_operand_print(current->result);
printf(" = ");
ir_operand_print(current->op1);
printf(" %s ", token_type_to_string(current->opcode)); // TODO: IR_ADD/SUB/MUL/DIV 应该映射到 Token 运算符
// 或者直接打印符号如 "+" "-"
ir_operand_print(current->op2);
break;
case IR_ASSIGN:
ir_operand_print(current->result);
printf(" = ");
ir_operand_print(current->op1);
break;
case IR_JUMP:
printf("L%s", current->result.val.label_name); // JUMP target
break;
case IR_JUMPF:
printf("IF ");
ir_operand_print(current->op1);
printf(" GOTO L%s", current->result.val.label_name); // JUMPF condition, target
break;
case IR_CALL:
if (current->result.type != OPERAND_TYPE_NONE) {
ir_operand_print(current->result);
printf(" = ");
}
printf("CALL ");
ir_operand_print(current->op1); // 函数名
printf(", %d args", current->op2.val.int_value); // 参数数量
break;
case IR_RETURN:
printf(" ");
ir_operand_print(current->op1); // 返回值
break;
case IR_LOAD_IMM:
ir_operand_print(current->result);
printf(" = ");
ir_operand_print(current->op1);
break;
case IR_LOAD_VAR:
ir_operand_print(current->result);
printf(" = *");
ir_operand_print(current->op1);
break;
case IR_STORE_VAR:
printf("*");
ir_operand_print(current->result);
printf(" = ");
ir_operand_print(current->op1);
break;
case IR_LABEL:
printf("L%s:", current->result.val.label_name);
break;
case IR_ALLOC_VAR:
printf("%s, SIZE %d", current->result.val.var_name, current->op1.val.int_value); // 变量名,大小(简化为1)
break;
default:
printf("UNKNOWN IR INSTRUCTION");
break;
}
printf("\n");
current = current->next;
}
}
// 释放IR指令序列占用的内存
void ir_free_sequence(IRInstruction *head) {
IRInstruction *current = head;
while (current) {
IRInstruction *temp = current;
current = current->next;
// 释放操作数中 strdup 的字符串
if (temp->result.type == OPERAND_TYPE_VAR) free(temp->result.val.var_name);
if (temp->result.type == OPERAND_TYPE_LABEL) free(temp->result.val.label_name);
if (temp->result.type == OPERAND_TYPE_FUNC_NAME) free(temp->result.val.func_name);
if (temp->op1.type == OPERAND_TYPE_VAR) free(temp->op1.val.var_name);
if (temp->op1.type == OPERAND_TYPE_LABEL) free(temp->op1.val.label_name);
if (temp->op1.type == OPERAND_TYPE_FUNC_NAME) free(temp->op1.val.func_name);
if (temp->op2.type == OPERAND_TYPE_VAR) free(temp->op2.val.var_name);
if (temp->op2.type == OPERAND_TYPE_LABEL) free(temp->op2.val.label_name);
if (temp->op2.type == OPERAND_TYPE_FUNC_NAME) free(temp->op2.val.func_name);
free(temp); // 释放指令本身
}
// 重置计数器,以便下次编译可以从0开始
temp_counter = 0;
label_counter = 0;
}
/*
* 逻辑分析:
* `ir.c` 实现了IR的各种操作,是IR生成和管理的基础。
*
* **1. 临时变量和标签计数器:**
* - `temp_counter` 和 `label_counter` 确保每次生成新的临时变量 `tN` 或标签 `LN` 时,它们都是唯一的。
* - 在 `ir_free_sequence` 中重置它们,确保每次编译都是从干净的状态开始。
*
* **2. `ir_new_operand_xxx()` 系列函数:**
* - 这些函数负责创建 `IROperand` 结构体,并正确设置其类型和值。
* - **重要:** 对于 `OPERAND_TYPE_VAR`, `OPERAND_TYPE_LABEL`, `OPERAND_TYPE_FUNC_NAME`,
* 它们会使用 `strdup()` 来复制传入的字符串。
* 这是为了防止原始字符串的内存被释放或修改,确保 `IROperand` 拥有自己独立的字符串副本。
* 这意味着在释放 `IRInstruction` 时,需要负责 `free()` 掉这些复制的字符串。
*
* **3. `ir_new_instruction()`:**
* - 用于创建 `IRInstruction` 结构体,设置操作码和操作数。
*
* **4. `ir_add_instruction()`:**
* - 将新生成的IR指令添加到IR指令链表的末尾。
* 这种简单的链表添加方式在指令数量不大时是可接受的,但对于大型程序,可能需要更高效的结构(例如数组和动态扩容)。
*
* **5. 调试和可视化:**
* - `ir_opcode_to_string()` 和 `ir_operand_print()`:将枚举值和操作数转换为可读的字符串,
* 这对于调试IR生成是否正确至关重要。
* - `ir_print_sequence()`:遍历并以格式化的方式打印整个IR指令序列。
* 这能让我们直观地看到AST是如何被转换为低级IR的。
*
* **6. 内存管理 (`ir_free_sequence`):**
* - 这是一个递归释放函数,遍历整个IR指令链表。
* - 对于每个指令,它会检查其操作数中是否有 `strdup()` 复制的字符串(例如变量名、标签名),并 `free()` 它们。
* - 最后释放指令本身。
* - **这是避免内存泄漏的关键!**
*
* 设计考量:
* - **数据独立性:** 通过 `strdup` 复制字符串,确保IR指令的操作数不会受到原始AST或Token内存释放的影响。
* - **调试可见性:** 提供了丰富的打印函数,便于验证IR的正确性。
* - **资源管理:** 提供了完整的内存释放机制。
*/
3. 增强 parser.c
以生成IR
我们将修改 parser.c
中的解析函数,让它们在构建AST的同时,也生成对应的IR指令。这个过程是一个典型的AST遍历。
修改 parser.c
我们将添加一个 generate_ir_from_ast
函数,它将递归遍历AST并生成IR指令。
// 文件名: parser.c (更新,主要新增IR生成逻辑)
// 描述: 语法分析器(Parser)和初步语义分析器的核心实现。
// 在这次更新中,它还将负责遍历AST并生成三地址码(IR)。
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <stdbool.h>
#include "token.h"
#include "lexer.h"
#include "ast.h"
#include "parser.h"
#include "symbol_table.h"
#include "ir.h" // 新增: 包含IR头文件
// --- 全局变量定义 (不变) ---
SymbolTable *global_symtab = NULL;
SymbolTable *current_symtab = NULL;
// 全局IR指令序列的头指针
IRInstruction *ir_head = NULL;
// 将ASTNodeType转换为可读字符串 (不变)
const char* ast_node_type_to_string(ASTNodeType type) {
switch (type) {
case AST_PROGRAM: return "AST_PROGRAM";
case AST_FUNCTION_DECL: return "AST_FUNCTION_DECL";
case AST_COMPOUND_STMT: return "AST_COMPOUND_STMT";
case AST_VAR_DECL: return "AST_VAR_DECL";
case AST_EXPR_STMT: return "AST_EXPR_STMT";
case AST_RETURN_STMT: return "AST_RETURN_STMT";
case AST_IF_STMT: return "AST_IF_STMT";
case AST_WHILE_STMT: return "AST_WHILE_STMT";
case AST_FOR_STMT: return "AST_FOR_STMT";
case AST_BINARY_EXPR: return "AST_BINARY_EXPR";
case AST_UNARY_EXPR: return "AST_UNARY_EXPR";
case AST_INTEGER_LITERAL: return "AST_INTEGER_LITERAL";
case AST_IDENTIFIER: return "AST_IDENTIFIER";
case AST_ASSIGN_EXPR: return "AST_ASSIGN_EXPR";
case AST_FUNCTION_CALL: return "AST_FUNCTION_CALL";
case AST_STRING_LITERAL: return "AST_STRING_LITERAL";
case AST_CHAR_LITERAL: return "AST_CHAR_LITERAL";
default: return "UNKNOWN_AST_NODE_TYPE";
}
}
// --- 辅助函数:Token 操作 (不变) ---
void parser_error(const char *format, ...);
static void expect(TokenType type);
static void consume();
static int match_and_consume(TokenType type);
// --- 递归下降解析函数声明 (不变) ---
static ASTNode* parse_program();
static ASTNode* parse_function_decl();
static ASTNode* parse_compound_statement();
static ASTNode* parse_statement();
static ASTNode* parse_declaration();
static ASTNode* parse_expression();
static ASTNode* parse_assignment_expression();
static ASTNode* parse_equality_expression();
static ASTNode* parse_relational_expression();
static ASTNode* parse_additive_expression();
static ASTNode* parse_multiplicative_expression();
static ASTNode* parse_primary_expression();
// --- AST 辅助函数实现 (不变) ---
ASTNode* ast_new_node(ASTNodeType type, int line, int column);
ASTNode* ast_new_program(ASTNodeList *decls);
ASTNode* ast_new_function_decl(TokenType return_type, char *name, ASTNode *body, int line, int column);
ASTNode* ast_new_compound_stmt(ASTNodeList *statements, int line, int column);
ASTNode* ast_new_var_decl(TokenType type_specifier, char *name, ASTNode *initializer, int line, int column);
ASTNode* ast_new_expr_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_return_stmt(ASTNode *expr, int line, int column);
ASTNode* ast_new_if_stmt(ASTNode *condition, ASTNode *then_stmt, ASTNode *else_stmt, int line, int column);
ASTNode* ast_new_binary_expr(TokenType op, ASTNode *left, ASTNode *right, int line, int column);
ASTNode* ast_new_integer_literal(int value, int line, int column);
ASTNode* ast_new_identifier(char *name, int line, int column);
ASTNode* ast_new_assign_expr(ASTNode *left, ASTNode *right, int line, int column);
ASTNodeList* ast_new_node_list(ASTNode *node);
void ast_add_to_list(ASTNodeList **head, ASTNode *node);
static void print_indent(int indent);
void ast_print(ASTNode *node, int indent);
void ast_free(ASTNode *node);
// --- IR 生成辅助函数 ---
// 用于将Token操作符转换为IR操作码
static IROpcode token_op_to_ir_op(TokenType op) {
switch (op) {
case TOKEN_PLUS: return IR_ADD;
case TOKEN_MINUS: return IR_SUB;
case TOKEN_ASTERISK: return IR_MUL;
case TOKEN_SLASH: return IR_DIV;
case TOKEN_ASSIGN: return IR_ASSIGN;
case TOKEN_EQ: return IR_EQ;
case TOKEN_NE: return IR_NE;
case TOKEN_LT: return IR_LT;
case TOKEN_LE: return IR_LE;
case TOKEN_GT: return IR_GT;
case TOKEN_GE: return IR_GE;
default:
parser_error("Unsupported operator for IR generation: %s", token_type_to_string(op));
return (IROpcode)-1; // Should not reach here
}
}
// 核心IR生成函数:递归遍历AST,生成IR指令
// 返回一个IROperand,代表该AST节点计算出的结果(通常是一个临时变量)
static IROperand generate_ir_from_ast(ASTNode *node) {
if (!node) return ir_new_operand_none();
switch (node->type) {
case AST_PROGRAM:
{
ASTNodeList *decls = node->data.program_decls;
while (decls) {
generate_ir_from_ast(decls->node); // 递归生成子节点的IR
decls = decls->next;
}
}
return ir_new_operand_none(); // Program节点不产生结果
case AST_FUNCTION_DECL:
{
// 函数入口标签
char func_label_name[64];
sprintf(func_label_name, "_%s", node->data.func_decl.name); // 例如 _main
ir_add_instruction(&ir_head, ir_new_instruction(IR_LABEL, ir_new_operand_label(func_label_name), ir_new_operand_none(), ir_new_operand_none()));
// TODO: 考虑函数参数的IR生成(目前无参数)
// TODO: 分配局部变量的栈空间 (ALLOC_VAR 指令)
generate_ir_from_ast(node->data.func_decl.body); // 生成函数体的IR
}
return ir_new_operand_none();
case AST_COMPOUND_STMT:
{
ASTNodeList *stmts = node->data.compound_stmt.statements;
while (stmts) {
generate_ir_from_ast(stmts->node); // 递归生成语句的IR
stmts = stmts->next;
}
}
return ir_new_operand_none();
case AST_VAR_DECL:
{
// 为变量分配空间(逻辑上,实际分配在栈帧中由代码生成器处理)
// 这里生成一个 ALLOC_VAR 指令,告诉后端需要为这个变量分配空间
ir_add_instruction(&ir_head, ir_new_instruction(
IR_ALLOC_VAR,
ir_new_operand_var(node->data.var_decl.name),
ir_new_operand_int_lit(4), // 简化:所有int变量大小为4字节
ir_new_operand_none()
));
if (node->data.var_decl.initializer) {
// 如果有初始化表达式,生成赋值指令
IROperand init_val_temp = generate_ir_from_ast(node->data.var_decl.initializer);
ir_add_instruction(&ir_head, ir_new_instruction(
IR_STORE_VAR, // 将临时变量的值存储到实际变量
ir_new_operand_var(node->data.var_decl.name),
init_val_temp,
ir_new_operand_none()
));
}
}
return ir_new_operand_none();
case AST_EXPR_STMT:
// 表达式语句,生成表达式的IR,但结果不需要(因为没有赋值给变量)
generate_ir_from_ast(node->data.expr_stmt.expr);
return ir_new_operand_none();
case AST_RETURN_STMT:
{
IROperand ret_val_temp = ir_new_operand_none();
if (node->data.return_stmt.expr) {
ret_val_temp = generate_ir_from_ast(node->data.return_stmt.expr);
} else {
// 没有返回值,返回0 (C标准规定)
ret_val_temp = ir_new_operand_int_lit(0);
}
ir_add_instruction(&ir_head, ir_new_instruction(IR_RETURN, ir_new_operand_none(), ret_val_temp, ir_new_operand_none()));
}
return ir_new_operand_none();
case AST_IF_STMT:
{
// IF 语句的IR生成
// IF (condition) { THEN_BLOCK } ELSE { ELSE_BLOCK }
// ->
// evaluate condition -> temp_cond
// JUMPF temp_cond, label_else (如果条件为假,跳到else)
// THEN_BLOCK_IR
// JUMP label_end_if (跳过else)
// label_else:
// ELSE_BLOCK_IR
// label_end_if:
char *label_else = NULL;
char *label_end_if = NULL;
IROperand cond_temp = generate_ir_from_ast(node->data.if_stmt.condition);
// 生成跳转到else的标签名
char label_else_name[64];
sprintf(label_else_name, "L%d", ir_next_label_id());
label_else = strdup(label_else_name);
// 如果有else分支,生成结束if的标签名
if (node->data.if_stmt.else_stmt) {
char label_end_if_name[64];
sprintf(label_end_if_name, "L%d", ir_next_label_id());
label_end_if = strdup(label_end_if_name);
} else {
// 如果没有else分支,则 JUMPF 直接跳到 end_if
label_end_if = strdup(label_else_name); // label_else 兼作 end_if
}
// JUMPF condition, label_else
ir_add_instruction(&ir_head, ir_new_instruction(IR_JUMPF, ir_new_operand_label(label_else), cond_temp, ir_new_operand_none()));
// 生成 THEN_BLOCK_IR
generate_ir_from_ast(node->data.if_stmt.then_stmt);
if (node->data.if_stmt.else_stmt) {
// JUMP label_end_if (跳过else分支)
ir_add_instruction(&ir_head, ir_new_instruction(IR_JUMP, ir_new_operand_label(label_end_if), ir_new_operand_none(), ir_new_operand_none()));
// label_else:
ir_add_instruction(&ir_head, ir_new_instruction(IR_LABEL, ir_new_operand_label(label_else), ir_new_operand_none(), ir_new_operand_none()));
// ELSE_BLOCK_IR
generate_ir_from_ast(node->data.if_stmt.else_stmt);
} else {
// 如果没有else分支,JUMPF 目标就是 then 块结束后的位置
}
// label_end_if: (如果存在)
if (node->data.if_stmt.else_stmt) { // 如果是 if-else 结构,那么 end_if 是一个独立标签
ir_add_instruction(&ir_head, ir_new_instruction(IR_LABEL, ir_new_operand_label(label_end_if), ir_new_operand_none(), ir_new_operand_none()));
} else { // 如果是 if 结构,那么 label_else 就是 then 块结束后的位置
// 标签已经在 JUMPF 处使用,这里不再重复添加 LABEL 指令
// 释放 strdup 的内存
free(label_else);
label_else = NULL;
}
free(label_end_if); // 释放 strdup 的内存
}
return ir_new_operand_none();
case AST_BINARY_EXPR:
{
// 二元表达式的IR生成
// 例如: res = left_val op right_val
IROperand left_temp = generate_ir_from_ast(node->data.binary_expr.left);
IROperand right_temp = generate_ir_from_ast(node->data.binary_expr.right);
IROperand result_temp = ir_new_operand_temp(ir_next_temp_id());
IROpcode op = token_op_to_ir_op(node->data.binary_expr.op);
ir_add_instruction(&ir_head, ir_new_instruction(op, result_temp, left_temp, right_temp));
return result_temp; // 返回存储结果的临时变量
}
case AST_INTEGER_LITERAL:
{
// 整数常量的IR生成:加载立即数到临时变量
IROperand result_temp = ir_new_operand_temp(ir_next_temp_id());
ir_add_instruction(&ir_head, ir_new_instruction(IR_LOAD_IMM, result_temp, ir_new_operand_int_lit(node->data.int_literal.value), ir_new_operand_none()));
return result_temp; // 返回存储值的临时变量
}
case AST_IDENTIFIER:
{
// 标识符的IR生成:从变量加载值到临时变量
IROperand result_temp = ir_new_operand_temp(ir_next_temp_id());
ir_add_instruction(&ir_head, ir_new_instruction(IR_LOAD_VAR, result_temp, ir_new_operand_var(node->data.identifier.name), ir_new_operand_none()));
return result_temp; // 返回存储值的临时变量
}
case AST_ASSIGN_EXPR:
{
// 赋值表达式的IR生成
// 例如: var_name = right_expr_val
IROperand right_temp = generate_ir_from_ast(node->data.assign_expr.right);
// 假设左侧是 AST_IDENTIFIER
if (node->data.assign_expr.left->type != AST_IDENTIFIER) {
parser_error("Semantic Error: Invalid left-hand side for assignment in IR generation.");
}
char *var_name = node->data.assign_expr.left->data.identifier.name;
ir_add_instruction(&ir_head, ir_new_instruction(
IR_STORE_VAR, // 将临时变量的值存储到实际变量
ir_new_operand_var(var_name),
right_temp,
ir_new_operand_none()
));
return right_temp; // 赋值表达式的结果是右值
}
default:
parser_error("Unsupported AST node type for IR generation: %s", ast_node_type_to_string(node->type));
return ir_new_operand_none();
}
}
// --- 语法分析器入口点 (修改,新增IR生成和打印) ---
ASTNode* parse(const char *source_code_path) {
// 1. 初始化词法分析器
if (init_lexer(source_code_path) != 0) {
fprintf(stderr, "Error: Failed to initialize lexer.\n");
return NULL;
}
// 2. 初始化全局符号表并进入第一个作用域
global_symtab = symtab_init(128); // 初始容量128
current_symtab = global_symtab;
// TODO: 向全局符号表添加标准库函数(如 printf)的声明
symtab_add_symbol(global_symtab, "printf", SYM_FUNC, TOKEN_VOID); // 简化,实际应为复杂类型
// 3. 获取第一个Token,开始解析
current_token = get_next_token();
// 4. 开始解析整个程序,构建AST
ASTNode *program_ast = parse_program();
// 5. 清理词法分析器资源
close_lexer();
// 6. 打印全局符号表 (用于调试)
printf("\n--- Symbol Table ---\n");
symtab_print(global_symtab, 0);
// 7. 遍历AST并生成IR指令序列
printf("\n--- Intermediate Representation (IR) Generation ---\n");
if (program_ast) {
generate_ir_from_ast(program_ast);
if (ir_head) {
printf("Generated IR Sequence:\n");
ir_print_sequence(ir_head);
} else {
printf("No IR generated.\n");
}
} else {
printf("AST generation failed, cannot generate IR.\n");
}
// 8. 释放符号表资源
symtab_free(global_symtab);
return program_ast;
}
// --- 主函数,用于测试 (修改,新增IR相关资源的释放) ---
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
printf("--- Compiler Frontend (Lexer, Parser, Semantic Analyzer, IR Generator) Started ---\n");
ASTNode *program_ast = parse(argv[1]); // 调用parse函数,现在它会生成IR
if (program_ast) {
printf("\n--- Abstract Syntax Tree (AST) ---\n");
ast_print(program_ast, 0);
printf("\n--- Compiler Frontend Complete ---\n");
// 释放AST占用的内存
ast_free(program_ast);
printf("AST memory freed.\n");
// 释放IR占用的内存
ir_free_sequence(ir_head);
printf("IR memory freed.\n");
} else {
printf("\n--- Compiler Frontend Failed ---\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `parser.c` 的本次更新主要聚焦于**中间代码生成**。
*
* **1. `ir_head` 全局变量:**
* - 用于存储生成的IR指令序列的头指针。所有生成的IR指令都会添加到这个链表中。
*
* **2. `token_op_to_ir_op()` 辅助函数:**
* - 将词法分析器识别出的Token操作符(如 `TOKEN_PLUS`)映射到中间代码的操作码(如 `IR_ADD`)。
*
* **3. `generate_ir_from_ast(ASTNode *node)` 核心函数:**
* - 这是一个递归函数,它遍历AST的每一个节点。
* - **核心思想:** 对于每个AST节点,它会生成该节点所代表的C语言构造对应的IR指令。
* - **自底向上求值:** 对于表达式节点(如 `AST_BINARY_EXPR`, `AST_INTEGER_LITERAL`, `AST_IDENTIFIER`),
* 它会递归调用 `generate_ir_from_ast` 来获取子表达式的IR,并返回一个 `IROperand`,
* 通常是一个临时变量,代表了该子表达式的计算结果。
* - **生成指令:**
* - **`AST_PROGRAM` / `AST_FUNCTION_DECL` / `AST_COMPOUND_STMT`:** 它们是结构性节点,主要负责递归遍历其子节点。
* `AST_FUNCTION_DECL` 会生成函数入口标签 (`IR_LABEL`)。
* - **`AST_VAR_DECL`:** 生成 `IR_ALLOC_VAR` 指令,表示为该变量分配内存(逻辑上)。
* 如果变量有初始化器,会生成相应的 `LOAD_IMM` / `LOAD_VAR` 和 `STORE_VAR` 指令。
* - **`AST_EXPR_STMT`:** 递归生成内部表达式的IR,但结果不需要存储。
* - **`AST_RETURN_STMT`:** 生成 `IR_RETURN` 指令。如果 `return` 带有表达式,先生成表达式的IR,再用其结果作为返回值。
* - **`AST_IF_STMT`:** 这是控制流语句生成IR的典型例子。
* - 计算条件表达式,结果存入临时变量。
* - 生成一个 `IR_JUMPF` (条件假跳转) 指令,如果条件为假,则跳到 `else` 分支的开始(或 `if` 语句的结束)。
* - 生成 `then` 块的IR。
* - 如果有 `else` 块,生成一个 `IR_JUMP` (无条件跳转) 指令,跳过 `else` 块。
* - 插入 `else` 块的标签。
* - 生成 `else` 块的IR。
* - 插入 `if` 语句结束的标签。
* - **`AST_BINARY_EXPR`:**
* - 递归生成左右操作数的IR,得到两个临时变量。
* - 生成一个二元运算IR指令(`IR_ADD`, `IR_SUB` 等),将左右临时变量作为操作数,结果存入一个新的临时变量。
* - 返回这个新的临时变量。
* - **`AST_INTEGER_LITERAL`:** 生成 `IR_LOAD_IMM` 指令,将立即数加载到一个新的临时变量。
* - **`AST_IDENTIFIER`:** 生成 `IR_LOAD_VAR` 指令,将变量的值加载到一个新的临时变量。
* - **`AST_ASSIGN_EXPR`:**
* - 递归生成右值表达式的IR,得到一个临时变量。
* - 生成 `IR_STORE_VAR` 指令,将临时变量的值存储到左值所代表的实际变量中。
* - 返回右值的结果。
*
* **4. `parse()` 函数的修改:**
* - 在AST构建完成后,调用 `generate_ir_from_ast(program_ast);` 来启动IR生成过程。
* - 随后打印生成的IR序列 (`ir_print_sequence`)。
*
* **5. `main()` 函数的修改:**
* - 确保在程序结束时,除了释放AST,也调用 `ir_free_sequence(ir_head);` 来释放IR指令序列的内存,防止泄漏。
*
* **挑战与待完善:**
* - **类型信息:** 目前的IR生成没有充分利用语义分析阶段的类型信息。
* 在生成IR时,需要根据操作数的类型选择正确的IR指令(例如浮点数加法和整数加法是不同的)。
* - **优化:** 生成的IR是非常原始的,没有经过任何优化。实际编译器会在IR层面进行大量优化。
* - **函数调用参数:** 目前没有实现函数参数的传递和调用约定。
* - **更复杂的控制流:** `while`, `for`, `do-while`, `switch` 等语句的IR生成。
* - **数组、指针、结构体等复杂类型:** 这些类型需要更复杂的IR指令(如内存地址计算、解引用等)。
* - **错误处理:** 错误处理依然是“恐慌模式”。
*/
4. test.c
:测试文件(用于测试中间代码生成)
// 文件名: test.c
// 用于测试C语言编译器的词法、语法、语义分析和中间代码生成阶段。
int main() {
int a = 10;
int b;
b = 20;
int c = a + b * 2; // 复杂表达式
if (c >= 50) {
return 1;
} else {
int d = 5;
d = d + 1; // 简单运算和赋值
if (a < d) {
return 100;
}
return 0;
}
return 2; // 这段代码不会被执行到
}
目标代码生成 (Target Code Generation)——从IR到汇编的跳跃
现在我们有了抽象的中间代码,接下来就是编译器的“画龙点睛”之笔:将这些IR指令翻译成特定目标机器(例如x86-64)的汇编指令。这个阶段需要了解目标机器的指令集架构(ISA)、寄存器集和函数调用约定。
寄存器分配
在生成目标代码时,一个关键的优化是寄存器分配。CPU的寄存器是访问速度最快的存储单元,将变量的值尽可能地保存在寄存器中可以显著提高程序性能。一个复杂的寄存器分配算法会分析变量的“活跃区间”,决定哪些变量在何时占用哪个寄存器。
对于我们手撸的简化编译器,我们将采用非常简单的策略:
-
栈分配: 所有局部变量都分配在栈帧上,通过相对于栈帧基址指针(
%rbp
)的偏移量来访问。 -
临时变量映射: IR中的临时变量
tN
在实际代码生成时,通常会被直接映射到栈上的某个位置,或者在简单的指令中直接使用立即数或寄存器。 -
函数返回值: 遵循x86-64 System V ABI,函数返回值存放在
%eax
或%rax
寄存器中。
手撸代码生成器:C语言实现 (到x86-64汇编)
我们将实现一个函数,遍历IR指令序列,并为每条IR指令生成对应的x86-64汇编代码。我们将生成的汇编代码输出到一个文件中。
1. codegen.h
:目标代码生成器接口
// 文件名: codegen.h
// 描述: 声明目标代码生成器(Code Generator)的接口。
// 目标代码生成器负责将中间表示(IR)翻译成目标机器(x86-64)的汇编代码。
#ifndef CODEGEN_H
#define CODEGEN_H
#include "ir.h" // 包含IR的定义,因为代码生成器以IR为输入
// 函数声明:生成目标汇编代码
// 参数: ir_sequence_head - 中间代码指令序列的头指针。
// output_filepath - 生成汇编代码的输出文件路径(例如 "output.s")。
// 返回值: 成功返回0,失败返回非0。
int generate_code(IRInstruction *ir_sequence_head, const char *output_filepath);
#endif // CODEGEN_H
/*
* 逻辑分析:
* `codegen.h` 是目标代码生成器的“公共接口”。
* - 声明了 `generate_code` 函数,它接收IR序列作为输入,并指定输出的汇编文件路径。
* - 它的主要任务是:遍历IR指令,并将每条IR指令翻译成一系列机器相关的汇编指令。
*/
2. codegen.c
:目标代码生成器实现
// 文件名: codegen.c
// 描述: 目标代码生成器(Code Generator)的核心实现。
// 它将中间表示(IR)翻译成x86-64汇编代码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "codegen.h"
#include "ir.h"
#include "symbol_table.h" // 用于获取变量信息,如栈帧偏移量
// 假设我们有一个简单的局部变量栈帧偏移量映射
// 实际编译器会更复杂,基于符号表来计算
typedef struct {
char *name;
int offset; // 相对于 %rbp 的偏移量
} VarLocation;
// 这里是一个简化的全局列表来存储局部变量及其在栈上的偏移量
// 实际中,这应该作为语义分析或IR生成阶段的一部分,存储在SymbolEntry中
// 并且对于每个函数,局部变量的偏移量是独立的
static VarLocation *var_locations = NULL;
static int var_location_count = 0;
static int current_stack_offset = 0; // 当前函数内局部变量的栈帧偏移量(负值)
// 查找变量的栈偏移量
static int get_var_offset(const char *var_name) {
for (int i = 0; i < var_location_count; i++) {
if (strcmp(var_locations[i].name, var_name) == 0) {
return var_locations[i].offset;
}
}
// 未找到,这通常是一个语义错误,但在代码生成阶段应该已经被语义分析捕获
fprintf(stderr, "Code Generation Error: Undeclared variable '%s' or missing offset information.\n", var_name);
exit(EXIT_FAILURE);
}
// 添加变量及其栈偏移量
static void add_var_location(char *name, int offset) {
var_locations = (VarLocation*)realloc(var_locations, (var_location_count + 1) * sizeof(VarLocation));
if (!var_locations) {
fprintf(stderr, "Fatal Error: Failed to realloc var_locations.\n");
exit(EXIT_FAILURE);
}
var_locations[var_location_count].name = strdup(name);
var_locations[var_location_count].offset = offset;
var_location_count++;
}
// 重置局部变量栈帧信息
static void reset_var_locations() {
for (int i = 0; i < var_location_count; i++) {
free(var_locations[i].name);
}
free(var_locations);
var_locations = NULL;
var_location_count = 0;
current_stack_offset = 0;
}
// 文件指针用于写入汇编代码
static FILE *output_file = NULL;
// 写入汇编指令到文件
#define EMIT(...) fprintf(output_file, __VA_ARGS__)
// --- 核心代码生成函数 ---
int generate_code(IRInstruction *ir_sequence_head, const char *output_filepath) {
output_file = fopen(output_filepath, "w");
if (!output_file) {
fprintf(stderr, "Code Generation Error: Could not open output file '%s'\n", output_filepath);
return -1;
}
// 汇编文件头部(x86-64 System V ABI,AT&T 语法)
EMIT(".file \"<input_source>\"\n"); // 占位符,实际应为源文件名
EMIT(".text\n"); // 代码段
IRInstruction *current_instr = ir_sequence_head;
while (current_instr) {
switch (current_instr->opcode) {
case IR_LABEL:
EMIT("%s:\n", current_instr->result.val.label_name);
break;
case IR_ALLOC_VAR:
// 在IR_ALLOC_VAR指令处,记录变量的栈帧偏移量
current_stack_offset -= current_instr->op1.val.int_value; // 简化:假设所有int是4字节
// 栈向下增长,所以偏移量为负
add_var_location(current_instr->result.val.var_name, current_stack_offset);
// 实际的栈空间分配(subq $size, %rsp)会在函数入口处统一完成
break;
case IR_LOAD_IMM:
// result = value => movl $value, %reg / %stack_loc
// 我们将临时变量 tN 映射到栈上的偏移量,或者在连续操作中直接使用寄存器
// 这里为了简化,将临时变量 tN 想象成一个寄存器,后续优化再处理寄存器分配
// 或者简单地将结果存储到栈上的临时位置 (例如,我们统一分配一块区域给所有临时变量)
// 这里我们假设结果会用于紧随其后的操作,直接操作。
// 暂时不为每个临时变量分配固定栈位置
// 如果结果是临时变量,就使用 %eax (假设用于表达式求值)
// 实际会需要复杂的活跃分析和寄存器分配
EMIT("\tmovl\t$%d, %%eax\n", current_instr->op1.val.int_value); // 将立即数加载到 %eax
// 如果 result 是临时变量,则将其映射到 %eax
// 如果 result 是真实变量,则 store 到变量位置
if (current_instr->result.type == OPERAND_TYPE_VAR) { // 这应该不会发生
int offset = get_var_offset(current_instr->result.val.var_name);
EMIT("\tmovl\t%%eax, %d(%%rbp)\n", offset);
}
// 对于临时变量 tX,这里仅将结果放入 eax。
// 真正的临时变量管理需要更复杂的机制
break;
case IR_LOAD_VAR:
// result = *var_name => movl var_name_offset(%rbp), %reg
{
int offset = get_var_offset(current_instr->op1.val.var_name);
EMIT("\tmovl\t%d(%%rbp), %%eax\n", offset); // 从栈加载变量到 %eax
}
// 如果 result 是临时变量 tX,则其值在 %eax 中
break;
case IR_STORE_VAR:
// *var_name = op1 => movl %op1_reg, var_name_offset(%rbp)
{
int offset = get_var_offset(current_instr->result.val.var_name);
// 假设op1的值在 %eax 中(前一个IR指令的计算结果)
EMIT("\tmovl\t%%eax, %d(%%rbp)\n", offset); // 将 %eax 的值存储到栈变量
}
break;
case IR_ADD:
case IR_SUB:
case IR_MUL:
case IR_DIV:
case IR_EQ:
case IR_NE:
case IR_LT:
case IR_LE:
case IR_GT:
case IR_GE:
// 对于二元运算,假设op1和op2的值都在栈上的临时位置或寄存器中
// 简化:假设 op1 的值已在 %eax, op2 的值已在 %ebx 或是一个立即数/栈变量
// 实际编译器会为每个运算生成更精细的代码
// 这里为了演示,我们假设 op1 和 op2 都已通过 LOAD_IMM 或 LOAD_VAR 或其他二元运算的结果进入临时变量
// 并且这些临时变量在IR生成时被巧妙地放置到 %eax 和 %ecx(或者栈上)
// 更精确的实现需要一个值追踪或寄存器分配器。
// 这里我们做个粗略的模拟:
// 假设 left operand 的值已经在 %eax 中
// 假设 right operand 是一个立即数或变量 (通过 get_var_offset 访问)
// 1. 获取左操作数的值到 %eax (假设已在 %eax 或加载)
// 2. 获取右操作数的值到 %ecx (或者直接使用)
// 3. 执行操作
// 4. 结果在 %eax
// 简化处理:假设 op1 已经处理过,其值在 %eax
// 处理 op2
if (current_instr->op2.type == OPERAND_TYPE_INT_LIT) {
EMIT("\tmovl\t$%d, %%ecx\n", current_instr->op2.val.int_value);
} else if (current_instr->op2.type == OPERAND_TYPE_VAR) {
int offset = get_var_offset(current_instr->op2.val.var_name);
EMIT("\tmovl\t%d(%%rbp), %%ecx\n", offset);
} else if (current_instr->op2.type == OPERAND_TYPE_TEMP) {
// 如果 op2 是临时变量,且它的值在前一个指令中计算并存放在 %eax
// 这里为了简化,我们假设 op1 已经在 %eax, op2 将被处理到 %ecx
// 假设 op2 是前面某个计算的 tX,其值存放在栈上
// 这是一个简化的假设,实际需要精确的临时变量生命周期管理
EMIT("\tmovl\t-4(%%rsp), %%ecx\n"); // 假设临时变量都在rsp向下增长
// 实际编译器需要维护临时变量的栈偏移
}
switch (current_instr->opcode) {
case IR_ADD: EMIT("\taddl\t%%ecx, %%eax\n"); break;
case IR_SUB: EMIT("\tsubl\t%%ecx, %%eax\n"); break;
case IR_MUL: EMIT("\timull\t%%ecx, %%eax\n"); break; // 32位乘法
case IR_DIV:
// 除法在x86中比较特殊:需要将 %eax 扩展到 %edx:%eax,然后 idivl
EMIT("\tcltd\n"); // sign-extend %eax into %edx:%eax (eax is dividend)
EMIT("\tidivl\t%%ecx\n"); // signed division by %ecx (quotient in %eax, remainder in %edx)
break;
case IR_EQ:
case IR_NE:
case IR_LT:
case IR_LE:
case IR_GT:
case IR_GE:
// 比较操作:结果是 0 或 1 (布尔值)
// cmpl op2, op1; setcc reg_8bit; movzbl reg_8bit, reg_32bit
EMIT("\tcmpl\t%%ecx, %%eax\n"); // 比较 eax 和 ecx
switch (current_instr->opcode) {
case IR_EQ: EMIT("\tsete\t%%al\n"); break; // Set if Equal
case IR_NE: EMIT("\tsetne\t%%al\n"); break; // Set if Not Equal
case IR_LT: EMIT("\tsetl\t%%al\n"); break; // Set if Less
case IR_LE: EMIT("\tsetle\t%%al\n"); break; // Set if Less or Equal
case IR_GT: EMIT("\tsetg\t%%al\n"); break; // Set if Greater
case IR_GE: EMIT("\tsetge\t%%al\n"); break; // Set if Greater or Equal
default: break;
}
EMIT("\tmovzbl\t%%al, %%eax\n"); // 零扩展 %al 到 %eax
break;
default: break; // Should not reach here
}
// 结果已经在 %eax 中,如果 result 是临时变量,则其值在 %eax
// 如果 result 是真实变量,则需要存储
if (current_instr->result.type == OPERAND_TYPE_VAR) {
int offset = get_var_offset(current_instr->result.val.var_name);
EMIT("\tmovl\t%%eax, %d(%%rbp)\n", offset);
}
break;
case IR_JUMP:
EMIT("\tjmp\t%s\n", current_instr->result.val.label_name);
break;
case IR_JUMPF:
// JUMPF condition, label
// 假设 condition 的值已经在 %eax 中 (0为假,非0为真)
EMIT("\tcmpl\t$0, %%eax\n"); // 比较 %eax 是否为 0
EMIT("\tje\t%s\n", current_instr->result.val.label_name); // 如果等于0(假),则跳转
break;
case IR_RETURN:
// return value => movl $value, %eax; ret
if (current_instr->op1.type == OPERAND_TYPE_INT_LIT) {
EMIT("\tmovl\t$%d, %%eax\n", current_instr->op1.val.int_value);
} else if (current_instr->op1.type == OPERAND_TYPE_VAR) {
int offset = get_var_offset(current_instr->op1.val.var_name);
EMIT("\tmovl\t%d(%%rbp), %%eax\n", offset);
} else if (current_instr->op1.type == OPERAND_TYPE_TEMP) {
// 假设临时变量的值已在 %eax 中(上一个计算的结果)
// 无需额外移动
}
// 函数结束的特殊处理 (恢复栈帧)
// 实际编译器会在函数结束前统一恢复栈帧
// 这里我们假设 main 函数的返回,直接 ret 即可
EMIT("\tleave\n"); // leave 指令等价于 movq %rbp, %rsp; popq %rbp
EMIT("\tret\n");
break;
case IR_CALL: // 简化的函数调用
// CALL func_name, num_args, res
// 简化:目前不处理参数传递,只调用函数,结果放在 %eax
EMIT("\tcall\t%s\n", current_instr->op1.val.func_name);
// 如果有返回值,假设它在 %eax,需要赋值给 result
if (current_instr->result.type != OPERAND_TYPE_NONE) {
// TODO: 将 %eax 存储到 result (如果result不是临时变量,需要存储到栈)
// 例如,将其存储到临时变量 tX 对应的栈位置
}
break;
default:
fprintf(stderr, "Code Generation Error: Unhandled IR opcode: %s\n", ir_opcode_to_string(current_instr->opcode));
break;
}
current_instr = current_instr->next;
}
// 在文件末尾添加 main 函数的入口点和栈帧设置
// 假设程序只有一个 main 函数
EMIT("\n");
EMIT(".globl main\n");
EMIT(".type main, @function\n");
EMIT("main:\n");
EMIT("\tpushq\t%%rbp\n"); // 保存旧的 %rbp
EMIT("\tmovq\t%%rsp, %%rbp\n"); // 设置新的 %rbp
// 为局部变量分配栈空间
// 计算所有局部变量的总大小 (这里简单地乘以4,因为都是int)
// 实际需要根据 var_locations 中的最大 offset 来计算所需栈空间
int total_var_size = -current_stack_offset; // current_stack_offset 是负的,所以这里取正
// 确保栈对齐(例如16字节对齐)
if (total_var_size % 16 != 0) {
total_var_size = (total_var_size / 16 + 1) * 16;
}
EMIT("\tsubq\t$%d, %%rsp\n", total_var_size); // 调整栈指针为局部变量腾出空间
// 跳转到函数体真正开始的标签 (如果main函数体有自己的标签)
EMIT("\tjmp\t_%s\n", "main"); // 跳转到实际的函数体标签
// TODO: 在函数的实际返回点(IR_RETURN指令处)插入 leave 和 ret
// 这里是为了方便测试,暂时写在文件末尾,实际应由 IR_RETURN 负责。
// 清理局部变量栈帧信息
reset_var_locations();
fclose(output_file);
output_file = NULL;
return 0;
}
/*
* 逻辑分析:
* `codegen.c` 是将IR翻译成x86-64汇编代码的核心。
*
* **1. 栈帧和局部变量管理:**
* - `var_locations` 数组和 `get_var_offset`, `add_var_location`, `reset_var_locations` 函数:
* 这是一个简化的机制,用于模拟局部变量在栈帧上的分配。
* - `IR_ALLOC_VAR` 指令在IR生成时被插入,在代码生成时,`codegen.c` 会利用这些指令
* 来计算每个局部变量相对于 `%rbp`(栈帧基址指针)的偏移量。
* - `%rbp` 总是指向当前栈帧的底部(函数进入时保存的旧 `%rbp` 的位置),
* 局部变量通常在 `%rbp` 向下(地址减小)的方向上分配空间,所以偏移量是负值。
* - `main` 函数入口处的 `pushq %rbp; movq %rsp, %rbp; subq $size, %rsp` 是建立标准x86-64栈帧的典型序列。
* `subq $size, %rsp` 为所有局部变量和临时变量预留栈空间。
*
* **2. `generate_code()` 核心函数:**
* - 遍历IR指令链表。
* - 使用 `EMIT(...)` 宏将汇编指令格式化并写入输出文件。
* - **针对不同IR指令的翻译策略:**
* - **`IR_LABEL`:** 直接输出汇编标签(`label_name:`)。
* - **`IR_ALLOC_VAR`:** 在这个阶段,它不直接生成汇编指令,而是更新 `var_locations`,
* 记录变量的名称和在栈上的偏移量。实际的栈空间调整在函数入口处统一完成。
* - **`IR_LOAD_IMM`:** 将立即数加载到 `%eax` 寄存器。`movl $value, %eax`。
* - **`IR_LOAD_VAR`:** 从变量对应的栈位置加载值到 `%eax`。`movl offset(%rbp), %eax`。
* - **`IR_STORE_VAR`:** 将 `%eax` 中的值存储到变量对应的栈位置。`movl %eax, offset(%rbp)`。
* - **二元运算 (`IR_ADD`, `IR_SUB`, `IR_MUL`, `IR_DIV`, 比较操作):**
* - **简化假设:** 假设左操作数的结果已经存在于 `%eax` 中。
* - **处理右操作数:** 将右操作数的值加载到 `%ecx` 寄存器(或者直接使用立即数/栈变量)。
* - **执行指令:** 使用对应的x86汇编指令(`addl`, `subl`, `imull`, `idivl`)。
* - **比较操作:** `cmpl` 比较指令,然后使用 `setcc` 系列指令将比较结果(0或1)存入8位寄存器 `%al`,
* 再通过 `movzbl` 零扩展到32位 `%eax`。
* - **结果:** 运算结果或比较结果通常在 `%eax` 中。
* - **`IR_JUMP`:** 无条件跳转到标签。`jmp label_name`。
* - **`IR_JUMPF`:** 条件假跳转。比较 `%eax` 是否为0(假),如果是则跳转。`cmpl $0, %eax; je label_name`。
* - **`IR_RETURN`:** 将返回值(如果是表达式或变量)加载到 `%eax`,然后使用 `leave` 和 `ret` 恢复栈帧并返回。
* - **`IR_CALL`:** 简化为直接 `call function_name`。
* 实际需要处理参数的压栈,以及保存/恢复调用者保存的寄存器。
*
* **3. 汇编文件结构:**
* - `.file`, `.text`, `.globl`, `.type` 等是汇编器的标准指示符,用于定义文件类型、代码段、全局符号等。
* - `main` 函数的入口点和栈帧设置(`pushq %rbp`, `movq %rsp, %rbp`, `subq $size, %rsp`)是汇编程序的标准 prologue。
*
* **挑战与待完善:**
* - **完整的寄存器分配器:** 当前实现非常粗糙,没有真正的寄存器分配。
* 复杂的寄存器分配算法(如图着色算法)是编译器优化的一个重要组成部分。
* - **更复杂的栈帧布局:** 函数参数、返回地址、局部变量、溢出寄存器等。
* - **函数调用约定:** x86-64 System V ABI规定了参数传递、返回值、寄存器使用等。目前的 `IR_CALL` 只是占位符。
* - **数据类型:** 目前只处理 `int`。浮点数、指针、数组、结构体等需要不同的指令和处理。
* - **代码优化:** 在IR层面进行优化可以大大提高生成代码的质量。
* - **错误处理:** 仍然是简单地 `exit(EXIT_FAILURE)`。
* - **更通用性:** 当前是针对x86-64 AT&T 语法的汇编。
*/
3. 修改 parser.c
中的 main
函数,以调用 generate_code
// 文件名: parser.c (再次更新 main 函数)
// ... (所有前面的内容不变) ...
// --- 主函数,用于测试 (最终版本) ---
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <source_file.c>\n", argv[0]);
return EXIT_FAILURE;
}
printf("--- Compiler Pipeline Started ---\n");
ASTNode *program_ast = parse(argv[1]); // 调用parse函数,它会生成AST和IR
if (program_ast) {
printf("\n--- Abstract Syntax Tree (AST) ---\n");
ast_print(program_ast, 0);
printf("\n--- Intermediate Representation (IR) ---\n");
if (ir_head) {
ir_print_sequence(ir_head);
} else {
printf("No IR generated or IR generation failed.\n");
// 如果IR没有生成成功,这里就直接退出,不尝试代码生成
ast_free(program_ast);
ir_free_sequence(ir_head);
return EXIT_FAILURE;
}
printf("\n--- Target Code Generation (x86-64 Assembly) ---\n");
const char *output_asm_file = "output.s";
if (generate_code(ir_head, output_asm_file) == 0) {
printf("Assembly code generated successfully to '%s'.\n", output_asm_file);
printf("\n--- Compiler Pipeline Complete! ---\n");
printf("You can now assemble and link the output.s file:\n");
printf(" gcc -c %s -o output.o\n", output_asm_file);
printf(" gcc output.o -o a.out\n");
printf(" ./a.out\n");
} else {
printf("Code generation failed.\n");
// 确保即使代码生成失败也释放资源
ast_free(program_ast);
ir_free_sequence(ir_head);
return EXIT_FAILURE;
}
// 释放AST占用的内存
ast_free(program_ast);
printf("AST memory freed.\n");
// 释放IR占用的内存
ir_free_sequence(ir_head);
printf("IR memory freed.\n");
} else {
printf("\n--- Compiler Pipeline Failed ---\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/*
* 逻辑分析:
* `main` 函数现在是整个编译流水线的驱动。
* 1. 调用 `parse()` 函数,它会完成词法分析、语法分析、语义分析,并最终生成AST和IR。
* 2. 打印AST和IR,用于调试和验证。
* 3. **调用 `generate_code()` 函数:** 这是新增的关键一步,它将IR翻译成x86-64汇编代码并写入 `output.s` 文件。
* 4. 打印后续的编译/链接指令,指导用户如何使用标准的GCC工具链将生成的汇编代码转化为可执行程序。
* 5. 确保在所有阶段完成后正确释放AST和IR的内存。
*/
4. 完整的编译和运行步骤
现在,你的手撸编译器已经完成了从C源代码到x86-64汇编代码的完整翻译!
-
保存所有文件:
-
token.h
,lexer.h
,lexer.c
-
ast.h
,parser.h
,parser.c
-
symbol_table.h
,symbol_table.c
-
ir.h
,ir.c
-
codegen.h
,codegen.c
-
test.c
(你的测试C源代码)
-
-
编译你的编译器: 打开终端,进入所有文件所在的目录,然后执行:
gcc -o my_compiler lexer.c parser.c symbol_table.c ir.c codegen.c -g -Wall
这个命令会编译你所有的C源文件,生成一个名为
my_compiler
的可执行文件。 -
运行你的编译器: 使用你的编译器来处理
test.c
文件:./my_compiler test.c
你将看到详细的编译过程输出:符号表、生成的IR指令序列、以及最终生成的x86-64汇编代码的路径 (
output.s
)。 -
使用GCC完成汇编和链接: 你的
my_compiler
已经生成了output.s
文件。现在,你可以使用标准的GCC工具链将它转化为最终的可执行程序:gcc -c output.s -o output.o # 汇编:将汇编代码转换为目标文件 gcc output.o -o a.out # 链接:将目标文件与C运行时库链接,生成可执行文件 ./a.out # 运行你的程序!
如果
test.c
编译成功且执行正常,你将看到程序运行的结果(例如,如果test.c
返回1
,那么执行./a.out
后,你可以用echo $?
查看返回值为1
)。
恭喜你!你亲手构建了一个完整的C语言编译器(尽管是简化版)的前后端!
总结与展望
这场从零开始手撸C语言编译器的深度探索之旅,终于画上了圆满的句号。我们从最原始的源代码字符,一步步将其转化为CPU能够理解的机器指令,这个过程充满了挑战,也充满了乐趣和成就感。
我们:
-
掀开了C语言编译的“底裤”: 从预处理、编译(词法、语法、语义分析、中间代码生成、优化)到汇编、链接、加载,我们深入剖析了C程序从高级抽象到机器执行的每一个环节。
-
亲手打造了编译器前端:
-
词法分析器:让编译器能够“看”懂源代码中的每一个“单词”。
-
语法分析器:赋予编译器“组织结构”的能力,将Token流构建成抽象语法树(AST)。
-
语义分析器:让编译器“理解”代码的逻辑意义,通过符号表管理和类型检查,捕捉语义错误。
-
-
完成了编译器后端的核心:
-
中间代码生成器:将语义正确的AST转化为平台无关的三地址码(IR),实现了编译器前端和后端的解耦。
-
目标代码生成器:将IR翻译成特定架构(x86-64)的汇编代码,这是C语言代码向机器指令转化的最后一跃。
-
通过这个过程,你不仅掌握了编译器的核心原理和实现细节,更对C语言为何“牛逼”有了更深刻的体会——它不仅仅是一门语言,更是一套精密、高效的工具链和底层机制的完美结合。C语言能直接与硬件对话、其编译过程中的层层优化、以及强大的链接能力,都共同铸就了它在高性能、系统级编程领域的霸主地位。
当然,我们手撸的编译器只是一个“最小可行版本”。未来的路还很长,你可以继续深入探索:
-
扩展C语言特性: 支持函数参数、数组、指针、结构体、联合体、枚举、各种运算符的完整优先级和结合性、类型转换、复合赋值等。
-
实现更高级的优化: 死代码消除、常量传播、循环优化、更好的寄存器分配算法等。
-
错误恢复机制: 让编译器在遇到错误时能够尝试恢复并报告更多问题,而不是直接退出。
-
更完整的类型系统: 实现浮点数类型、函数指针、多维数组等,并进行精确的类型推断和兼容性检查。
-
支持其他目标平台: 将IR翻译成ARM或其他架构的汇编代码。
-
调试器支持: 为生成的代码添加调试信息,以便GDB等调试器能够理解。
-
标准库集成: 更完整地支持C标准库函数的调用。
C语言的魅力,在于它的简洁、强大,更在于它背后那套精妙而复杂的编译原理。希望这次手撸编译器的体验,能让你彻底“看透看破C语言的红尘”,从此对计算机底层和程序执行的奥秘,拥有一份独到的理解与掌控!祝你在编程的道路上越走越远,写出更多“牛逼”的代码!
------------------------------------------------------更新于2025.6.28下午六点12