Atom Extensions
在Auto语言v0.2之前的设计中,Atom格式只是用来传递数据的格式,类似于JSON/XML。
在考虑引进ASTL之后,我们需要把Atom格式的ASTL当作一门语言来处理, 这就需要提供新的语法,可以直接(语法糖或新语法解构)或间接(宏)来扩展Atom格式的语法。 另外,我们还需要引入一些新的规则,来简化Atom格式,提高它的可读性和易用性。
总而言之,Atom不再只是给机器用,而是也可以给人用。
简化规则
标准严格的Atom是比较繁琐的,这是因为没有引入任何简化规则,每一个信息都要写上。
例如,在astl.md文档中,我们用标准Atom格式来表达一段代码的语法树:
stmt {
use {
kind: "c-include" // due to include
items: [
item {
name: "stdio.h"
kind: "sys" // due to `<..>`
}
]
}
}
stmt {
fn {
name: "main"
return: int
args: []
body {
stmt {
kind: call
call {
name: printf
args: ["Hello, World!\n"]
}
}
stmt {
kind: return
return {
value: 0
}
}
}
}
}作为数据,这样的固定层次,固定格式的数据,非常利于机器解析。 但它本身的信息冗余量太高了,和原本的编程语言相比,可读性差了一个档次:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}Auto语言更简略:
This example depends on multi-file modules and cannot run inline — open it in the Playground
use c <stdio.h>
fn main {
print("Hello, World!")
}如果我们希望能够直接读写Atom格式的ASTL,就需要能把它简化到C或Auto语言差不多的层次。
那么为什么C和Auto语言的代码,比他们所代表的ASTL简化那么多呢? 这是因为C语言和Auto语言隐含了很多语法规则,这些规则写在编译器里,而不用在代码中直接表达出来, 因此就比Atom格式的ASTL要简单多了。后者包含了所有的信息。
因此,要简化Atom格式,就需要引入类似于C/Auto语言的隐含规则。 于C/Auto不同,ASTL本身是跨语言的,Atom格式也应当是通用的,因此它的隐含规则应当是通用的,且不依赖于任何具体编程语言。
我们的简化目标如下:
use.c {
sys "stdio.h"
}
fn main int {
call printf ("Hello, World!")
ret 0
}要从前面的冗长的Atom格式简化为这样的Atom格式,需要哪些隐藏规则呢?
我们可以把某些子属性提升到上一层,作为参数来表示,这样不但更简单,解构也更清晰:
例如:
fn {
name: "add"
return: int
args: [
arg {
name: "a"
type: int
}
arg {
name: "b"
type: int
}
]
body: [
// ...
]
}可以添加如下隐藏规则:
name作为主属性,可以直接写在fn后面,做成类似于函数声明的语法结构。
这样上面的代码就变成了:
fn main {
return: int
args: [
arg a {
type: int
}
arg b {
type: int
}
]
body: {
// ...
}
}同理,arg里的a和b也被提上来了。
我们再添加一条规则:
return和type作为副属性,可以放在主属性后面,类似于auto语言中的变量类型或函数返回类型:
fn main int {
args: [
arg a int {}
arg b int {}
]
body {
// ....
}
}我们再加两条规则:
args可以提升到上一层,用()表达。- 如果一个节点内容为空,在不产生歧义的前提下,可以省略掉后面的
{}符号。
由于()在这个位置具有唯一性,内容必然是arg类型,因此arg关键字也可以省略掉:
fn main (a int, b int) int {
body {
}
}再加一条规则,body节点作为默认的子结构展开节点,可以忽略掉名称:
fn main (a int, b int) int {
// ... body contents
}添加了这几条规则之后,ASTL仍然是Atom格式,但比标准的类JSON的树状格式就简略太多了。
这种简化规则,人可以通过学习来理解,那么机器怎么理解呢? 换句话说,如果让机器来解读上面的简化代码,如何恢复成冗长但无信息遗漏的标准Atom格式?
在C和Auto语言里,这些规则(即程序语言本身的”语法“规则和”语义“规则),是写在编译器里的。 在Atom/ASTL里,我们把这些规则写在另一个地方,称为Atom的语法”规范“(Syntax Scheme)。
语法规范(Syntax Scheme)
我们给Atom设计两种不同层次的规范:
- 通用规范,对所有节点都适用
- 节点规范,仅对单个类型的节点适用。
我们看看上一节添加的几个简化规则应该如何来编写:
name作为主属性,可以直接写在fn后面,做成类似于函数声明的语法结构。return和type作为副属性,可以放在主属性后面,类似于auto语言中的变量类型或函数返回类型:args可以提升到上一层,用()表达。- 如果一个节点内容为空,在不产生歧义的前提下,可以省略掉后面的
{}符号。
从这几个规则里,可以总结出如下的通用规范:
- 对任意节点,主属性可以提升到上一层,作为节点名称后的第一个名称。即主名称。
- 对任意节点,参数列表可以提上到上一层,用
()表达。 - 对任意节点,副属性可以提升到上一层,放在主名称和参数列表之后。
- 对任意节点,如果内容为空,在不产生歧义的前提下,可以省略掉豁免的
{}符号。
注意,这里产生歧义的地方,是node (args) {}如果省略掉{},得到的是node (args),很容易和”函数调用“语法混淆。 不过在纯粹的Atom格式中,由于所有的函数调用表达式都已经通过求值计算出了结果,因此不存在此歧义。
这些规范不容易用Atom格式本身来表达,因此我么把它们写在Atom的解析器中。 这个方法类似于C语言的编译器本身写入的语法规则。
未来等Auto语言足够强大,我们也会进一步考虑如何把这些隐式的规则,改写成显示的规范。
对于特定的节点,哪些属性是”主属性“,哪些是”副属性“,哪些子属性可以当作参数,这些具体的规则, 就需要针对每个不同的节点类型来声明了。这就是具体的”节点规范“(Node Schema).
假设我么需要对fn这个节点类型添加规则,可以这么写:
node fn {
@primary
name str
@secondary
return type
@args
args []prop
@kids
body []node
}这段规范的意思是:
对于
fn节点:
- 它的
name属性是主属性- 它的
return属性是副属性- 它的
args属性是一个参数列表- 它的
body属性是一个子节点列表,并且是fn节点的默认展开点
这样写的好处是清晰明了,但需要我们对Auto语言添加两个扩展:
- 需要添加新的语言特性:支持标注格式
@note,并增加@primary等特殊标注的支持 - 添加新的
node语法结构。
我们发现node的语法结构和type类型声明结构很像, 因此考虑直接扩展type语法结构来实现它。
即,任何用type来定义的自定义类型,都可以用类似节点定义的语法来新建该类型的实例。
这样的好处是,大部分自定义类型不再需要专门设计“构造函数”。而且节点定义的语法总体比C语言的结构体实例化的语法要更可读。
这样,上面的规范可以改写为:
type fn {
@primary
name str
@secondary
return type
@args
args []prop
@kids
body []node
}如果不想用标注来实现这个规范,我们可以给type定义本身添加几个隐藏规则:
type定义的成员列表中,第1个成员就是主属性。type定义的成员列表中,第2个成员是副属性。- 名称为
body成员是子节点展开点。如果一个类型不存在body成员,则不存在子节点(也称为叶节点)。 body属性之外的所有成员都可以作为参数,其顺序和成员定义的顺序一致。
如果未来有更复杂的节点规范,我们再考虑在Auto语言里添加标注特性来支持。
这样的规则下,对任意类型,我们都可以用节点的语法来实例化:
type Point {
x float
y float
}
fn main() {
let p = Point(1, 2)
}这里的Point(1, 2)实际上并需要单独定义一个Point构造函数,而是一个节点实例化,只不过{}省略掉了而已。
注意,当主属性或副属性被提升到参数列表之外时,参数列表里需要去掉他们。
type student {
name str
gender Gender
age int
rank int
}
fn main() {
// 提升主属性
let john = student John (17, 21, Male)
// 两个属性都提升
let jane = student Jane Female (17, 5)
// 两个都不提升
let alice = student ("Alice", Female, 19, 10)
}当然,这个示例中,这类提升显得并没有太大作用。这是因为对于student这类概念来说, “副属性”并不太好理解。所以对于这类概念,我们一般只用主属性。
主属性一般是“名称”或“ID”之类,往往可以用来唯一确定一个实例的属性。 而副属性比较适合那种在有了主属性之后,用来给实例分类的属性,例如kind,return_type,category之类。 尤其是kind或return_type这类属性,提升之后和Auto语言的变量类型\函数类型很像,符合直觉。
另外,节点的参数列表可以用key:value的形式来提高可读性:
let bob = student ("Alice", Female, age: 19, rank: 10)这个示例里名字和性别都显而易见,不需要标注,而age和rank类型相同,不容易区分,加上了参数名称之后就很清晰了。
最后,主属性提升后还有一个隐藏作用,那就是相当于定义了一个新的局部变量:
fn main() {
let John = student ("John", Male, 17, 21)
print(John.rank)
student Bob (Male, age: 19, rank: 41)
print(Bob.rank)
}这两种写法的效果是一样的。
展望:如果我们继续下去,通过简化或扩展来强化ASTL的特性,慢慢把它做到和Auto语言本身的形式几乎一致, 那时候就相当于实现了语言自举了。
换句话说,如果ASTL和Auto语言通过渐近的过程走向统一,那Auto语言本身就实现了和各种语言的转译, 届时Auto语言就成为了一种真正的”通用语言“。
访问(Query)
为了方便使用,我们需要扩展ASTL的语法,让它可以方便地访问Atom中不同位置的数据。 在XML中,有XPath这种访问工具。在HTML中,有querySelector这种访问机制。
Atom作为和XML类似的结构化数据,也应当有方便的类似的Query访问机制。
实现访问机制,有两套方案:
- 内置机制:在Auto语言甚至ASTL内直接支持访问机制。有两个路线,语言路线和库函数路线。
- 外置机制:在Atom的解释器里添加访问机制,这需要在Rust里实现
atom::query库。
考虑到ASTL未来的目标是和Auto语言双向奔赴,我决定采用内置机制方案。 而语言路线和库函数路线,我倾向于语言路线,即添加一种新的语法来表达访问(query)操作。
访问语法
其实任何语言中本身已经有一部分语法支持数据的动态访问,最常见的就是通过变量名来访问数据。
在ASTL中,我们也可以通过变量名称来访问当前视野内,以及往上层递归的一层层视野内的数据。
但是这种访问机制是单线向上的,并不能用来访问整棵语法树中其他分支的深层节点。
而且变量也只是每一层中的少数数据(用存量语句或节点实例化语句得到的数据可以通过变量名来访问), 大部分其他数据也缺少访问机制。
我们需要一种更通用,且适合ASLT树状结构的访问机制。
有两种不同的方案:
- 绝对路径,从整棵树的根节点开始,向下寻找目标。
- 相对路径,从当前节点开始,通过转折的形式寻找目标。
这样的访问路径,和文件路径很像。
文件路径的访问方案如下:
- 用
.表示当前目录 - 用
..表示上级目录 - 用
/分隔目录,其中/开头表示根目录 - 用
*与名称字符串表示文件名的匹配
XPath使用的方案如下:
- 用
/分隔不同级别 - 用
.表示相对路径的当前节点 - 用
..表示父节点 - 用
//表示深层访问 - 用
[expr]进行筛选 - 用
@获取属性
QuerySelector采用的则是CSS Selector。
- 用元素名称来筛选,如
input表示所有的<input>节点 - 用
.class-name来按照class属性进行筛选 - 用
#id来通过元素ID直接访问 - 用
[attr]来进行属性筛选。 - 用
,,,>,~,+,||等符号进行不同类型的组合筛选 - 用
:进行Pseudo筛选
这两套方案各有优劣:
XPath既支持从根节点搜索,也支持从当前节点搜索,且能够向上访问。QuerySelector只支持从根节点或当前节点向下搜索,不能向上访问。
XPath的属性筛选器里可以调用函数,进行字符串匹配等复杂操作QuerySelector的属性选择器使用几种固定实现的选择形式,不如XPath的筛选器灵活
XPath只能按照节点名称筛选,没有.class-name和#id- 后者在
QuerySelector中是最常用的筛选器,且性能较高
- 后者在
QuerySelector的筛选器组合非常灵活
Atom的访问器综合两者的优势,提出如下需求:
- 既可以向下访问,也可以向上访问,且两个方向都可以进行深层访问
- 可以通过节点类型,主属性和副属性来进行筛选。
- 灵活的属性筛选器,内部可以调用任何函数
- (高阶)支持筛选器组合,具体方案参考
QuerySelector。 - 可以利用Auto语言的变量等语言特性迅速定位节点。
- 为访问器设计专门的语法块,而不止是用字符串来表示。