试用GO开发pyhton编译器:字节码基础

试用GO开发pyhton编译器:字节码基础,第1张

掌握一门编程语言最好的办法或许是将它的编译器设计出来。毫无疑问那些开发Python编译器的人应该是世界上对Python了解最深刻的人群之一。我用python开发过不少程序,但是每次反思或复盘的时候总是感觉对Python的认知还不到位,由此也看了很多讲Python的书,但看的时候感觉好像懂了,但过了一段时间后又忘了,也就是说单纯看书很难将某一项技术完全内化。当然技能的掌握必然要从实践中来,但是我发现在使用Python开发程序时,我总是使用它的一部分功能就够了,或者说居于我的思维模式限制,我在使用python开发时总是落入一个套路,这使得我只能掌握python技术的冰山一角,就如同井底之蛙一样只了解一小块内容,为了能够打破认知局限,让我自己能更全面的对python的设计原理有更深入的了解,我打算尝试做一个能运行的python编译器。

我在标题中使用了”试用”,也就是这是一个尝试性质,编译器的技术难度足够大,我不清楚能做到哪一步,所以采取了走一步看一步的方式,能做多少就多少,也有可能尝试后发现太难而做不下去,因此是”试用“的由来。

我计划用Go语言来实现python编译器,这样完成这个项目后我们能收获一箭双雕的好处,一是掌握如何使用GO来开发一个复杂程序,一是对python的设计原理能有深入的了解和掌握。首先我们来尝试做一个简单的,基于栈的虚拟机,后面我们会把python编译成字节码,然后在我们设计的虚拟机上运行,这个过程跟java类似。

对虚拟机而言,首先需要字节码,它们是针对虚拟机的一系列 *** 作指令,例如push 1, push 2, add,这三条字节码会把数值1,2压入虚拟机,然后d出栈顶的两个数值进行相加,把相加结果放到堆栈的顶部,如下图所示:

首先我们要实现的是字节码,所谓字节码就是一个 *** 作指令,后面跟着0个或1个 *** 作数,例如push 1, add等,每个 *** 作指令用一个数值表示,一旦虚拟机读取到对应的数值时就执行相应 *** 作,如果我们使用0x01表示push,那么当虚拟机读取到数字0x01时,它就会把跟在这个指令后面的4个字节对应的数值压入堆栈。

我们先创建一个文件夹叫GO_Python,然后在里面再创建一个文件夹叫code,接着创建文件code.go,它对应字节码的实现代码,我们先完成一些基本定义:

package code 

import (
	"encoding/binary"
	"fmt"
)

type Instructions [] byte //字节码集合

type Opcode byte  // *** 作码

const (
	opConstant opcode = iota
	OpPop  //d出堆栈
	OpAdd  //将栈顶两个数d出并相加,把结果压入堆栈
	OpSub
	OpMul 
	OpDiv
	//后面还有更多 *** 作码需要定义
)

假设我们把python代码编译成字节码后,它们就对应Instructions,在字节码中有一些字节代表了特定 *** 作,例如push, add, sub等,这些 *** 作就对应 *** 作码。 *** 作码可以使用不同的数值来区分,因此代码中定义了枚举类型数值来对应 *** 作码,注意到 *** 作码的类型为byte,这意味着我们的虚拟机最多支持128种不同 *** 作。

我们还需要对 *** 作码做进一步描述,例如给定 *** 作码OpPop后,我们希望能找到它对应的字符串,例如"POP",同时不同 *** 作码其实对应不同的 *** 作数,例如OpAdd就对应两个 *** 作数,所有这些信息我们都需要以代码的方式记录下来,因此我们继续添加如下定义:

type Definition struct {
	Name string // *** 作码对应的字符串
	OperandWidths [] int // *** 作数对应的字节数
}

var definitions = map[Opcode]*Definition {
	OpConstant: {"OpConstant", []int{2}},
}

func Lookup(op byte) (*Definition, error ) {
	//给定 *** 作码,返回它对应的信息定义
	def, ok := definitions[Opcode(op)]
	if !ok {
		return nil, fmt.Errorf("opcode %d undefined", op)
	}

	return def, nil
}

代码中需要说明的一点是operandWidths,它对应 *** 作数的字节长度,例如对于表达式 255 + 1,我们需要使用 *** 作码opAdd将两个数值d出,然后相加,由于255对应两个字节,1对应1个字节,于是对应的definition就是{“OpAdd", []{2, 1}}。由此我们可以理解上面代码中 *** 作码"OpConstant"对应的 *** 作数有2个字节的长度,OpConstant *** 作符的作用是在一个常量数组中查找对应数组,它的 *** 作数就是数组下标,我们会把代码中所定义的一切常量都放入到一个特定的常量数组中,相关内容后续我们会进一步解释。

接下来我们看如何将 *** 作码以及 *** 作数转换成一条可以被虚拟机执行的指令,所谓”指令“其实就是byte数组,数组的第一个字节对应 *** 作符的数值,后续字节对应 *** 作数的内容。假设有一条字节码为 OpConstant 65534, 那么将它转换为指令时,第一个字节就对应 *** 作码OpConstant的数值,也就是0,接下来就对应 *** 作数的字节内容,由于65534拆解成两个最字节就是0xFF, 0xFE,于是这条 *** 作码转换为”指令“后就是[]byte{0x0, 0xFF, 0xFE}, 我们看对应的代码实现:

func Make(op Opcode, operands ...int) []byte {
	//给定 *** 作码,创建字节码指令
	def , ok := definitions[op]
	if !ok {
		return []byte{}
	}

	//一条指令的字节长度包括 *** 作码对应的长度加上 *** 作数对应的长度
	instructionLen :=1  // *** 作码长度始终为1
	for _, w := range def.OperandWidths {
		instructionLen += w 
	}
    //一条指令由一系列字节组成,第一个字节就是 *** 作码,接下来的字节对应 *** 作数
	instructions := make([]byte, instructionLen) 
	instructions[0] = op //设置 *** 作码对应的字节
	offset := 1
	for i, o := range operands {
		width := def.OperandWidths[i]
		switch width {
		case 2:
			//把一个16比特数,也就是uint16类型的数值拆解成2个byte放到数组中
			binary.BigEndian.PutUint16(instruction[offset:], uint16(o))
		}
		offset += width 
	}

	return instruction
}

于是当我们的虚拟机在执行指令[]byte{0x0, 0xFF, 0xFE}时,它发现第一个字节为0,于是它就知道要执行OpConstant *** 作,也就是要从常量数组中把对应的内容拿出来,同时根据definitions结构体可以知道,对应的 *** 作数有两个字节,于是它把接下来的两个字节也就是0xFF,0xFE组合起来称为一个 *** 作数,也就是获得了65534这个数值,然后将65534作为数组的下标,从常量数组中把下标为65534的元素取出来。

最好的学习方式就是即时反馈,所以我们有了一些代码后,要尽快把它们运行起来,看看执行结果,这样我们才能通过实验来验证逻辑或者是清除头脑中的疑惑,如果没有即时反馈,那么我们很快就会因为困惑积累过多而放弃努力,因此我们将以测试的方式把上面代码跑起来,在同一目录下创建明文code_test.go的文件,在里面添加测试代码如下:

package code 
import "testing"

func TestMake(t *testing.T) {
	tests := []struct {
		op Opcode 
		operands []int 
		expected []byte 
	} {
		{OpConstant, []int{65534}, []byte{0x0, 0xFF, 0xFE}},
	}

	for _, tt := range tests {
		instruction := Make(tt.op, tt.operands...)
		if len(instruction) != len(tt.expected) {
			t.Errorf("instruction has wrong length. want=%d, got=%d",
		len(tt.expected), len(instruction))
		}

		for i , b := range tt.expected {
			if instruction[i] != tt.expected[i] {
				t.Errorf("wrong byte at pos %d, want=%d, got = %d", i, b, instruction[i])
			}
		}
	}
}

完成上面代码后,进入到code目录然后执行:

go  test

这样就能将测试用例跑起来,通过结果可以看到用例能通过,也就是Make函数准确的将 *** 作码及其对应的 *** 作数转换成了一条指令字节数组,为了好消化,我们一次不要搞太多,先在这里停止。

完整代码请查看GitHub

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996180.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存