Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析

用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析

原创
作者头像
云微
修改于 2020-06-08 02:10:16
修改于 2020-06-08 02:10:16
1.4K0
举报

项目github地址及源码:

https://github.com/yunwei37/tryC

这一篇讲讲在tryC中词法分析器是怎样构建的

词法分析器是什么玩意

回想一下上一篇我们说的词法分析阶段,编译器做了这样一件事:

代码语言:txt
AI代码解释
复制
对源程序进行阅读,并将字符序列,也就是源代码中一个个符号收集到称作记号(token)的单元中

帮编译器执行词法分析阶段的模块,就叫词法分析器啦。词法分析器能够对源码字符串做预处理,以减少语法分析器的复杂程度。

词法分析器以源码字符串为输入,输出为标记流(token stream),即一连串的标记,比如对于源代码中间:

代码语言:txt
AI代码解释
复制
    num = 123.4;

这样一个赋值语句中,变量num算是一个token,“=”符号算是一个token,“123.4”算是一个token;每个token有自己的类别和属性,比如“123.4”的类别是数字,属性(值)是123.4;每个token可以用这一对儿表示:{token, token value},就像“123.4”可以表示为{Num, 123.4}

词法分析器输入上面那句话,就得到这样一个标记流:

代码语言:txt
AI代码解释
复制
{Sym, num}, {'=', assign}, {Num, 123.4}

词法分析器的具体实现

由于词法分析器对于各个语言基本都是大同小异,在其他地方也有很多用途,并且手工构造的话实际上是一个很枯燥又容易出错的活计,因此其实已经有了不少现成的实现,比如 lex/flex 。

通常词法分析器的实现会涉及到正则表达式、状态机的一些相关知识,或者通过正则表达式用上面那些工具来生成。但对于我们这样一个简单的解释器来说,手工构造词法分析器,并且完全不涉及到正则表达式的知识,理解起来也并不是很困难啦。

先来看看token是怎样写的

token的数据结构如下:

代码语言:txt
AI代码解释
复制
int token;                      // current token type
union tokenValue {              // token value
    symbol* ptr;               
    double val;                 
} token_val;
  • 用一个整型变量 token 来表示当前的 token 是什么类型的;
  • 用一个联合体来表示附加的token属性,ptr可以附加指针类型的值,val可以附加数值。

token 的类型采用枚举表示定义:

代码语言:txt
AI代码解释
复制
/* tokens and classes (operators last and in precedence order) */
enum {
    Num = 128, Char, Str, Array, Func,
    Else, If, Return, While, Print, Puts, Read,
    Assign, OR, AND, Equal, Sym, FuncSym, ArraySym, Void,
    Nequal, LessEqual, GreatEqual, Inc, Dec
};

比如我们会将“==”标记为Equal,将Num标记为Sym等等。从这里也可以看出,一个标记(token)可能包含多个字符;而词法分析器能减小语法分析复杂度的原因,正是因为它相当于通过一定的编码(采用标记来表示一定的字符串)来压缩和规范化了源码。

另外,一些单个字符我们直接作为token返回,比如:

代码语言:txt
AI代码解释
复制
'}' '{' '(' ')' ';' '[' ']' .....

词法分析器真正干活的函数们

首先需要说明一下,源码字符串为输入,输出为标记流(token stream),这里的标记流并不是一次性将所有的源代码翻译成长长的一串标记串,而是需要一个标记的时候再转换一个标记,原因如下:

  1. 字符串转换成标记流有时是有状态的,即与代码的上下文是有关系的。
  2. 保存所有的标记流没有意义且浪费空间。

所以通常的实现是提供一个函数,每次调用该函数则返回下一个标记。这里说的函数就是 next() 。

这是next()的基本框架:其中“AAA”"BBB"是token类型;

代码语言:txt
AI代码解释
复制
void next() {
    while (token = *src) {
        ++src;
        if(token == AAA ){
            .....
        }else if(token == BBB ){
            .....
        }
    }
}

用while循环的原因有以下几个:

  • 处理错误: 如果碰到了一个我们不认识的字符,可以指出错误发生的位置,然后用while循环跳过当前错误,获取下一个token并继续编译;
  • 跳过空白字符; 在我们实现的tryC语言中,空格是用来作为分隔用的,并不作为语法的一部分。因此在实现中我们将它作为“不识别”的字符进行跳过。

现在来看看AAA、BBB具体是怎样判断的:

换行符和空白符
代码语言:txt
AI代码解释
复制
...
if (token == '\n') {
    old_src = src;              // 记录当前行,并跳过;
}
else if (token == ' ' || token == '\t') {        }
...
注释
代码语言:txt
AI代码解释
复制
...
else if (token == '#') {            // skip comments
    while (*src != 0 && *src != '\n') {
                src++;
    }
}
...
单个字符
代码语言:txt
AI代码解释
复制
...
else if ( token == '*' || token == '/'  || token == ';' ||  token == ',' ||
token == '(' || token == ')' || token == '{' || token == '}' ||  token == '[' || token == ']') {
    return;
}

...
数字

token 为Num;

token_val.val为值;

代码语言:txt
AI代码解释
复制
...
else if (token >= '0' && token <= '9') {        // process numbers
    token_val.val = (double)token - '0';
    while (*src >= '0' && *src <= '9') {
        token_val.val = token_val.val * 10.0 + *src++ - '0';
    }
    if (*src == '.') {
        src++;
        int countDig = 1;
        while (*src >= '0' && *src <= '9') {
            token_val.val = token_val.val + ((double)(*src++) - '0')/(10.0 * countDig++);
        }
    }
    token = Num;
    return;
}

...
字符串

token 为Str;

token_val.ptr保存字符串指针;

代码语言:txt
AI代码解释
复制
...
        else if (token == '"' ) {               // parse string
            last_pos = src;
            char tval;
            int numCount = 0;
            while (*src != 0 && *src != token) {
                src++;
                numCount++;          
            }
            if (*src) {
                *src = 0;
                token_val.ptr = malloc(sizeof(char) * numCount + 8);
                strcpy(token_val.ptr, last_pos);
                *src = token;
                src++;
            }
            token = Str;
            return;
        }

...
字符

token 为Char;

token_val.val为值;

代码语言:txt
AI代码解释
复制
...
        else if (token == '\'') {               // parse char
            token_val.val = *src++;
            token = Char;
            src++;
            return;
        }

...
变量:这是最复杂的一部分

对变量的处理需要以下几个步骤:

  1. 获取完整的变量名:
  2. 在符号表中查找变量:
  3. 如果在符号表中找到了变量,根据变量不同的类型,返回不同的token值;
  4. 如果没有找到,在符号表中间插入新的变量

关于符号表具体的内容,会独立出一篇文章来解释。

代码语言:txt
AI代码解释
复制
...
        else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) {
            last_pos = src - 1;             // process symbols
            char nameBuffer[MAXNAMESIZE];
            nameBuffer[0] = token;
            while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || (*src == '_')) {
                nameBuffer[src - last_pos] = *src;
                src++;
            }
            nameBuffer[src - last_pos] = 0;                 // get symbol name
            int i;
            for (i = symPointer-1; i >= 0; --i) {           // search symbol in symbol table 
                if (strcmp(nameBuffer, symtab[i].name) == 0) {      // if find symbol: return the token according to symbol type
                    if (symtab[i].type == Num || symtab[i].type == Char) {
                        token_val.ptr = &symtab[i];
                        token = Sym;
                    }
                    else if (symtab[i].type == FuncSym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else if (symtab[i].type == ArraySym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else {
                        if (symtab[i].type == Void) {
                            token = Sym;
                            token_val.ptr = &symtab[i];
                        }
                        else token = symtab[i].type;
                    }
                    return;
                }
            }
            strcpy(symtab[symPointer].name, nameBuffer);        // if symbol not found, create a new one 
            symtab[symPointer].levelNum = currentlevel;
            symtab[symPointer].type = Void;
            token_val.ptr = &symtab[symPointer];
            symPointer++;
            token = Sym;
            return;
        }
...
其他的一些符号,可能需要再多读取一个字符才能确定具体token
代码语言:txt
AI代码解释
复制
...
        else if (token == '=') {            // parse '==' and '='
            if (*src == '=') {
                src++;
                token = Equal;
            }
            return;
        }
        else if (token == '+') {            // parse '+' and '++'
            if (*src == '+') {
                src++;
                token = Inc;
            }
            return;
        }
        else if (token == '-') {            // parse '-' and '--'
            if (*src == '-') {
                src++;
                token = Dec;
            }
            return;
        }
        else if (token == '!') {               // parse '!='
            if (*src == '=') {
                src++;
                token = Nequal;
            }
            return;
        }
        else if (token == '<') {               // parse '<=',  or '<'
            if (*src == '=') {
                src++;
                token = LessEqual;
            }
            return;
        }
        else if (token == '>') {                // parse '>=',  or '>'
            if (*src == '=') {
                src++;
                token = GreatEqual;
            }
            return;
        }
        else if (token == '|') {                // parse  '||'
            if (*src == '|') {
                src++;
                token = OR;
            }
            return;
        }
        else if (token == '&') {                // parse  '&&'
            if (*src == '&') {
                src++;
                token = AND;
            }
            return;
        }

...
错误处理
代码语言:txt
AI代码解释
复制
...
        else {
            printf("unexpected token: %d\n", token);
        }

...
match函数:用于匹配当前token并获取下一个token
代码语言:txt
AI代码解释
复制
// 匹配一个记号,并获取下一个token:
void match(int tk) {
    if (token == tk) {
        next();
    }
    else {          // 遇到了一个错误
        exit(-1);
    }
}

可对照源码查看(如果觉得写得还行麻烦您帮我点个star哦)

https://github.com/yunwei37/tryC

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析
这一部分,我们再回过头来看看变量、函数是怎样存储和处理的、以及符号表是怎样构建的。
云微
2020/06/05
1.2K0
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计
通常我们说的 “编译器” 是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。
云微
2020/06/05
1.7K0
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
云微
2023/02/24
6370
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1
我们来看看两个概念,EBNF和递归下降文法,以及如何用这两个方法来计算tryC中的表达式。
云微
2020/06/05
1.8K0
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
云微
2023/02/11
3780
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2
布尔表达式和算术表达式的代码之前已经讲过了,这里看看statement的实现,以及如何在语法分析的同时解释执行:
云微
2020/06/05
8540
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
云微
2023/02/11
5760
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言
这一系列教程希望面向初学者,使用c语言手工实现一个简单的解释器来玩,不需要您掌握除了c语言以外的其他前置知识,也不需要您学习过编译原理的相关知识(当然如果能对简单的数据结构有所了解的话会更好,比如树、栈等)。
云微
2020/06/05
1.5K0
如何编写一个 Python 词法分析器
Python 词法分析器是一种可以将 Python 代码分解成一组记号的程序。这些记号是 Python 语法的基本组成单位,包括标识符、关键字、运算符、分隔符等。词法分析器在 Python 解释器中扮演着重要的角色,它负责将源代码转换为计算机可以理解的形式。
用户11021319
2024/03/22
2180
如何编写一个 Python 词法分析器
简易C语言词法分析程序
浪漫主义狗
2024/03/22
1970
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
云微
2023/02/11
5120
【死磕Sharding-jdbc】---SQL解析-词法分析
sharding-jdbc对SQL解析的源码主要在下图所示parsing模块中,由下图可知SQL解析主要分为两部分:lexer和parser。lexer就是本文需要分析的词法分析:
用户1655470
2018/08/16
1.2K0
【死磕Sharding-jdbc】---SQL解析-词法分析
mold源码阅读十二 创建一些输出段
这里对verdef和verneed段进行构造,实际写入内容。其中包含了字符串信息,因此还会将字符串写入dynstr中。
AkemiHomura
2023/10/22
2280
词法分析器(Lexer)的实现
写下Compiler系列的主要目的,是为了记录一下本人在学习编译原理以及做出一个简单的Compiler的历程,为后续向二进制安全的更深领域的学习打下基础。
h1J4cker
2022/12/01
1.9K0
Java编写的C语言词法分析器
    这是java编写的C语言词法分析器,我也是参考很多代码,然后将核心代码整理起来,准备放在QQ空间和博客上,目的是互相学习借鉴,希望可以得到高手改进。这个词法分析器实现的功能有打开文件、保存文件、打开帮助文档、文本域内容的剪切和复制和黏贴、进行词法分析 程序的项目结构如图,Word类和Unidentifiable类是两个JavaBean类,存放的参数有两个row(整型)、word(String),row用于获取行数,word用于获取标识符,LexerFrame是词法分析器的界面类,Analyze封装了进行词法分析的核心代码 ,doc文件夹放一个帮助文档,当用户点击帮助按钮时可以弹出来以帮助用户使用。 Github项目链接:https://github.com/u014427391/lexer1.1.0,欢迎star //核心程序:
SmileNicky
2019/01/17
1.3K0
一个用基于Java语言编写的词法分析器代码的自动生成程序,模仿lex程序的需求应用设计 DokymeLex
推荐理由:一个用基于Java语言编写的词法分析器代码的自动生成程序,模仿lex程序的需求应用设计完成 DokymeLex,Language files blank comment code,Java 13 130 119 1176,SUM: 13 130 119 1176,概述,这是一个模仿Lex程序功能的词法分析器代码生成程序,简称“编译器的编译器”。该程序能够读取由用户定义的.dkm文件,分析该文件中的声明、正规定义、规则并生成能够通过JVM运行的JAVA的词法分析器源代码。Lex简介,Lex helps write programs whose control flow is directed by instances of regular expressions in the inp
用户4073486
2023/01/02
5860
编译原理:2. 词法分析
词法的(Lex-i-cal):与语言的单词或词汇有关,但有别于语言的文法和结构的。
浪漫主义狗
2023/09/04
7620
编译原理:2. 词法分析
编译原理词法分析程序c语言_编译器常用的语法分析方法
前面已经介绍了编译器的预处理,词法分析,词法分析器的实现,也在其中说到了语法分析的任务和过程。
全栈程序员站长
2022/11/16
7920
自制计算器——《自制编程语言》二
get_token()接受的入参是一个Token结构体指针,函数会分割出记号装入Token结构体并返回。下面是上面两个函数声明和Token结构体的定义:
Charlie_W
2021/04/09
1.7K0
自制计算器——《自制编程语言》二
【编译原理】词法分析:C/C++实现
编译原理是计算机科学领域的一个重要分支,它研究如何将高级编程语言的源代码转化成计算机能够执行的机器代码或中间代码的过程。编译原理涵盖了编译器的设计和实现,其中编译器是一种将源代码翻译成目标代码的软件工具。编译器的主要任务包括语法分析、词法分析、语义分析、优化和代码生成等环节。
SarPro
2024/02/20
1.9K0
【编译原理】词法分析:C/C++实现
推荐阅读
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析
1.2K0
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计
1.7K0
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计
6370
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1
1.8K0
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现
3780
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2
8540
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法
5760
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言
1.5K0
如何编写一个 Python 词法分析器
2180
简易C语言词法分析程序
1970
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言
5120
【死磕Sharding-jdbc】---SQL解析-词法分析
1.2K0
mold源码阅读十二 创建一些输出段
2280
词法分析器(Lexer)的实现
1.9K0
Java编写的C语言词法分析器
1.3K0
一个用基于Java语言编写的词法分析器代码的自动生成程序,模仿lex程序的需求应用设计 DokymeLex
5860
编译原理:2. 词法分析
7620
编译原理词法分析程序c语言_编译器常用的语法分析方法
7920
自制计算器——《自制编程语言》二
1.7K0
【编译原理】词法分析:C/C++实现
1.9K0
相关推荐
用c语言手搓一个500+行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档