Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/jxwufan/Kiscript

JavaScript interpreter written purely in C
https://github.com/jxwufan/Kiscript

Last synced: about 2 months ago
JSON representation

JavaScript interpreter written purely in C

Lists

README

        

#KiScript解释器
—— 一个简易的类JavaScript解释器

@[KiScript,解释器, JavaScript]

-------------------

##小组成员
张海: 语法解析及语法树生成的实现

邬凡: 运行框架的设计,闭包函数面向对象等功能的实现

张逸瑶: 所有类型之间运算符的实现及测试

李明哲: 测试及报告撰写
##程序简介
###程序基本介绍
本程序是一个由C语言编写的跨平台程序,可以用于解释一个符合ECMAScript 5.1,参考ECMAScript 6.0并带有一些自定义语法的类JavaScript语言Kiscript。

###程序运行平台与配置
本程序基于glib, libgc库,在Clion IDE下编写,可在MacOS/Linux环境下编译运行。

###KiScript的特殊语法

- 语言所用编码为UTF-8而非UTF-16;
- 不含正则表达式;
- 不存在关键字作为标识符;
- 不含1.或.1;
- 字符串中\0永远被看做NUL;
- 数组下标格式为字面量或将变量值的字符串;
- 不自动添加分号;
- 不含in;
- if / for / while / do while语句后需接语句块{}而非单句语句;
- 输出函数为Log()函数而非console.log()。

##程序结构与设计
###语法分析
递归下降文法分析器

解释器使用了递归下降的方法手动构造 JavaScript 的语法解析,参考的标准是 [ECMAScript 5.1](ecma-international.org/ecma-262/5.1/index.html),并使用 GLib 提供基本数据结构和可移植性。

在正式开始编写解析器之前,组员张海用了一两天的时间,对标准的结构和脚本语法的结构进行探索,建立合适的模型,并不断尝试和调整组织代码的方式,经过大量的时间才有了现在的解析器架构。

根据 EcmaScript 5.1 标准中的说明,脚本的语法至少分为词法、表达式语法、语句语法和函数语法四个部分,其中后三者使用词法分析的输出作为输入。程序在不同的文件中分别实现了这四个模块的语法,让它们互相调用,最终完成对脚本的解析。

脚本使用一个 `token_t` 结构代表,结构中包含一个 `id` 域表明 Token 的类型,以及一个 `data` 域携带可能相关的数据。通过多个附加的函数指针,`token_t` 可以被正确地克隆和释放。解析中的错误通过一种特殊类型的 Token,从发生错误的位置向上传递。

因为采用手写递归下降解析器的方式,解析过程中跳过了解析树生成,可以直接产生便于执行的语法树,同时也使得左右递归和前向判断容易实现,灵活度很高。

为了使得代码表达更加清晰,程序使用了数十个辅助函数和辅助宏,使得解析代码中尽量表达语义而非冗长重复的代码块。

解析器总共实现了 64 种 Token,可以报告 111 种错误,共计 5700+ 行,其中关于文法和语法树的注释也有近 1000 行。在编写代码的过程中,总共产生了 136 次 commit、9600+ 行的插入和 4100+ 行的删除,可以说花费了许多心血。

###Token解析
语法分析器所分析出来的语法树就是一棵由token通过指针连接而形成的树。语法分析器所分析出来的token有不同的类型:标识符,关键字,空格,数值,字符串等。而每种类型的token的功能也各不相同,因此需要将每个token解析其含义,根据树的遍历顺序来以此实现token的含义。
```c
typedef struct {
token_id_t id;
gpointer data;
clone_func_t data_clone_func;
free_func_t data_free_func;
to_string_func_t data_to_string_func;
GPtrArray *children;
} token_t;

token_t * token_new(token_id_t id, gpointer data, clone_func_t data_clone_func,
free_func_t data_free_func, to_string_func_t data_to_string_func);

token_t *token_new_gstring(token_id_t id, GString *string);

token_t *token_new_no_data(token_id_t id);

token_t *token_clone(token_t *token);
```
以上是解释器对于token这一类型的定义以及生成这一类型的接口。需要注意的是本程序完全用C语言编写,故需要设计多个接口方便编码。

###变量设计
当语法树中的Token被解析出来之后,解释器需要运行它们。这时就必须要设计一些抽象的概念来完成它们。因此我们需要设计Variable,也就是被解释语言KiScript里的变量。变量一共有八种类型。其中除了比较常见的变量类型以外,解释器还把函数也看做一种变量。
```C
typedef enum {
VARIABLE_UNDEFINED,
VARIABLE_NULL,
VARIABLE_BOOL,
VARIABLE_NUMERICAL,
VARIABLE_STRING,
VARIABLE_OBJECT,
VARIABLE_EXCEPTION,
VARIABLE_FUNC
} variable_type_t;
```
设计出来的Variable的结构如下:
```C
typedef struct {
variable_type_t variable_type;
gpointer variable_data;
token_t *function_token;
activation_record_t *AR;
gboolean new_flag;
} variable_t;
```
其中`variable_type`指定变量当前的类型;`variable_data`指向所对应类型的数据;`function_token`和`AR`是函数变量特有的,`function_token`指向语法树内,`AR`则指向函数在声明时所在的AR;`new_flag`则是迎合GHashTable内存自主释放的机制而设计的标记,表示当前这个variable的哈希表是否已被引用过。

解释器对于每一类Variable都实现了方法进行构造:
```C
variable_t *variable_new (variable_type_t variable_type, gpointer variable_data, activation_record_t *AR);
variable_t *variable_null_new ();
variable_t *variable_undefined_new ();
variable_t *variable_bool_new (gpointer bool_data);
variable_t *variable_numerical_new (gpointer numerical_data);
variable_t *variable_string_new (gpointer string_data);
variable_t *variable_object_new ();
variable_t *variable_exception_new (gpointer exception_data);
variable_t *variable_function_new (gpointer function_data, activation_record_t *AR);
variable_t *variable_clone (variable_t *variable);
```

###返回结构
考虑到解释进行的过程其实就是一个遍历语法树的过程。每次解析一个token的时候,都会先把它的子树遍历一遍。子树运行完之后需要返回一个结构用于表示子树的运行情况,这部分便是return_struct,即返回结构。该部分的结构如下。
```C
typedef struct {
return_status_t status;
variable_t *mid_variable;
variable_t *end_variable;
} return_struct_t;
```
其中`status`为返回结构的类型,这暗示子树的返回方式。`status`共有以下几种类型。
```C
typedef enum {
STAUS_UNDEFINED,
STAUS_NORMAL,
STAUS_THROW,
STAUS_CONTINUE,
STAUS_BREAK,
STAUS_RETURN,
} return_status_t;
```
可以看出,除了一般的返回状态以外,解释器还要考虑循环语句的break和continue返回状态,异常的扔回状态等,从而完善被解释代码的丰富性。

###AR设计
为了实现控制每个变量和函数的作用域,在解释语言的时候必须要注意Activation Record,即AR。解释器中的AR结构如下:
```c
typedef struct _activation_record{
struct _activation_record *dynamic_link;
struct _activation_record *static_link;
GHashTable *AR_hash_table;
} activation_record_t;
```
`dynamic_link`指向上一个scope的AR,`static_link`指向全局AR,而每个AR的本质就是一个哈希表,用于储存变量名与变量指针这一键值对。

##程序功能
###基本表达式与赋值
程序可以实现对变量与常值之间的基本表达式和赋值语句的解释。这些基本表达式包括一般的二元运算,如数值之间的+-*/运算与<,>运算,字符串的+运算,布尔值之间的逻辑运算,整数的&,|,<<,>>位运算,还有+=,-=等二元赋值表达式等;也包括一般的一元运算,如逻辑非!,自增/减符号++和--,负号-等;还包括一个三元运算符?:。

此外,程序还可解释一些特殊的表达式,如下表:

| 特殊表达式 | |
| :-------- | -----------: |
| 1 + true | true + false |
| 1 + null | 1 + "123" |
| null + "123" | 12 < "2" |
| 12 < "23" | "12" < "2" |
| 12 < "23#" | a="35"; ++a; |
| a=null; ++a; | a=true; ++a; |
| -true; |

>测试数据运行结果截图:
>

Simple_Statement


>

###if语句
程序可以实现对条件判断语句if(){}的解释,其中()中间的表达式不得为空,{}语句块必须要用花括号进行包括,不得为单句表达式。

>测试数据运行结果截图:

>

###for/while循环语句
程序可以实现对循环语句for(;;){}和while(){}的解释,其中while语句的括号内和for语句的括号内第二项的表达式不得为空,{}语句块必须要用花括号进行包括,不得为单句表达式。

>测试数据运行结果截图:
>

>

###数组
程序可以实现对数组的定义与数组使用语句的解释,并且支持数组的下标访问。需要注意的是,解释器不会自主检查数组越界问题,若无严重问题则会照常运行。

>测试数据运行结果截图:
>



>

###函数、递归与闭包
程序可以实现对函数、递归以及闭包语句的解释。
>测试数据运行结果截图:
>



>



>

###对象
程序可以实现对对象表达式的解释。
>测试数据运行结果截图:

>

###类与继承
Javascript的类与继承特性主要体现在每个类的prototype对象以及每个对象的__proto__成员实现继承,对象在调用成员的时候回根据__proto__产生的链找到对应的函数,实现继承。

所有的类对象指向Function,所有的对象指向Object.prototype。由于篇幅具体标准参考ES 5.1 spec。
####类的构造方法
声明函数即为声明类。函数声明的同时会产生函数名.prototype对象。当使用new新建一个类实例的时候,实例的__proto__字段会指向当前类对象的prototype成员。注意这里所有对象的成员都可以动态绑定。
####this
this为对象成员函数访问对象的唯一方法,而由于对象成员是动态绑定的,成员函数只有在调用的时候才会确定this指向的对象。除此之外当函数当做构造函数使用的时候,this绑定的是所返回的新对象,通过this可以对新对象进行构建。

当函数没有作为成员函数被调用的时候,在浏览器里其指向的是全局的window,由于我们的解释器并不存在这样的东西,在这里使用了全局的AR来代替,即this指向全局的AR。
####create
create(proto)为Object类的成员函数,用来创造一个新的__proto__指向proto参数的Object对象。所有的类由于__proto__都会指向Object,所以所有的类都能通过prototype chain找到所继承的函数来创建相应的新的对象实例。
####call
call(obj)为Function的一个成员函数,其作用为使用当前call绑定的类对象来初始化参数里面的对象。

>测试数据运行结果截图:
>



>

###字符串的应用
程序实现了对部分字符串函数与参数的解释。可被解释的函数或参数有length、substr()和split()函数。需要注意的是,这部分的解释实现全部是由JS编写的builtin交由解释器实现的。
>测试数据运行结果截图:

>

###REPL
解释器默认输入为文件输入。为了设计更加人性化,程序还设计了文件输入和交互输入切换的功能。在文件中输入**REPL();**函数即可将解释器转化为交互输入,其输入模式与Python相似,均为逐行解释,暂不支持多行输入。
>测试数据运行结果截图:

>