ZhangJie Software Development Engineer

实现一个简单的脚本语言zscript


本文使用go语言实现一个简单的脚本语言zscript。包括zcomplier、zasm、zvm三个部分。其中,zcomplier为zscript编译器:负责对zscript脚本文件(.zs)进行词法分析、语法分析,并生成zvm汇编代码文件(.zasm)。zasm为zscript汇编器:负责对zcomplier生成的汇编文件(.zasm)进行汇编,并生成zvm可执行文件(.zse)。zvm为zscript虚拟机:负责加载并执行可执行文件(.zse)。

zscript

项目完整代码详见:https://github.com/AnonymousRookie/zscript

zcomplier

zcomplier负责对zscript脚本文件(.zs)进行词法分析、语法分析,并生成zvm汇编代码文件(.zasm)。 汇编语言是一种可以被硬件处理器或者虚拟机直接理解的代码,由精心设计的指令组成。

直接翻译一段代码是非常困难的,然而如果把编译的过程划分为几个不同的阶段,每个阶段只处理完成特定的任务,翻译过程就回极大的简化。

编译器通常可以分为前端和后端,前端包括词法分析、语法分析、语义分析和中间代码生成;后端则由目标代码生成和一些可选的优化阶段组成。将编译器的阶段分为前端和后端可以带来很多好处,比如优化和移植。

词法分析

词法分析的主要目的是把每行代码转换成一个由token组成的集合,通过有限状态机能够很容易的实现。。

基于有限状态机的词法分析器的结构非常简单:整个工作都是在一个大的循环中完成的,当这个循环重复执行的时候,就会完成分词和属性标注的工作。循环的每一次迭代主要做三件事:读入下一个字符,如果有必要迁移到下一状态,执行当前状态要求的任何操作。例如:词法分析器开始时处于“开始状态”,读入第一个字符,假设第一个字符是8,词法分析器马上迁移到整数状态,认为8是一个整型值的第一个数字,随着循环的不断进行,会读入更多的字符,每读入一个字符,都可能引发一次状态迁移,然而只要读入的仍然是数字,状态就仍然保持在整型状态。

语法分析

语法分析主要是对词法分析阶段产生的token集合进行处理。zcomplier中通过递归下降分析法实现了一个简单的parser。递归下降分析法,主要通过重复使用嵌套,多次调用递归函数完成。

语义分析

词法分析器保证字符流中的词法单元都是合法的,然而只进行语法检查是不够的,因为语言除了语法以外,还有语义。例如:标识符重定义、在定义范围外使用标识符等,这些错误都不会在语法分析阶段发现。

中间代码生成

语法分析和语义分析阶段过后得到的就是中间代码,它是一种清晰的并且结构化的表示脚本的内部存储形式。从理论上讲,中间代码的关键特征就是与特定的源语言和目标语言完全无关。

中间代码模块的接口负责完成以下几个任务:

  • 向当前中间代码流的末尾添加指令;
  • 向添加后的指令添加各种类型的操作数;
  • 自动生成下一个唯一的跳转目标索引并将其添加到指令流中;
  • 添加源代码标注;
  • 根据中间代码节点在流中的顺序获取中间代码。

目标代码生成

中间代码和zvm汇编语言指令通常都是一一对应的,从每条中间代码指令到zvm汇编指令的翻译可以很轻松的完成。

示例

  • demo.zs
func sum(a, b)
{
    return a + b;
}

func main()
{
    var str;
    str = "Hello zscript!";
    print(str);

    var a;
    var b;
    var c;

    a = 1.1;
    b = 2;
    c = 3;
    
    var s;
    s = sum(a, b);
    print(s);

    var ret;
    ret = 1 + 9 * 4 / (8 - 5) * 2 + sum(a, b) - c;
    print(ret);

    return;
}
  • 编译demo.zs生成demo.zasm
./zcomplier demo.zs
  • demo.zasm
; Functions
Func sum
{
    Param b
    Param a

    ; return a + b;
    Push a
    Push b
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Pop _RetVal
    Ret 
}
Func main
{
    Var str
    Var a
    Var b
    Var c
    Var s
    Var ret

    ; str = "Hello zscript!";
    Push "Hello zscript!"
    Pop _T0
    Mov str, _T0

    ; print(str);
    Push str
    CallHostApi print

    ; a = 1.1;
    Push 1.100
    Pop _T0
    Mov a, _T0

    ; b = 2;
    Push 2
    Pop _T0
    Mov b, _T0

    ; c = 3;
    Push 3
    Pop _T0
    Mov c, _T0

    ; s = sum(a, b);
    Push a
    Push b
    Call sum
    Push _RetVal
    Pop _T0
    Mov s, _T0

    ; print(s);
    Push s
    CallHostApi print

    ; ret = 1 + 9 * 4 / (8 - 5) * 2 + sum(a, b) - c;
    Push 1
    Push 9
    Push 4
    Pop _T1
    Pop _T0
    Mul _T0, _T1
    Push _T0
    Push 8
    Push 5
    Pop _T1
    Pop _T0
    Sub _T0, _T1
    Push _T0
    Pop _T1
    Pop _T0
    Div _T0, _T1
    Push _T0
    Push 2
    Pop _T1
    Pop _T0
    Mul _T0, _T1
    Push _T0
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Push a
    Push b
    Call sum
    Push _RetVal
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Push c
    Pop _T1
    Pop _T0
    Sub _T0, _T1
    Push _T0
    Pop _T0
    Mov ret, _T0

    ; print(ret);
    Push ret
    CallHostApi print

    ; return;
    Ret 
}

zasm

简单来说,汇编器的工作就是打开输入文件,转换它的内容,并把结果写到输出文件中。 zasm负责对zcomplier生成的汇编文件(.zasm)进行汇编,并生成zvm可执行文件(.zse)。

.zse可执行文件格式

// 文件头
Header
// 指令流
Instruction Stream
// 字符串表
String Table
// 函数表
Function Table
// 主应用程序API调用表
HostApiCall Table

示例

  • demo.zasm
; Functions
Func sum
{
    Param b
    Param a

    ; return a + b;
    Push a
    Push b
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Pop _RetVal
    Ret 
}
Func main
{
    Var str
    Var a
    Var b
    Var c
    Var s
    Var ret

    ; str = "Hello zscript!";
    Push "Hello zscript!"
    Pop _T0
    Mov str, _T0

    ; print(str);
    Push str
    CallHostApi print

    ; a = 1.1;
    Push 1.100
    Pop _T0
    Mov a, _T0

    ; b = 2;
    Push 2
    Pop _T0
    Mov b, _T0

    ; c = 3;
    Push 3
    Pop _T0
    Mov c, _T0

    ; s = sum(a, b);
    Push a
    Push b
    Call sum
    Push _RetVal
    Pop _T0
    Mov s, _T0

    ; print(s);
    Push s
    CallHostApi print

    ; ret = 1 + 9 * 4 / (8 - 5) * 2 + sum(a, b) - c;
    Push 1
    Push 9
    Push 4
    Pop _T1
    Pop _T0
    Mul _T0, _T1
    Push _T0
    Push 8
    Push 5
    Pop _T1
    Pop _T0
    Sub _T0, _T1
    Push _T0
    Pop _T1
    Pop _T0
    Div _T0, _T1
    Push _T0
    Push 2
    Pop _T1
    Pop _T0
    Mul _T0, _T1
    Push _T0
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Push a
    Push b
    Call sum
    Push _RetVal
    Pop _T1
    Pop _T0
    Add _T0, _T1
    Push _T0
    Push c
    Pop _T1
    Pop _T0
    Sub _T0, _T1
    Push _T0
    Pop _T0
    Mov ret, _T0

    ; print(ret);
    Push ret
    CallHostApi print

    ; return;
    Ret 
}
  • 根据demo.zasm进行汇编生成demo.zse
./zasm demo.zasm

zvm

虚拟机(VM)是一种运行时环境,它是一个专门用来帮助其他软件或数据(可执行代码)执行的软件。虚拟机相对于其他类型运行时环境的本质区别在于,它刻意模拟一台真实计算机的配置和功能,通过虚拟处理器、虚拟内存地址空间和虚拟寄存器等来完成。

zvm负责加载并执行可执行文件(.zse)。

虚拟机的主要组成部分:

  • 指令流
  • 运行时堆栈
  • 全局数据表

虚拟机生命周期的各个阶段:

  • 装载脚本并初始化主要的数据结构;
  • 定位脚本的入口点并开始执行;
  • 通过处理指令流中的下一条指令来持续执行;
  • 终止执行并释放主要的数据结构。

.zse可执行文件格式

// 文件头
Header
// 指令流
Instruction Stream
// 字符串表
String Table
// 函数表
Function Table
// 主应用程序API调用表
HostApiCall Table

执行.zse可执行文件

./zvm demo.zse

上一篇 go学习笔记

下一篇 Qt开发笔记

Comments

Content