MyLang 是一种简单的教育编程语言,受到Python
、 JavaScript
和C
启发,作为个人挑战而编写,主要是为了在短时间内享受编写递归下降解析器的乐趣并探索解释器的世界。不要指望一个成熟的脚本语言具有可供生产使用的库和框架。然而, MyLang
有一个最小的内置函数集,它也可以用于实际目的。
MyLang 是用可移植的C++17 编写的:目前,该项目除了标准 C++ 库之外没有任何依赖项。要构建它,如果您安装了GNU make
,只需运行:
$ make -j
否则,只需将所有 .cpp 文件传递给编译器并将src/
目录添加到包含搜索路径中。没有依赖性的最好的事情之一是不需要一次性构建的构建系统。
只需传递BUILD_DIR
选项make
:
$ make -j BUILD_DIR=other_build_directory
如果您也想运行 MyLang 的测试,则只需使用 TESTS=1 进行编译并使用 OPT=0 禁用优化,以获得更好的调试体验:
$ make -j TESTS=1 OPT=0
然后,运行所有测试:
$ ./build/mylang -rt
值得注意的是,虽然像 GoogleTest 和 Boost.Test 这样的测试框架比我们在src/tests.cpp
中的简单测试引擎强大和灵活得多,但它们是外部依赖项。依赖性越少越好,对吗? :-)
描述MyLang
最简单的说法是:一种看起来像 C 的动态 python 语言。也许,学习这种语言的最快方法是查看samples/
目录中的脚本,同时查看下面的简短文档。
MyLang
是一种动态鸭子类型语言,就像Python
一样。如果您了解Python
并且愿意使用{ }
大括号,您将自动能够使用它。没有什么惊喜。字符串是不可变的,就像在Python
中一样,数组可以使用[ ]
定义,就像在Python
中一样,字典也可以使用{ }
定义。该语言还支持使用与Python
相同的[start:end]
语法的数组切片。
据说, MyLang
在几个方面与Python
和其他脚本语言不同:
支持使用const
声明的解析时常量。
所有变量都必须使用var
声明。
变量有一个作用域,就像C
中一样。当在嵌套块中使用var
显式重新声明变量时,支持遮蔽。
所有表达式语句必须以;
结尾就像C
、 C++
和Java
中的那样。
存在关键字true
和false
,但没有boolean
类型。就像在 C 中一样, 0
是 false,其他一切都是true
。然而,字符串、数组和字典都有一个布尔值,就像在Python
中一样(例如,空数组被认为是false
)。 true
内置函数只是整数1
的别名。
赋值运算符=
可以像在C
中一样在表达式内部使用,但由于数组扩展功能,没有逗号运算符之类的东西。
MyLang 支持经典的for
循环和显式的foreach
循环。
MyLang 目前不支持自定义类型。然而,字典支持一个很好的语法糖:除了主要语法d["key"]
之外,对于字符串键,还支持语法d.key
。
变量始终使用var
声明,并存在于声明的作用域中(同时在嵌套作用域中可见)。例如:
# Variable declared in the global scope
var a = 42 ;
{
var b = 12 ;
# Here we can see both `a` and `b`
print ( a , b ) ;
}
# But here we cannot see `b` .
可以使用以下熟悉的语法声明多个变量:
var a , b , c ;
但有一个警告,可能是MyLang
唯一“令人惊讶”的功能:初始化变量不像 C 那样工作。考虑以下语句:
var a , b , c = 42 ;
在本例中,我们不是仅仅声明a
和b
并将c
初始化为 42,而是将所有三个变量初始化为值 42。要将每个变量初始化为不同的值,请使用数组扩展语法:
var a , b , c = [ 1 , 2 , 3 ] ;
常量的声明方式与变量类似,但不能在嵌套作用域中隐藏。例如:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
在MyLang
中,常量在解析时求值,其方式与C++
的constexpr声明类似(但我们讨论的是编译时)。初始化const
时,除了整套const内置函数之外,还可以使用任何类型的文字。例如:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
要了解如何准确地评估常量,请使用-s
选项运行解释器,以在运行脚本之前转储抽象语法树。对于上面的例子:
$ cat > t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
$ ./build/mylang t
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
)
--------------------------
惊讶吗?那么,除了数组和字典之外的常量甚至不会被实例化为变量。它们只是在运行时不存在。让我们使用x
添加一条语句:
$ cat >> t
print(x);
$ cat t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
print(x);
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
CallExpr(
Id("print")
ExprList(
"hello world a,b,c"
)
)
)
--------------------------
hello world a,b,c
现在,一切都应该有意义了。数组和字典也会发生几乎相同的情况,只是后者也在运行时进行实例化,以避免到处都有潜在的巨大文字。考虑以下示例:
$ ./build/mylang -s -e 'const ar=range(4); const s=ar[2:]; print(ar, s, s[0]);'
Syntax tree
--------------------------
Block(
ConstDecl(
Id("ar")
Op '='
LiteralArray(
Int(0)
Int(1)
Int(2)
Int(3)
)
)
ConstDecl(
Id("s")
Op '='
LiteralArray(
Int(2)
Int(3)
)
)
CallExpr(
Id("print")
ExprList(
Id("ar")
Id("s")
Int(2)
)
)
)
--------------------------
[0, 1, 2, 3] [2, 3] 2
正如您所看到的,在初始化常量s
,切片操作已在解析时进行了评估,但这两个数组也在运行时存在。相反,const 表达式上的下标操作会转换为文字。这看起来是一个很好的性能权衡:整数、浮点数和字符串等小值在const 求值期间被转换为文字,而数组和字典(可能很大)在运行时保留为只读符号,但仍然允许对它们的一些操作(例如[index]
和len(arr)
)要进行常量评估。
MyLang
目前仅支持以下(内置)类型:
None none
的类型,相当于Python 的None
。刚刚声明的变量没有分配值, none
值(例如var x;
)。这同样适用于没有返回值的函数。此外,它还被find()
等内置函数用作特殊值,以防失败。
Integer有符号指针大小的整数(例如3
)。
Float浮点数(例如1.23
)。在内部,它是一个长双精度。
字符串类似“hello”的字符串。字符串是不可变的并且支持切片(例如s[3:5]
或s[3:]
或s[-2:]
,与Python
中具有相同的含义)。
数组数组和元组的可变类型(例如[1,2,3]
)。它可以包含不同类型的项目,并且支持可写切片。数组切片的行为类似于副本,但在幕后,它们使用写时复制技术。
字典字典是使用Python
语法定义的哈希映射: {"a": 3, "b": 4}
。使用熟悉的语法d["key-string"]
或d[23]
访问元素,可以使用find()
查找并使用erase()
删除。目前,只有字符串、整数和浮点数可以用作字典的键。 Perks :类似标识符的字符串键也可以使用“member of”语法访问: d.key
。
函数独立函数和 lambda 都具有相同的对象类型,并且可以像任何其他对象一样传递(见下文)。但是,只有 lambda 可以有捕获列表。常规函数无法在 const 求值期间执行,而pure
函数可以。纯函数只能看到常量及其参数。
Exception唯一可以抛出的对象类型。要创建它们,请使用内置的exception()
或其快捷方式ex()
。
条件语句的工作方式与C
中的完全相同。语法是:
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
对于单语句块,可以像C
中一样省略{ }
大括号。 conditionExpr
可以是任何表达式,例如: (a=3)+b >= c && !d
。
当conditionExpr
是可以常量求值的表达式时,整个if语句将被true分支替换,而false分支将被丢弃。例如,考虑以下脚本:
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
它不仅总是打印“yes”,而且在执行此操作之前甚至不需要检查任何内容。检查抽象语法树:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
支持经典的while
和for
循环。
while ( condition ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
for ( var i = 0 ; i < 10 ; i += 1 ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
这里,像上面的情况一样,可以省略{ }
大括号。与C
相比,只有一些差异值得指出:
目前MyLang
中不存在++
和--
运算符。
要声明多个变量,请使用语法: var a, b = [3,4];
或者只是var a,b,c,d = 0;
如果您希望所有变量具有相同的初始值。
要增加多个变量的值,请使用语法: a, b += [1, 2]
。在极其罕见和复杂的情况下,在 for 循环的增量语句中,我们需要使用不同的表达式为每个变量分配一个新变量,请利用赋值中的扩展语法: i, j = [i+2, my_next(i, j*3)]
。
MyLang
使用非常熟悉的语法支持foreach
循环:
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Foreach 循环可用于数组、字符串和字典。例如,迭代字典中的每个<key, value>
对很简单:
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
要仅迭代每个键,只需var k in d
即可。
MyLang
也支持 foreach 循环中的枚举。检查以下示例:
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
换句话说,当容器名称前面带有关键字indexed
时,第一个变量在每次迭代时都会分配一个渐进的数字。
在迭代小型固定大小数组的数组时(考虑元组),可以在 foreach 循环中直接扩展这些“元组”:
var arr = [
[ " hello " , 42 ] ,
[ " world " , 11 ]
] ;
foreach ( var name , value in arr ) {
print ( name , value ) ;
}
# This is a shortcut for :
foreach ( var elem in arr ) {
# regular array expansion
var name , value = elem ;
print ( name , value ) ;
}
# Which is a shortcut for :
foreach ( var elem in arr ) {
var name = elem [ 0 ] ;
var value = elem [ 1 ] ;
print ( name , value ) ;
}
声明一个函数很简单:
func add ( x , y ) {
return x + y ;
}
但也支持一些快捷方式。例如,对于像上面这样的单语句函数,可以使用以下语法:
func add ( x , y ) => x + y ;
此外,虽然始终为无参数函数编写()
是一个好习惯,但它们在这种语言中实际上是可选的:
func do_something { print ( " hello " ) ; }
在MyLang
中,函数被视为常规符号,并且该语言中的独立函数和 lambda 之间没有实质性差异。例如,我们可以将上面的add
函数声明为 lambda:
var add = func ( x , y ) => x + y ;
注意:在表达式中创建函数对象时,我们不允许为它们指定名称。
Lambda 也支持捕获列表,但为了保证清晰度,不支持隐式捕获。当然,lambda 可以像任何其他对象一样返回。例如:
func create_adder_func ( val ) =>
func [ val ] ( x ) => x + val ;
var f = create_adder_func ( 5 ) ;
print ( f ( 1 ) ) ; # Will print 6
print ( f ( 10 ) ) ; # Will print 15
正如任何人所期望的那样,带有捕获的 Lambda 函数有一个state 。考虑以下脚本:
func gen_counter ( val ) => func [ val ] {
val += 1 ;
return val ;
} ;
var c1 = gen_counter ( 5 ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
# Clone the `c1` lambda object as `c2` : now it will have
# its own state , indipendent from `c1` .
var c2 = clone ( c1 ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c2: " , c2 ( ) ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
它生成输出:
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
常规用户定义函数对象(包括 lambda)不被视为const
,因此不能在 const 求值期间运行。这是一个相当强的限制。考虑以下示例:
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
在这种情况下,脚本无法创建sorted_people
const 数组。因为我们将函数对象传递给 const sort()
内置函数,所以我们将收到ExpressionIsNotConstEx
错误。当然,如果sorted_people
被声明为var
,脚本将会运行,但是数组将不再是 const ,并且我们将无法从任何解析时优化中受益。因此,虽然sort()
内置函数可以在 const 求值期间调用,但当它具有自定义compare func
参数时,这就不再可能了。
为了克服刚才描述的限制, MyLang
有一种特殊的纯函数语法。当一个函数在func
之前使用pure
关键字声明时,解释器会以一种特殊的方式对待它:它可以在 const 求值期间和运行时随时调用,但该函数不能看到全局变量,也不能捕获任何内容:它只能使用常量及其参数的值:这正是我们在 const 求值过程中所需要的。例如,要在 const 求值期间生成sorted_people
,只需编写:
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
纯函数可以定义为独立函数,也可以与非常量参数一起使用。因此,如果一个函数可以声明为pure
,那么它应该始终以这种方式声明。例如,考虑以下脚本:
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
语言引擎在运行时使用的抽象语法树将是:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
FuncDeclStmt(
Id("add2")
<NoCaptures>
IdList(
Id("x")
)
Expr04(
Id("x")
Op '+'
Int(2)
)
)
VarDecl(
Id("non_const")
Op '='
Int(25)
)
CallExpr(
Id("print")
ExprList(
CallExpr(
Id("add2")
ExprList(
Id("non_const")
)
)
)
)
CallExpr(
Id("print")
ExprList(
Int(7)
)
)
)
--------------------------
27
7
正如您所看到的,在第一种情况下,会发生实际的函数调用,因为non_const
不是常量,而在第二种情况下,就像我们将文字整数传递给print()
一样。
与其他构造一样, MyLang
具有类似于Python
的异常处理,但使用类似于C++
语法。基本结构是try-catch
语句。让我们看一个例子:
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
注意:如果常量表达式(例如int("blah")
)产生异常,则在 const 求值过程中,将直接报告错误,绕过任何异常处理逻辑。其原因是为了强制实施早期失败。
也允许使用多个catch
语句:
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
而且,如果可以使用相同的代码处理多个异常,也可以使用更短的语法:
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
异常可能包含数据,但目前没有任何内置异常包含数据。可以使用try-catch
块捕获的内置运行时异常列表是:
相反,无法捕获SyntaxErrorEx
等其他异常。在MyLang
中也可以使用 catch-anything 块捕获任何异常:
try {
# body
} catch {
# Something went wrong .
}
目前,该语言不支持自定义类型。因此,不可能像其他语言那样抛出任何类型的对象。要引发异常,需要使用特殊的内置函数exception()
或其快捷方式ex()
。考虑以下示例:
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
正如直觉所示,我们使用ex()
创建并随后抛出了一个名为MyError
异常对象,其中包含1234
作为负载数据。后来,在catch
块中,我们捕获了异常,并使用内置的exdata()
提取了有效负载数据。
如果给定的异常不需要有效负载,则可以将ex()
的结果保存在变量中,然后使用可能更令人愉快的语法抛出它:
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
支持使用专用的rethrow
关键字在 catch 语句主体中重新引发异常:
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
在某些情况下,在执行可能引发异常的代码块后,可能需要进行一些清理。对于这些情况, MyLang
支持众所周知的finally
子句,其工作方式与C#
中完全相同:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
值得注意的是, try-finally
结构(没有任何catch
子句)也是允许的。
当 const 参数传递给以下内置函数时,将在解析时对其进行求值。
defined(symbol)
检查symbol
是否已定义。如果定义了符号,则返回 1,否则返回 0。
len(container)
返回给定容器中的元素数量。
str(value, [decimal_digits])
将给定值转换为字符串。如果value
是浮点数,则第二个参数指示输出字符串中所需的小数位数。
int(value)
将给定字符串转换为整数。如果该值是浮点数,则会被截断。如果该值是字符串,则将对其进行解析并转换为整数(如果可能)。如果该值已经是整数,则将按原样返回。
float(value)
将给定值转换为浮点数。如果该值为整数,则会将其转换为浮点数。如果该值是字符串,如果可能的话,它将被解析并转换为浮点型。如果该值已经是浮点数,它将按原样返回。
clone(obj)
克隆给定的对象。对于数组、字典和带捕获的 lambda 等非平凡对象很有用。
type(value)
以字符串形式返回给定值的类型名称。对于调试很有用。
hash(value)
当value
用作 key 时,返回字典内部使用的哈希值。目前,只有整数、浮点数和字符串支持hash()
。
array(N)
返回一个none
的数组