热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

编译原理c语言词法分析器,用C语言实现一个真正的词法分析器

词法分析,是编译器的第一个模块,也是最简单的模块。最简单,指的是相对于编译器这种大型程序而言,与一般的代码相比还是有点复杂的

词法分析,是编译器的第一个模块,也是最简单的模块。

最简单,指的是相对于编译器这种大型程序而言,与一般的代码相比还是有点复杂的。

关于词法分析的简介,可以看之前的文章:

词法分析器的简单思路

按照通常的C代码惯例,前缀暂时设置为scf,Simple Compiler Framework,简单编译器框架。

它所需的基本数据结构,就是动态字符串和双向链表,前两篇文章已经做了简单的代码介绍。

用C语言写个动态字符串

用C语言实现Linux风格的双向链表

首先,需要定义一个枚举类型,说明词法分析要支持的单词类型,即各种运算符、常量、标识符。

因为是从C和C++中抽取了容易实现的那部分语法,单词类型还是很多的,见如下几张图片:

第一张图,是各种运算符的类型定义,与流行的编程语言基本一样。

7de26fbd6d07441dd27c666180caf37e.png各种运算符

第二张,大小括号、分号、逗号、冒号等语法标示符号,箭头、点号等运算符。

箭头->,一般表示指针。点号,表示取类的成员。

三个点号,表示函数的动态参数,例如printf(const char* fmt, ...)在词法分析时就会用到。

空格,space,在词法分析时作为分隔符之一。它是不需要传递到语法分析阶段的,用完之后需要忽略掉。

EOF,表示源代码文件的结尾,fgetc()之类的函数在文件结束时会返回这个值。把它也作为一个单词,用于提示词法分析过程的结束。

07686aefbb7cdb043ac5a74b000099f6.png括号之类的也算运算符

第三张,主要是用于代码流程控制的关键字。

ced591f3503b8a99b22baf26ea9c9878.png流程控制的关键字

第四张,是用于类型定义的关键字。

bd9adad9cf81c01876204a4c08c3b3d0.png数据类型的关键字

第五张,也是这个枚举的最后一部分,是常量和标识符。

de60ba4da11e805ae3496fc6c85b542c.png其他

定义完了这个枚举,就可以定义单词的数据结构,如下图:

30e0ec7cb415255485706ef54f1da59d.png

list,用于把它挂载到词法分析器的链表上,按照先进先出顺序(FIFO),以备后续的语法分析时读取。

type,填写为上面那个枚举的其中之一,用于表示这个单词的类型。

data,用于存储常量的值,可以是常量数字或者常量字符串。

text,用于存储单词的原始文本,即这个词的源代码。

file、line、pos,用于存储单词所在的源码文件名,行号,行内的位置,用于打印错误信息。

接下来定义几个与单词相关的函数,alloc()、clone()、free(),等等。

042b713c4fbc2cd910b031d6b02a2517.png

然后在C文件里实现这三个函数。

ac0ec68c32ac9d24a32735c350c3142f.png

86785894511c6aa831e83932bb290f8f.png

c4f65fbbcea3c2cb71171ac053695c4e.png

最后,定义词法分析器(lexer)的数据结构,scf_lex_t。

ea1f7c78be8bd77fc6a5d577931a81ed.png

scf_lex_key_word_t,用于定义关键字的源码字符串,和关键字对应的单词类型。

在一门编程语言中,关键字是固定的。

如果在词法分析时获得的字符串,能在关键字列表里查到,那么它就是个关键字。

关键字列表,就是scf_lex_key_word_t类型的数组。

scf_lex_escape_char_t,用于转义字符,一般是\r \n \t \0这4个,以\表示接下来的一个字符是转义字符。

如果能在转义字符列表里查到,要把这两个字符转化为对应的转义ascii码。例如'\n'要转化为一个字节的整数10。

转义字符列表,就是scf_lex_escape_char_t类型的数组。

scf_lex_error_t,用于存储错误信息。因为不一定发现一个错误就立即终止分析(打印错误信息),有可能积累到一定的数量之后再一起打印出来,所以它用于保存错误信息。

scf_lex_char_t,用于表示从源码文件里读取的一个字符。

词法分析,是一个字符一个字符的读取。

如果发现当前字符是下一个单词的,那么就要把它放回去,等分析下一个单词时再把它取出来,所以需要一个链表挂载暂时被放回的单词。

不能真把它写回源码文件再读出来,fgetc()之后文件指针已经变了,再写回去会覆盖掉下一个字符的。把它挂载一个链表上,优先从链表读,链表为空的时候再读源文件,是比较好的处理方法。

从当前单词的末尾字符多查看一个字符(即下一个单词的第一个字符),才能确定当前单词的终止位置,这叫LL1下降分析。这个多查看的字符,也叫终止符。

(这里我不是故意黑龙书,而是就这么点事,它居然真写的云山雾罩,让人一头雾水,读不下去。)

854de2265178cd365404a84de814db7a.png

scf_lex_t,词法分析器的数据结构,它有open()、close()、push_word()、pop_word()这4个函数。

其中push_word()和pop_word()是给语法分析器(parser)用的,语法分析时也存在LL1下降分析的场景,push_word()用于放回一个单词。

word_list_head,用于存储语法分析时这个临时被放回的单词,与词法分析的场景类似。

error_list_head,用于存储还没有打印的错误信息。

char_list_head,用于存储临时被放回的字符。

FILE* fp,源码文件的文件指针,从它这里读取字符。

nb_identities,标识符的个数。随着分析过程这个值是不断增加的,要根据它的值给获取的每一个标识符编不同的号码。

所有数据结构的定义说完了,接下来是代码实现。

scf_lex_open()的path是源码文件的路径,二级指针plex用于返回创建的词法分析器。

2204a0c706585bf41e152ab786f7dd3c.png

f932a55ea91208f6eba876bd32efb459.png

open、close、push_word,都比较简单,真正的词法分析在pop_word()函数里。

语法分析器(parser)在需要一个单词时,就调用词法分析器(lexer)的pop_word()函数,获取下一个单词。

之所以这么设计,而不是先完成整个源码文件的词法分析,是为了减少单词的库存,毕竟分析完的单词也是要占内存的。如果源码文件比较大,万一内存不够了就尴尬了,所以采取现使用现生产的JIT模式,暂时用不着的单词就让它在源码文件里存着。

在具体的词法分析之前,先准备关键字列表和转义字符列表。

b2d55bb4746c2df115dff8bc33fc7818.png

896308af5e619729a31dd173481ecf9c.png

更改这个关键字数组,就可以给词法分析器添加关键字。

转义字符暂时设置了4个,也可以更改这个数组添加。

下图是两个查找函数,用于查找关键字和转义字符。

93437fdcf5d3a4bddfb4f5599fe849ea.png

接下来,是词法分析的核心函数,scf_lex_pop_word(),它声明是:

int scf_lex_pop_word(scf_lext_t* lex, scf_lex_word_t** pword);

二级指针用于返回单词,返回值用于返回错误码,成功返回0,失败返回一个负数(一般是-1)。

5160c93e8cda46a62c82968062a22aad.pngpop_word1

它被语法分析器调用(也可以直接被main函数调用,只做词法分析),所以在单词链表不空的情况下(有被语法分析器放回的单词),优先返回链表头部的单词。

在单词链表空的情况下,调用_lex_pop_char()获取一个字符,开始新单词的分析。

如果是EOF,源码已经读完了,就返回一个EOF单词,获得这个词之后语法分析就会结束,同时词法分析也结束了。

如果是\n \r \t和空格,一律当作空格丢弃,但是\n时要更新行号,其他字符要更新列号。

_lex_space()函数会丢弃所有被当作空格的字符。它包含一个递归调用,用于处理连续的空格。如果当前字符不被当作空格,它就调用_lex_push_char(),把它放回到字符链表上。

0f69e2abc46fb210c3d5d13ab50740da.png

丢弃完空格之后,继续分析下一个单词。

接着图片pop_word1的代码926行,如下图的pop_word2。

cddaa00c6f9c6a5d701edf4e9233df17.pngpop_word2

它分析+ - * / %开头的运算符,例如+ += ++,- -= --,* *=,/ /=,% %=这些运算符。

图pop_word3主要是一些单个字符的特殊运算符,各种括号,逗号,冒号,分号。

185ce1fa42f21a1e0c584b7b69d09cc0.pngpop_word3

pop_word4,是以& | ! =开头的运算符,例如& && &=,| || |=,= ==。

c07085e34bfc632a9b6d57da945cd4d0.pngpop_word4

最后一张,图pop_word5。

a66536f85dae252bf4609b2e5d744b45.pngpop_word5

前几行是分析以>和 >> >= >>=,<<<<= <<=。

然后是点号,它可能是1个点或者3个点,分别表示类的成员和函数的可变参数。

以&#39;开始的是字符,以"开始的是字符串,它们都需要做转义字符分析。在词法分析时,它们都是起始字符和终止字符,用于标示字符和字符串的起始和终止。

转义字符在源代码里实际是2个字符,要转义成1个。

字符串,可以是包含或者不包含转义字符的多个字符。

0到9开头的是常量数字,即使是16进制以0x开头,8进制以0开头,开始也是0。

10进制的开头0到9都可能。

暂时不支持直接以点号开头的浮点数,所以0.5必须写成0.5,不能写成.5,否则报词法错误。

标识符,以下划线_或者a-z或者A-Z开头,中间可以包含数字。

总体结构说完了,里面的一些具体分析的函数,接下来几篇再说。

最后还是推荐下龙书,编译原理,因为编译器领域的著作比较少,另外两本虎书和鲸书更难读。

举报/反馈



推荐阅读
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • CMake跨平台开发实践
    本文介绍如何使用CMake支持不同平台的代码编译。通过一个简单的示例,我们将展示如何编写CMakeLists.txt以适应Linux和Windows平台,并实现跨平台的函数调用。 ... [详细]
  • 数据库内核开发入门 | 搭建研发环境的初步指南
    本课程将带你从零开始,逐步掌握数据库内核开发的基础知识和实践技能,重点介绍如何搭建OceanBase的开发环境。 ... [详细]
  • 本文详细介绍了 Apache Jena 库中的 Txn.executeWrite 方法,通过多个实际代码示例展示了其在不同场景下的应用,帮助开发者更好地理解和使用该方法。 ... [详细]
  • 深入理解Java泛型:JDK 5的新特性
    本文详细介绍了Java泛型的概念及其在JDK 5中的应用,通过具体代码示例解释了泛型的引入、作用和优势。同时,探讨了泛型类、泛型方法和泛型接口的实现,并深入讲解了通配符的使用。 ... [详细]
  • 火星商店问题:线段树分治与持久化Trie树的应用
    本题涉及编号为1至n的火星商店,每个商店有一个永久商品价值v。操作包括每天在指定商店增加一个新商品,以及查询某段时间内某些商店中所有商品(含永久商品)与给定密码值的最大异或结果。通过线段树分治和持久化Trie树来高效解决此问题。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 深入解析Spring Cloud Ribbon负载均衡机制
    本文详细介绍了Spring Cloud中的Ribbon组件如何实现服务调用的负载均衡。通过分析其工作原理、源码结构及配置方式,帮助读者理解Ribbon在分布式系统中的重要作用。 ... [详细]
  • 本文深入探讨了 Java 中的 Serializable 接口,解释了其实现机制、用途及注意事项,帮助开发者更好地理解和使用序列化功能。 ... [详细]
  • 本文探讨了如何在模运算下高效计算组合数C(n, m),并详细介绍了乘法逆元的应用。通过扩展欧几里得算法求解乘法逆元,从而实现除法取余的计算。 ... [详细]
  • 本文详细介绍了Java中的访问器(getter)和修改器(setter),探讨了它们在保护数据完整性、增强代码可维护性方面的重要作用。通过具体示例,展示了如何正确使用这些方法来控制类属性的访问和更新。 ... [详细]
  • 题目Link题目学习link1题目学习link2题目学习link3%%%受益匪浅!-----&# ... [详细]
  • 本文详细探讨了VxWorks操作系统中双向链表和环形缓冲区的实现原理及使用方法,通过具体示例代码加深理解。 ... [详细]
  • Linux设备驱动程序:异步时间操作与调度机制
    本文介绍了Linux内核中的几种异步延迟操作方法,包括内核定时器、tasklet机制和工作队列。这些机制允许在未来的某个时间点执行任务,而无需阻塞当前线程,从而提高系统的响应性和效率。 ... [详细]
author-avatar
蓝天ab白云
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有