Skip to content

Atom Extensions ​

在Auto语言v0.2之前的设计中,Atom格式只是用来传递数据的格式,类似于JSON/XML。

在考虑引进ASTL之后,我们需要把Atom格式的ASTL当作一门语言来处理, 这就需要提供新的语法,可以直接(语法糖或新语法解构)或间接(宏)来扩展Atom格式的语法。 另外,我们还需要引入一些新的规则,来简化Atom格式,提高它的可读性和易用性。

总而言之,Atom不再只是给机器用,而是也可以给人用。

简化规则 ​

标准严格的Atom是比较繁琐的,这是因为没有引入任何简化规则,每一个信息都要写上。

例如,在astl.md文档中,我们用标准Atom格式来表达一段代码的语法树:

astl
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
        }
      }
    }
  }
}

作为数据,这样的固定层次,固定格式的数据,非常利于机器解析。 但它本身的信息冗余量太高了,和原本的编程语言相比,可读性差了一个档次:

c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

Auto语言更简略:

Needs modules

This example depends on multi-file modules and cannot run inline — open it in the Playground

auto
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格式也应当是通用的,因此它的隐含规则应当是通用的,且不依赖于任何具体编程语言。

我们的简化目标如下:

astl
use.c {
  sys "stdio.h"
}

fn main int {
  call printf ("Hello, World!")
  ret 0
}

要从前面的冗长的Atom格式简化为这样的Atom格式,需要哪些隐藏规则呢?

我们可以把某些子属性提升到上一层,作为参数来表示,这样不但更简单,解构也更清晰:

例如:

astl
fn {
  name: "add"
  return: int
  args: [
    arg {
      name: "a"
      type: int
    }
    arg {
      name: "b"
      type: int
    }
  ]

  body: [
    // ...
  ]
}

可以添加如下隐藏规则:

  1. name作为主属性,可以直接写在fn后面,做成类似于函数声明的语法结构。

这样上面的代码就变成了:

astl
fn main {
  return: int
  args: [
    arg a {
      type: int
    }
    arg b {
      type: int
    }
  ]
  body: {
    // ...
  }
}

同理,arg里的a和b也被提上来了。

我们再添加一条规则:

  1. return和type作为副属性,可以放在主属性后面,类似于auto语言中的变量类型或函数返回类型:
astl
fn main int {
  args: [
    arg a int {}
    arg b int {}
  ]
  
  body {
    // ....
  }
}

我们再加两条规则:

  1. args可以提升到上一层,用()表达。
  2. 如果一个节点内容为空,在不产生歧义的前提下,可以省略掉后面的{}符号。

由于()在这个位置具有唯一性,内容必然是arg类型,因此arg关键字也可以省略掉:

astl
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设计两种不同层次的规范:

  1. 通用规范,对所有节点都适用
  2. 节点规范,仅对单个类型的节点适用。

我们看看上一节添加的几个简化规则应该如何来编写:

  1. name作为主属性,可以直接写在fn后面,做成类似于函数声明的语法结构。
  2. return和type作为副属性,可以放在主属性后面,类似于auto语言中的变量类型或函数返回类型:
  3. args可以提升到上一层,用()表达。
  4. 如果一个节点内容为空,在不产生歧义的前提下,可以省略掉后面的{}符号。

从这几个规则里,可以总结出如下的通用规范:

  1. 对任意节点,主属性可以提升到上一层,作为节点名称后的第一个名称。即主名称。
  2. 对任意节点,参数列表可以提上到上一层,用()表达。
  3. 对任意节点,副属性可以提升到上一层,放在主名称和参数列表之后。
  4. 对任意节点,如果内容为空,在不产生歧义的前提下,可以省略掉豁免的{}符号。

注意,这里产生歧义的地方,是node (args) {}如果省略掉{},得到的是node (args),很容易和”函数调用“语法混淆。 不过在纯粹的Atom格式中,由于所有的函数调用表达式都已经通过求值计算出了结果,因此不存在此歧义。

这些规范不容易用Atom格式本身来表达,因此我么把它们写在Atom的解析器中。 这个方法类似于C语言的编译器本身写入的语法规则。

未来等Auto语言足够强大,我们也会进一步考虑如何把这些隐式的规则,改写成显示的规范。

对于特定的节点,哪些属性是”主属性“,哪些是”副属性“,哪些子属性可以当作参数,这些具体的规则, 就需要针对每个不同的节点类型来声明了。这就是具体的”节点规范“(Node Schema).

假设我么需要对fn这个节点类型添加规则,可以这么写:

auto
node fn {
  @primary
  name str

  @secondary
  return type

  @args
  args []prop

  @kids
  body []node
}

这段规范的意思是:

对于fn节点:

  • 它的name属性是主属性
  • 它的return属性是副属性
  • 它的args属性是一个参数列表
  • 它的body属性是一个子节点列表,并且是fn节点的默认展开点

这样写的好处是清晰明了,但需要我们对Auto语言添加两个扩展:

  1. 需要添加新的语言特性:支持标注格式@note,并增加@primary等特殊标注的支持
  2. 添加新的node语法结构。

我们发现node的语法结构和type类型声明结构很像, 因此考虑直接扩展type语法结构来实现它。

即,任何用type来定义的自定义类型,都可以用类似节点定义的语法来新建该类型的实例。

这样的好处是,大部分自定义类型不再需要专门设计“构造函数”。而且节点定义的语法总体比C语言的结构体实例化的语法要更可读。

这样,上面的规范可以改写为:

auto
type fn {
  @primary
  name str

  @secondary
  return type

  @args
  args []prop

  @kids
  body []node
}

如果不想用标注来实现这个规范,我们可以给type定义本身添加几个隐藏规则:

  1. type定义的成员列表中,第1个成员就是主属性。
  2. type定义的成员列表中,第2个成员是副属性。
  3. 名称为body成员是子节点展开点。如果一个类型不存在body成员,则不存在子节点(也称为叶节点)。
  4. body属性之外的所有成员都可以作为参数,其顺序和成员定义的顺序一致。

如果未来有更复杂的节点规范,我们再考虑在Auto语言里添加标注特性来支持。

这样的规则下,对任意类型,我们都可以用节点的语法来实例化:

auto
type Point {
  x float 
  y float
}

fn main() {
  let p = Point(1, 2)
}

这里的Point(1, 2)实际上并需要单独定义一个Point构造函数,而是一个节点实例化,只不过{}省略掉了而已。

注意,当主属性或副属性被提升到参数列表之外时,参数列表里需要去掉他们。

auto
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的形式来提高可读性:

auto
let bob = student ("Alice", Female, age: 19, rank: 10)

这个示例里名字和性别都显而易见,不需要标注,而age和rank类型相同,不容易区分,加上了参数名称之后就很清晰了。

最后,主属性提升后还有一个隐藏作用,那就是相当于定义了一个新的局部变量:

auto
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树状结构的访问机制。

有两种不同的方案:

  1. 绝对路径,从整棵树的根节点开始,向下寻找目标。
  2. 相对路径,从当前节点开始,通过转折的形式寻找目标。

这样的访问路径,和文件路径很像。

文件路径的访问方案如下:

  • 用.表示当前目录
  • 用..表示上级目录
  • 用/分隔目录,其中/开头表示根目录
  • 用*与名称字符串表示文件名的匹配

XPath使用的方案如下:

  • 用/分隔不同级别
  • 用.表示相对路径的当前节点
  • 用..表示父节点
  • 用//表示深层访问
  • 用[expr]进行筛选
  • 用@获取属性

QuerySelector采用的则是CSS Selector。

  • 用元素名称来筛选,如input表示所有的&lt;input&gt;节点
  • 用.class-name来按照class属性进行筛选
  • 用#id来通过元素ID直接访问
  • 用[attr]来进行属性筛选。
  • 用,,,&gt;,~,+,||等符号进行不同类型的组合筛选
  • 用:进行Pseudo筛选

这两套方案各有优劣:

  • XPath既支持从根节点搜索,也支持从当前节点搜索,且能够向上访问。
    • QuerySelector只支持从根节点或当前节点向下搜索,不能向上访问。
  • XPath的属性筛选器里可以调用函数,进行字符串匹配等复杂操作
    • QuerySelector的属性选择器使用几种固定实现的选择形式,不如XPath的筛选器灵活
  • XPath只能按照节点名称筛选,没有.class-name和#id
    • 后者在QuerySelector中是最常用的筛选器,且性能较高
  • QuerySelector的筛选器组合非常灵活

Atom的访问器综合两者的优势,提出如下需求:

  • 既可以向下访问,也可以向上访问,且两个方向都可以进行深层访问
  • 可以通过节点类型,主属性和副属性来进行筛选。
  • 灵活的属性筛选器,内部可以调用任何函数
  • (高阶)支持筛选器组合,具体方案参考QuerySelector。
  • 可以利用Auto语言的变量等语言特性迅速定位节点。
  • 为访问器设计专门的语法块,而不止是用字符串来表示。

语法糖 ​

扩展 ​

宏 ​

Released under the MIT License.