9 个版本
0.1.8 | 2023年8月4日 |
---|---|
0.1.7 | 2023年8月4日 |
0.0.4 | 2023年3月6日 |
0.0.3 | 2023年2月25日 |
120 在 编程语言 中
150KB
4K SLoC
"Stack Programming Language" =SPL
SPL 是一种简单、简洁的连加脚本语言。
示例
func main { mega | with args ;
"Running with args: " print
args:iter
{ str | " " concat } swap:map
&print swap:foreach
"" println
println <{ "and with that, we're done" }
0
}
"5分钟" SPL:in
-
def
引入一个变量。def a
-
写入一个常量将其推入栈。这适用于字符串和数字。
"Hello, World!"
-
使用
=<name>
将最高值分配给变量。在这种情况下,是 "Hello, World!"=a
-
这可以写为单行 - 换行符始终是可选的,等于一个空格。
def a "Hello, World!" =a
-
变量由两个函数组成:
<name>
和=<name>
。使用<name>
再次获取值。a
-
使用
print
函数打印值。它从栈中取出一个值并打印它而不换行。要带换行符打印,请使用println
。末尾的分号表示 '如果此函数返回任何内容,则丢弃它'。这可以用于字符串使其成为注释,但不适用于数值常量。println;
Hello, World!
-
使用
func
关键字引入一个函数。{ mega |
是返回类型声明,在 SPL 中是在块内完成的。在这种情况下,我们的函数返回一个mega
类型的值,这是一个 128 位整数。func main { mega |
-
下面将解释
with
声明。它定义了args
参数。with args ;
-
现在,我们可以像以前一样编写代码
def list
-
SPL 有一个可变长度的数组类型,列表。要创建任何构造(对象),我们使用
:new
。List:new =list
-
要将元素添加到列表末尾,我们使用
push
。所有构造方法都像之前的new
例子一样,以冒号结尾。"Hello," list:push
注意小写的
list
,因为我们是在变量中的构造体上执行 push 操作。 -
现在,我们也将 "World!" 推送到列表中。
"World" list:push
太棒了!我想现在就打印它,但怎么办呢?
-
我们目前无法直接打印列表(根据我们所知),但我们可以遍历它!
{ | with item ; item print; " " print; } list:foreach; "" println;
这里有很多东西需要解释!
{ |
创建了一个没有返回类型的闭包(在 C 风格语言中,这将是一个空函数)。with item ;
声明参数。这是可选的,如果函数不需要参数,则不需要。运行"a" "b" "c"
并调用一个带有 b c ; 的参数,将每个字母留在相应的变量中。- 我们 already know what print does - it prints the item and a space in this case.
- 分号表示我们不在乎打印的结果。在这种情况下,打印不返回任何内容,但我添加分号只是为了清晰或以防万一。
}
结束闭包,并将其放在我们的栈顶。list:foreach
调用我们的list
上的foreach
方法,该方法使用 callable this ; 声明 - 这意味着我们需要提供与隐含的this
参数(它可以是任何名称 - 解释器根本不在乎名称 -this
只是惯例)的一个参数。这里的callable
不是一个类型!foreach
也不返回任何内容,但我添加分号是为了清晰。- 然后打印一个换行符。
Hello, World!
-
SPL 使用
<lower> <upper> Range:new
创建范围。你可以遍历它们。0 5 Range:new:iter
-
现在,让我们将这些值都乘以 5。
{ mega | 5 * } swap:map
等等,怎么了?为什么方法调用突然不一致,迭代器没有被调用,它现在变成了别的!
确实看起来如此,不是吗?
swap
交换栈顶的两个值。a b -> b a
。这意味着我们实际上是在调用迭代器。在调用之前,闭包和迭代器被交换了。swap:map
是写swap :map
的更简洁方式。迭代器上的 map 函数(通常通过大多数集合构造的
:iter
获取)用于将函数应用于迭代器中的所有项。这里的闭包实际上接受一个参数,但省略了 with 声明。较长版本的代码将是{ mega | with item ; item 5 * }
但是这相当笨拙,所以当参数直接传递给下一个函数时,它们通常只是简单地保留在栈上。代码中的
*
是一个接收两个数字并将它们相乘的函数。同样适用于+
、-
、%
和/
。在其它语言中,a b -
相当于a - b
。而lt
、gt
和eq
用于比较值。返回值只需在函数退出时在栈上留下一些东西即可,返回声明实际上可以省略,但是分号无法确定要丢弃的结构数量,所以除非你绝对确定,否则不应该这样做。在这种情况下,我们绝对确定它永远不会用分号调用,因为映射迭代器除了返回对象之外没有其他用途(这在实践中大多数闭包都是这种情况),因此我们可以省略返回类型声明,并得到
{ | 5 *}
。真 neat! -
我们可以像使用数组一样在迭代器上使用
foreach
。使用_str
将数字转换为字符串。{ | _str println } swap:foreach
0 5 10 15 20
范围包含下限,但不包含上限。它们通常与其他语言的等效伪代码类似使用
for(int i = 0; i < 5; i++) { println((String) i * 5); }
-
SPL实际上并不完全是可拼接的。它还支持后缀参数
println <{ "and with that, we're done" }
这实际上不是一个特殊解释器的功能,而是一个特殊词法分析器的功能。这与非后缀版本100%等效,其中字符串在
println
之前。同样也可以用于对象调用。让我们用后缀重写之前的代码
Range:new <{ 0 5 } :iter :map <{ { | 5 * } } :foreach <{ { | _str println } }
我骗了。现在这不再100%等效了。让我们看看底层发生了什么。
call Range objpush const mega 0 const mega 5 objpop objcall new objcall iter objpush const func 0 const mega 5 call * end objpop objcall map objpush const func 0 call _str call println end objpop objcall foreach
您可以看到现在有
objpush
和objpop
指令。这正在做我们之前示例中swap
所做的事情。然而,swap只能交换最顶部的值,但后缀参数允许任何数量的值。这就是为什么有一个专门为此设计的指令。它也可以通过AST修改来使用,但在正常语言使用中无法获取它,因为它可能会导致解释器恐慌。objpush
和objpop
在名为objcall栈的单独栈上操作,而不是主对象栈。
更多教程内容将在后面提供。
SPL中的Rust嵌入
由于SPL几乎不完整的标准库,因此需要嵌入Rust来执行许多任务。可以按以下方式执行:
> cat rust-test.spl
func main { |
1 rusty-test _str println
0
}
func rusty-test @rust !{
println!("hii");
let v = #pop:Mega#;
#push(v + 1)#;
}
> spl --build rust-test.spl demo
---snip---
> ./spl-demo/target/release/spl-demo rust-test.spl
hii
2
如您所见,这相对直接;但当前有一些主要限制
- 这是一个新二进制文件,不能链接到当前运行的程序
- 目前不能自动添加crates
第二个问题容易解决,但我打算先解决第一个问题。遗憾的是,解决这个问题需要将代码编译为动态库,并且还需要让它与正在运行的程序一起工作。如果有人知道如何正确地这样做,我会非常感谢一个PR或issue,解释它。
依赖项
~54KB