试用GO开发python编译器:实现词法解析

2024-04-30 21:48

本文主要是介绍试用GO开发python编译器:实现词法解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

编译器由于涉及到编译原理,了解计算机科学的同学就能感触到,编译原理是较为抽象,无论从原理还是从实践上都是比较难把握的对象。在接触理论性较强,难度较大的问题时,最好的办法是从最简单的情况入手,先从感性上获得认知,为后面的理性认知打下基础,因此我们先从编译原理算法的基础入手,首先掌握词法解析。

上一节我们体会了“字节码”,现在问题在于”巧妇难为无米之炊“,我们总得有东西让我们产生字节码,这需要我们有能力将给定的代码进行解析后产生对应的字节码,而将代码转换为字节码的过程需要一系列非常复杂的操作,本节我们先从这些复杂操作的第一步,也就是词法解析开始。

词法解析简单来说就是对编程语言中的对象进行分类,例如在代码中,”1“,”234“,”3.14“等这类字符串我们将他们归类为NUMBER,用数值1来表示,类似”def", “map”, “string”,“with”,这类字符串我们将他们归类为KEYWORD,用数值2来表示,类似”+“,”-“,”*“,"/" ,"(",")",我们归类为OPERATOR,用数值3表示,类似"x",“y”,"my_str"等这类字符串归类为IDENTIFIER,用数值4表示,以此类推。如果我们把代码中对应的元素进相应归类后,一段看起来很复杂的代码其实就是一系列归类符号的组合,例如语句"x + (y - 1) "就可以转换成IDENTIFIER OPERATOR IDENTIFIER OPERATOR IDENTIFIER OPERAOTR NUMBR,由此词法解析其实是对源代码进行分析时所做的第一步抽象。

在词法解析中例如上面用来进行归类的标签,例如OPERATOR, IDENTIFIER,等我们统称为token,在python内核系列文章里面,我们下载了python编译器代码,里面有一个文件夹叫Grammar,在里面有一个文件叫token,打开之后能看到如下内容:

请添加图片描述
文件里面描述的就是对不同符号的归类,从上面可以看出左括号被归类为LPAR,所有的操作符号都有对应的归类,当读取一段Python代码后,将代码中不同符号根据上面的对应关系完成归类的过程就是词法解析。

接下来我们开始词法解析的实现,首先定义具体的数据结构,在上节基础上新建一个文件夹名为Token,在里面添加一个"token.go”文件,添加如下代码:

package token type TokenType string type Token struct {Type TokenType  //类型Literal  string  //对应字符串
}//例如数值”1“对应的实例为Token {"NUMBER", "1"}

根据python语法的token文件,我们先进行一系列常量定义:

const (ILLEGAL = "ILLEGAL"EOF = "EOF"INDENT = "INDENT"  //变量类型对应的归类NUMBER = "NUMBER"  //数值类型对应的归类EQUAL = "=" //赋值操作符PLUS = "+" //加号操作符LPAR = "("RPAR = ")"LBRACE = "{"RBRACE = "}"COMMA = ","DEF = "def"  //关键字
)

上面我们堆一系列符号进行了归类,当然还有很多符号没有进行相应归类,后面我们用到的时候再处理,要不然会搞得过于复杂。接下来我们的目标是读取一段代码字符串,将字符串分割成不同的单元,然后将这些单元对应到给定分类。在相同目录下新建一个文件夹叫lexer,里面添加一个文件名为lexer_test.go,相应内容如下:

package lexer import("testing""token"
)func TestNextToken(t *testing.T) {input := `=+(){},`tests := []struct {expectedType  token.TokenType expectedLiteral string } {{token.EQUAL, "="},{token.PLUS, "+"},{token.LPAR, "("},{token.RPAR, ")"},{token.LBRACE, "{"},{token.RBRACE, "}"},{token.COMMA, ","},{token.EOF, ""},}l := New(input)for i, tt := range tests {tok := l.NextToken()if tok.Type != tt.expectedType {t.Fatalf("test[%d] - tokenType wrong. expected=%q, got=%q",i, tt.expectedType, tok.Type)if tok.Literal != tt.expectedLiteral {t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",i, tt.expectedLiteral, tok.Literal)}}}
}

上面的测试用例还不能运行,我们需要先实现函数New,它返回一个对象能够提供接口NextToken,首先我们先进入Token文件夹设置一下其包信息:

go mod init token

然后进入到lexer文件夹,同样初始化一下包信息:

go mod init lexer

为了能成功引用token包,我们要打开lexer文件夹下的go.mod添加如下信息:

replace token => ../token

然后运行命令:

go mod tidy

完成上面操作后,lexer包就能使用token包导出的接口了。下面我们完成一个基本功能的词法解析器,在lexer文件夹下面新建一个文件为lexer.go,输入内容如下:

package lexer type Lexer struct {input string  //要解析的源代码字符串position int  //当前读取的字符位置readPosition int //下一个要读取的字符位置,也就是position + 1ch  byte  //读取的字符
}func New(input string) *Lexer { //生成一个词法解析器l := &Lexer{input: input}return l
}

我们看上面代码,为何需position和readPosition两个变量呢,其中position指向当前读取的字符所在位置,readPosition指向下一个要读取的字符位置,在解析过程中,我们在读取到当前字符时,还需要看下一个字符的内容才能决定要执行的操作,因此我们还需要readPosition来指向下一个字符,下面实现相关的函数:

package lexer 
import "token"type Lexer struct {input string  //要解析的源代码字符串position int  //当前读取的字符位置readPosition int //下一个要读取的字符位置,也就是position + 1ch  byte  //读取的字符
}func New(input string) *Lexer { //生成一个词法解析器l := &Lexer{input: input}l.readChar() //先读取第一个字符return l
}func (l *Lexer) readChar() { //读取当前字符if l.readPosition >= len(l.input) {l.ch = 0} else {l.ch = l.input[l.readPosition]}l.position = l.readPosition l.readPosition += 1
}func (l *Lexer) NextToken() token.Token{//读取一个字符,判断是否属于特定分类var tok token.Tokenswitch l.ch {case '=':tok = newToken(token.EQUAL, l.ch)case '(':tok = newToken(token.LPAR, l.ch)case ')':tok = newToken(token.RPAR, l.ch)case ',':tok = newToken(token.COMMA, l.ch)case '+':tok = newToken(token.PLUS, l.ch)case '{':tok = newToken(token.LBRACE, l.ch)case '}':tok = newToken(token.RBRACE, l.ch)case 0:  //读取到末尾tok.Literal = ""tok.Type = token.EOF }l.readChar()return tok 
}func newToken(tokenType token.TokenType, ch byte) token.Token {return token.Token{Type: tokenType, Literal: string(ch)}
}

上面代码逻辑比较简单,每次从输入的字符串中读取一个字符,看看读到的字符是否属于特定分类,现在可以跑起测试用例,在命令行窗口进入lexer目录然后运行go test,没问题的话可以看到测试用例通过。

下面重点来了,我们要解析一段python代码,其内容如下:

def  add(x, y):assert x > 0 and y > 0return x + y

上面是python代码定义的一个函数,在上面代码字符串中add, x, y, 属于一个类别,也就是变量名,我们用IDENTIFIER表示,(,)分别属于LPRA和RPRA。def, return, assert and属于同一个类别,也就是关键字,我们在lexer_test.go中添加对应测试用例

func TestNextToken2(t *testing.T) {input := `def add(x, y):z = x + yreturn z`tests := []struct {expectedType token.TokenType expectedLiteral string } {{token.DEF, "def"},{token.IDENTIFIER, "add"},{token.LPAR, "("},{token.IDENTIFIER, "x"},{token.COMMA, ","},{token.IDENTIFIER, "y"},{token.RPAR, ")"},{token.COLON, ":"},{token.IDENTIFIER, "z"},{token.EQUAL, "="},{token.IDENTIFIER, "x"},{token.PLUS, "+"},{token.IDENTIFIER, "z"},{token.RETURN, "return"},{token.IDENTIFIER, "z"}}l := New(input)for i, tt := range tests {tok := l.NextToken()if tok.Type != tt.expectedType {t.Fatalf("test[%d] - tokenType wrong. expected=%q, got=%q",i, tt.expectedType, tok.Type)if tok.Literal != tt.expectedLiteral {t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",i, tt.expectedLiteral, tok.Literal)}}}
}

这里我们有个问题需要解决,那就是识别变量名,变量名的规则是以字母开头,后面跟着数字或者是下划线,因此解析逻辑就是,当我们读取到字符时,我们就进入到变量名的识别流程,也就是读取到字符后,如果接下来读取的还是字符,数字或者是下划线,我们就不断的往下走,直到遇到不是字符,数字或下划线的符号为止,由此我们在lexer.go中实现如下代码:

func (l *Lexer) NextToken() token.Token{var tok token.Tokenswitch l.ch {....default:if isLetter(l.ch) {tok.Literal = l.readIdentifier()return tok} else {tok = newToken(token.ILLEGAL, l.ch)}}l.readChar()return tok 
}//如果读取到字符,数字或者下划线那么就继续读取下一个字符
func (l *Lexer) readIdentifier() string {position := l.position for isLetter(l.ch) {l.readChar()}//获取到变量名字符串return l.input[position : l.position]
}func isLetter(ch byte) bool {return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}

现在我们有一个问题,那就是满足变量名的条件中有一个字符串,他们不能简单看做是变量,这些字符串就是关键字,例如def, return, assert and,这些字符串有特定的功能,虽然他们的组成规则满足变量名的要求,但是我们要专门把他们抽取出来,于是我们回到token.go进行相应处理:

const (ILLEGAL = "ILLEGAL"EOF = "EOF"IDENTIFIER = "IDENTIFIER"  //变量类型对应的归类NUMBER = "NUMBER"  //数值类型对应的归类EQUAL = "=" //赋值操作符PLUS = "+" //加号操作符LPAR = "("RPAR = ")"LBRACE = "{"RBRACE = "}"COMMA = ","COLON = ":"LESS = "<"GREATER = ">"DEF = "def"  //关键字INT = "int"RETURN = "return"ASSERT = "assert"AND = "and"
)var keywords = map[string]TokenType {"def" : DEF,"int" : INT,"return" : RETURN,"assert" : ASSERT,"and" : AND,
}//将关键字从变量名中区别开来
func LookupIdent(ident string) TokenType {if tok, ok := keywords[ident]; ok {return tok }return IDENTIFIER
}

由此我们获得到变量名后,就得再做一次处理,看看所得变量名是不是关键字,由此回到lexer.go:

default:if isLetter(l.ch) {tok.Literal = l.readIdentifier()//看看变量名是否属于关键字tok.Type = token.LookupIdent(tok.Literal)return tok} else {tok = newToken(token.ILLEGAL, l.ch)}

现在我们还有一个问题没有处理到,那就是空格,回车,换行等特殊符号,对Python而言空格有特定作用,但我们这里先忽略它,于是在读取字符时,遇到空格,回车,换行的字符时要忽略他们,所以在lexer.go中要做如下处理:

func (l *Lexer) NextToken() token.Token{//读取一个字符,判断是否属于特定分类var tok token.Token//忽略空格,回车,换行等特定字符l.skipSpecialChar()....
}func (l *Lexer) skipSpecialChar() {//不读取回车换行,空格等这些特定字符for l.ch == ' ' || l.ch == '\t' || l.ch =='\n' || l.ch == '\r' {l.readChar()}
}

现在我们还有一种特定字符串需要处理,那就是数字,数字的规则就是,它由“0”到“9”这几个字符组成,我们暂时忽略调浮点数,只处理整数,于是一旦我们读取的字符串以数字开头时,我们就进入数字识别流程,接下来的字符必须跟着数字,一旦读取到非数字字符时,我们就判断当前读取到的字符合在一起是否形成有效数字,由此我们继续修改lexer.go:

func (l *Lexer) NextToken() token.Token{var tok token.Token//忽略空格,回车,换行等特定字符l.skipSpecialChar()switch l.ch {...default:if isLetter(l.ch) {tok.Literal = l.readIdentifier()//看看变量名是否属于关键字tok.Type = token.LookupIdent(tok.Literal)return tok} else if isDigit(l.ch) {tok.Type = token.NUMBERtok.Literal = l.readNumber()return tok} else {tok = newToken(token.ILLEGAL, l.ch)}}	
}	unc (l *Lexer) readNumber() string {position := l.position for isDigit(l.ch) {l.readChar()}return l.input[position : l.position]
}func isDigit(ch byte) bool {return '0' <= ch && '9' >= ch
}

完成以上代码后,我们当前所有测试都能通过,这也意味着着我们当前的工作已经能够对Python代码做初步的解析了。眼尖的同学可能会意识到一个问题,那就是在读取数字时,如果我们遇到非法的数字字符串例如“123abc",此时词法解析器会将其读作123和abc,这个问题在以后章节中我们再做处理。

完整代码请点击这里
https://github.com/wycl16514/go_python_lexer_1.git

这篇关于试用GO开发python编译器:实现词法解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/950006

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Python版本信息获取方法详解与实战

《Python版本信息获取方法详解与实战》在Python开发中,获取Python版本号是调试、兼容性检查和版本控制的重要基础操作,本文详细介绍了如何使用sys和platform模块获取Python的主... 目录1. python版本号获取基础2. 使用sys模块获取版本信息2.1 sys模块概述2.1.1

一文解析C#中的StringSplitOptions枚举

《一文解析C#中的StringSplitOptions枚举》StringSplitOptions是C#中的一个枚举类型,用于控制string.Split()方法分割字符串时的行为,核心作用是处理分割后... 目录C#的StringSplitOptions枚举1.StringSplitOptions枚举的常用