Tnze Programming Language
本项目设计了一种实验性的脚本语言,主要用途是在各种应用程序中作为嵌入式解释器,提供通过自定义脚本调用内部API的功能。
简单来说,就是语法更加现代化的 Lua 语言
语言的设计目标
- 简单易学,没有令人望而却步的各种符号
- 拥有现代化的语法,表达能力强、不罗嗦
- 提供优秀的语言交互接口(FFI)
- 提供“全局异步支持”,所有函数都是异步函数
- 所有设计均符合现代程序员的编程习惯,不搞特殊
- 提供官方的风格化指南,统一代码风格
语言的设计思路
- 提供优秀的类型系统,利于早期发现编程错误
- 脚本依赖宿主的接口都需要显式声明,容易检查宿主环境是否能运行当前脚本
- 函数是一等公民,能正确处理闭包等编程原语
- 基于类型擦除的泛型,作为解释型语言正合适
- 没有默认的“空值”,可能为空的值都需要套Option类型实现,从语法上杜绝空指针异常
- 包含各种常用的类型:
- 布尔值:
bool
- 无符号整数:
u8
,u16
,u32
,u64
,u128
,u256
,u512
- 有符号整数:
i8
,i16
,i32
,i64
,i128
,i256
,i512
- IEEE 754 浮点数:
f8
,f16
,f32
,f64
- 定长数组:
ary<N: usize, T: any>
- 不定长数组:
vec<T: any>
- 字符串:
string
,vec<u8>
- 映射表:
map<K, V>
- 迭代器:
iter<T>
- 积类型与和类型:
struct{}
,union{}
- 布尔值:
- 联合体自带标签,可以自行区分当前是哪一种变体
- 相对丰富的控制流关键字:
if
,for
,match
- 通过
for
关键字统一提供多种循环变体 - 非try-catch方案的错误处理
语言的实现要求
- 解释器易于在多种语言上实现,要提供Rust、Go、C、C#、JS等常见语言的解释器
- 解释器库要对宿主提供简单明了的接口,方便从宿主给脚本提供API
- 解释器能输出良好的错误信息,高效定位问题原因
- 内置模块功能,用于将代码划分为模块,利于项目代码解耦
- 至少能提供马马虎虎、勉勉强强的性能
语言的生态建设手段
- 提供完整的字符串操作工具
- 提供功能强大的官方标准库
Sample Codes
一些示例代码
改变执行流的语法
if
分支
用来创建执行流的分支,像大多数其他语言一样。
规则是条件表达式不需要加小括号,而代码块必须加大括号。
要注意 CONDITION
必须是bool
类型的表达式。
// "if" 表达式
if CONDITION {
EXPRESSION
}
// "if-else" 表达式
if CONDITION {
EXP1
} else {
EXP2
}
// "if-else-if" 表达式
if COND1 {
EXP1
} else if COND2 {
EXP2
} else if COND3 {
EXP3
} else if ...
... else {
EXPN
}
再次强调,if
是一种表达式:
// 三元表达式
if COND { EXP1 } else { EXP2 }
for
循环
各种循环都用 for
一个关键字就够了。
像其他语言一样,你当然可以在循环体里面用 break
和 continue
改变执行流。
While 型循环
我相信你知道这是什么。
要注意 COND
必须是一个bool
类型的表达式
for COND {
// do something
}
Loop 型循环
条件表达式总是 True 的While型循环,你也可以叫它死循环。
可以省略那个 true
for {
// keep doing something
}
For 型循环
就是传统的 For 循环
for STATEMENT; CONDITION; STATEMENT {
// do something
}
Do-While 型循环
对不起,没有设计这个
要用到的时候就这么凑合写吧:
for {
// do something
if COND { break; }
}
For-Each 型循环
这种循环是配合迭代器使用的
for let value : ITERATOR {
print(value);
}
详情请查看专门讲迭代器的章节
表达式求值
既然 if
是表达式,为什么 for
不能也是呢?
Loop 型循环求值
先来看最简单的情况
value = for {
// do something
if COND { break EXPR; }
};
// value == EXPR
这是一个“无限循环”,仅当运行到 break
的时候结束循环。整个表达式的值就等于 break 时提供的值。
其他循环求值
这些循环不需要 break
也会自己结束,你需要给循环自己结束情况指定一个 Python 同款的 else
表达式,大括号是必要的。
value = for COND {
// do something
} else {
EXPR
};
当 COND
条件不成立、循环结束时,就会对 else
代码块求值,并将其作为 for
表达式的值了。
当然,有 else
并不会影响你用 break
。
value = for COND {
// do something
if COND { break EXPR1; }
// do something
} else {
EXPR2
};
match
匹配
一些语言里有 “switch
语句”,而 match
具有相同的功能,但是更加强大。
match EXPR {
PATTERN1 => { EXPR1 }
PATTERN2 => {
EXPR2
}
PATTERN3 | PATTERN4 => {
EXPR3
}
default => { EXPR4 }
}
太好了,不需要break
,我们有救了!
以上代码完全等效于用
==
依次比较的实现:let v = EXPR; if v == PATTERN1 { EXPR1 } else if v == PATTERN2 { EXPR2 } else if v == PATTERN3 || v == PATTERN4 { EXPR3 } else { EXPR4 }
至于是否需要实现 Rust 语言那种更强的功能,以后再说
数据、变量、类型
定义变量到底是用
let
呢,var
呢val
呢,还是:=
呢?好纠结! 算了,就用let
吧。。
想必大家都知道什么是变量,在本语言中,用let
关键字定义一个变量。
具体来说,首先先写一个let
,再跟着变量的标识符,然后写=
,然后是初始化表达式,最后别忘了分号;
。
let IDENTIFIER = INIT_EXPRESSION;
就是这样,很简单。
有些人喜欢说“把值绑定到了标识符上”,也行。
类型
编程的时候不同的数据有不同的类型,字符串是字符串,整数是整数,浮点数是浮点数。
无论是表达式还是变量,能够赋值的前提是二者的类型要兼容。 你不能把一个类型为整数的值赋给一个类型为字符串的变量,否则就会出问题。
定义变量的时候你可以在标识符后面加一个冒号:
,再加一个类型名,指定这个变量的类型。
以下是一个正确的示例,其中i32
是整数类型,string
是字符串类型,稍后会介绍。
let my_number: i32 = 8693;
let my_string: string = "Tnze";
上下交换一下,就变成了一个错误的示例。具体来说,一个语法错误。 你不能把字符串当成整数,也不能把整数当作字符串。
let my_number: i32 = "Tnze"; // 错误
let my_string: string = 8693; // 错误
整数类型
整数的英语叫做 Integer,因此我们把整数类型表示为 i8
、i16
、i32
、i64
、i128
、i256
和i512
。
i
代表整数,后面紧跟着的数字代表计算机用多少个bit
去表示它。
用的bit
越多,能表示的范围越大,i8
只能表示 -128 ~ 127 范围内的整数,而i32
能表示的范围是 -2147483648 ~ 2147483647。
上面这些整数类型都有其对应的“无符号”版本,英文是 Unsigned Integer,用 u8
、u16
、u32
、u64
、u128
、u256
、u512
表示。
这里的“符号”指的就是负号,因此无符号整数全都是非负数。u8
的表示范围是 0 ~ 255,而 u32
表示范围是 0 ~ 4294967295。
如果你看不懂上面在说什么,请用搜索引擎查找“计算机表示整数的方法”、“二进制”、“原码、反码和补码”
浮点数类型
所谓浮点数,就是有小数点的数,并且小数点还会“浮动”。
什么意思呢?就是说我们已经有了整数,那么怎么表示一个小数呢?
很简单,用两个整数,第一个整数告诉我们这个数是什么,第二个整数告诉我们小数点在哪里就可以了。
例如我要表达
1.414
这个小数,那么我只要让第一个数为1414
,第二个数为1
,不就可以表示“小数点在第一个数字后面”了吗。欸,这不就是科学计数法么。。。
基于上述思想,业界就制定了一个叫做 IEEE 754 的标准,旨在用二进制的科学计数法来表示二进制的小数,也就是我们常说的浮点数。
与整数一样,浮点数也有不同bit
数量的版本,我们分别表示为 f8
、f16
、f32
、f64
。
其中f32
一般称作“单精度浮点数”,而f64
一般称作“双精度浮点数”。
比较少用到的f16
被称作“半精度浮点数”,f8
被称作“四分之一精度浮点数”。
字符串类型
字符串类型名称是 string
。
它其实是一个 u8
的不定长数组,内部数据必须是有效的 UTF-8 编码字符串。
由于它是一个不定长数组,因此长度信息是储存在不定长数组结构里的,字符串结尾并不保证有一个\0
结束标记。
结构体类型
struct {
Foo: i32,
Bar: string,
}
联合体类型
和C语言的联合体不同,本语言的联合体是一种 Tagged Union。 一个联合体类型的值内部有一个隐藏的成员,指示当前到底是哪一种变体。
union {
Foo: i32,
Bar: string,
}
数组类型
数组类型分两种,固定长度的与动态长度的。固定长度的数组叫做ary
,而动态长度的数组叫做vec
固定长度的数组具有两个类型参数,元素类型T
与数组长度N
:ary<T, N>
动态长度的数组只有一个类型参数:vec<T>
数组的创建
数组通过大括号初始化:
let my_array: ary<i32, 5> = ary<i32, 5>{1, 2, 3, 4, 5};
let my_vector: vec<u8> = vec<u8>{6, 7, 8, 9, 10, 11, 12};
数组的访问
通过方括号运算符访问数组元素,下标从0开始
let a = my_array[0];
let b = my_vector[1];
my_array[2] = 8192;
my_vector[3] = 0;
数组类型的转换
固定长度数组与动态长度数组可以互相转换。 当动态长度数组转换为静态长度数组时,如果长度不匹配则会出错
let foo = ary<i32, 3>{0, 0, 0};
let bar = vec<i32>(foo);
bar.push(1);
let foo2 = ary<i32, 4>(bar); // {0, 0, 0 1}
函数类型
详情请看专门讲函数的章节
运算
提供一组基本的运算符,可用于构建表达式。 运算符实际代表的运算与其应用在什么类型的数据上有关。
数字的加减乘除
包括 +
, -
, *
, /
。可以应用在所有整数及浮点数类型上。
溢出时,结果取决于具体实现。
对于整数除法,还可以使用%
求余数,像大多数语言一样。
数字的比较
可以用 >
, <
, >=
, <=
比较数字大小。表达式的类型为bool
。
布尔值的运算
逻辑与:&&
,逻辑或:||
,逻辑非:!
。
函数:一等公民
我们把函数和闭包都叫做函数,类型也一样,就像 Go 语言的那样
函数字面量的一般形式如下
fn(param1: T1, param2: T2, ...) -> R {
// do something
}
函数是一个值
你可以把它赋值给一个变量,赋值语句后面要记得加分号
let my_func = fn(param1: T1, param2: T2, ...) -> R {
// do something
};
也可以作为一个函数的参数
func1(fn(param1: T1, param2: T2, ...) -> R {
// do something
})
现在你就可以跟其他语言一样去调用它了
my_func(arg1, arg2);
函数可以递归调用
递归调用就是说在函数的内部调用函数本身
let fib = fn(n: u32) -> u32 {
return if n == 0 {
0
} else if n == 1 {
1
} else {
fib(n - 1) + fib(n - 2)
};
};
这样的语法设计好像会对解释器造成困扰?因为确定
fib
的类型必须先解析后面的函数体, 而解析函数体的时候又要确认fib
的类型。所以应该先解析
fn(n: u32) -> u32
这个函数签名的部分,这样就确定了fib
的类型。 再继续解析剩余的函数体,这样在调用fib
的时候就没有问题了。大概。。。
能想出这样的代码,我怕不是个天才
let func = fn() {
print("foo");
func = fn() { print("bar"); };
};
func(); // 打印foo
func(); // 打印bar
func(); // 打印bar
函数可以捕获外部的值,此时你也可以叫它闭包
所以说闭包就是使用了外部变量的函数,明白了吗
let a = 10;
my_func = fn() {
print(a); // 变量a不是在函数里面定义的,但是却可以使用
a += 1;
};
my_func(); // 打印10
my_func(); // 打印11
a = 20;
my_func(); // 打印20
方法
方法是一种关联到特定类型上的函数
Integrated Entity-Component-System
把 ECS 的编程理念融入语言设计里会怎么样?
Component
组件是一种类型,通过关键字component
定义:
component Pos(x: f32, y: f32); // 有多个值的组件,每个值都要有自己的名字
component Health(u8); // 只有一个值的组件,这个值可以是匿名的
component Player(): Pos, Health; // 组件可以依赖其他组件,
component Enemy(): Pos, Health; // 当add_comp组件时如果它的依赖组件不存在会报错
Entity
实体可以通过entity()
创建,新创建的实体不包含任何组件。
随后可以通过.add_comp<T: component>(...values)
方法添加组件。
player := entity();
player.add_comp<Pos>(x=0.0, y=0.0);
player.add_comp<Health>(20);
player.add_comp<Player>();
Query
可以通过query<...T>()
查询所有“包含泛型参数中全部组件的”实体,返回的是一个迭代器。
随后可以通过.get_comp<T>()
方法获得组件的值。
for e : query<Pos, Health>() {
print(e.get_comp<Pos>().x);
print(e.get_comp<Pos>().y);
print(e.get_comp<Health>());
}
或者把语法设计成这样可以提高性能?
for Pos(x, y), Health(health) : query() {
print(x);
print(y);
print(health);
}