Perl 6 - Rakudo and NQP Internals

标题: Rakudo and NQP Internals
子标题: The guts tormented implementers made
作者: Jonathan Worthington

关于这个课程

Perl 6 是一种大型语言, 包含许多要求正确实现的功能。

这样的软件项目很容易被难控制的复杂性淹没。
Rakudo 和 NQP 项目的早期阶段已经遭受了这样的困难, 因为我们学到了 - 艰难的方式 - 关于复杂性, 出现并可能在实现过程中不受限制地扩散。

本课程将教您如何使用 Rakudo 和 NQP 内部。 在他们的设计中编码是一个大量学习的过程, 关于如何(以及如何不)写一个 Perl 6 实现, 这个过程持续了多年。 因此, 本课程还将教你事情的来龙去脉。

关于讲师

  • 计算机科学背景
  • 选择旅行世界,并帮助实现 Perl 6, 而不是做博士
  • 有不止一种方法来获得"永久头部损伤" :-)
  • 不知何故在 Edument AB 被聘用, 作为讲师/顾问
  • 从 2008 年开始成为 Rakudo Perl 6 核心开发者
  • 6model, MoarVM, NQP 和 Rakudo 各个方面的缔造者

课程大纲 - 第一天

  • 鹰的视角: 编译器和 NQP/Rakudo 架构
  • NQP 语言
  • 编译管道
  • QAST
  • 探索 nqp::ops

课程大纲 - 第二天

  • 6model
  • 有界序列化和模块加载
  • 正则表达式和 grammar 引擎
  • JVM 后端
  • MoarVM 后端

鹰的视角

编译器和 NQP/Rakudo 架构

编译器做什么

编译器真的是"只是"翻译。

编译器把高级语言 (例如 Perl 6) 翻译成低级语言 (例如 JVM 字节码)。

Perl 6 - Rakudo and NQP Internals_第1张图片
input-compiler-output.png

接收直截了当的输入(文本)并产生直截了当的输出(文本或二进制), 但内部的数据结构很丰富

像字符串那样处理东西, 通常是最后的手段

运行时做什么

运行像 Perl 6 这样的语言不仅仅是将它转换为低级代码。 此外, 它需要运行时支持来提供:

  • 内存管理
  • I/O, IPC, OS 交互
  • 并发
  • 动态优化

构建我们需要的东西来构建东西

我们已经以现有的编译器构造技术进行了各种尝试来构建 Perl 6。 编译器的早期设计至少有一部分是基于常规假设的。

这样的尝试是信息性的, 但从长远来看还不够好。

Perl 6 提出了一些有趣的挑战...

Perl 6 是使用 Perl 6 解析的

Perl 6 的标准文法(grammar)是用 Perl 6 写的。它依赖于...

  • 可传递的 最长 token 匹配 (我们会在之后看到更多关于它的东西)
  • 能够在不同语言之间来回切换 (主语言, 正则表达式语言, 引用(quoting)语言)
  • 能够动态地派生出新语言 (新运算符, 自定义引用构造)
  • 在自下而上的表达式解析和自顶向下的更大的结构解析之间无缝集成
  • 保持令人惊叹的错误报告的各种状态

所有这些本质上代表了解析中的新范例。

非静态类型或动态类型

Perl 6 是一种 渐进类型化的语言。

my int $distance = distance-between('Lund', 'Kiev');
my int $time = prompt('Travel time: ').Int;
say "Average speed: { $distance / $time }";

我们想利用 $distance$time 原生整数来产生更好的代码, 如果我们不知道类型(应该只是输出代码中的原生除法指令)。

模糊编译时和运行时

运行时可以做一些编译时:

EVAL slurp @demos[$n];

编译时可以做一些运行时:

my $comp-time = BEGIN now;

注意编译时计算的结果必须持久化直到运行时, 这它们之间可能有一个处理(process)边界!

NQP 作为语言

Perl 6 文法显然需要用 Perl 6 表示。这反过来将需要集成到编译器的其余部分。用 Perl 6 编写整个编译器是很自然的。

然而, 一个完整的 Perl 6 太大, 给它写一个好的优化器花费太多时间。
因此, NQP (Not Quite Perl 6) 语言诞生了:它是 Perl 6 的一个子集, 用于实现编译器。NQP 和 Rakudo 大部分都是用 NQP 写的。

NQP 作为编译器构造工具链

NQP src 目录中不仅仅是 NQP 本身。

  • NQP, how, core: 这些包含 NQP 编译器, 元对象(它指定了 NQP 的类和 roles 的工作方式)和内置函数。
  • HLL: 构造高级语言编译器的通用结构, 在 Rakudo 和 NQP 之间共享。
  • QAST: Q 抽象语法树的节点。代表着程序语法的树节点。(即, 当它执行时会做什么)。
  • QRegex: 解析和执行 regexes 和 grammars 时所提到的对象。
  • vm: 虚拟机抽象层。因为 NQP 和 Rakudo 可以运行在 Parrot, JVM 和 MoarVM 上。

QAST

QAST 树是 NQP 和 Rakudo 内部最重要的数据结构之一。

抽象语法树表示程序在执行时执行的操作。它是抽象的意思是从一个程序被写入的特定语言中抽象出来。

Perl 6 - Rakudo and NQP Internals_第2张图片
example-qast.png

QAST

不同的 QAST 节点代表着像下面这样的东西:

  • 变量
  • 运算 (算术, 字符串, 调用, 等等.)
  • 字面值
  • Blocks

注意像类那样的东西没有 QAST 节点, 因为那些是编译时声明而非运行时执行。

nqp::op 集合

编译器工具链的另一个重要部分是 nqp::op 指令集。你会有两种方式遇到它, 并且了解它们之间的差异很重要!

您可以在 NQP 代码 中使用它们, 在这种情况下, 您说您希望在程序中的那个点上执行该操作:

say(nqp::time_n())

在代表着正在编译的程序的 QAST树 中也使用完全相同的指令集:

QAST::Op.new(
    :op('call'), :name('&say'),
    QAST::Op.new( :op('time_n') )
)

Bootstrapping in a nutshell

人们可能会想知道 NQP 几乎完全用 NQP 编写时是如何编译的。

在每个 vm 子目录中都有一个 stage0 目录。它包含一个编译的 NQP(Parrot上是PIR文件, JVM上是JAR文件等)然后:

Perl 6 - Rakudo and NQP Internals_第3张图片
nqp-bootstrapping-stages.png

因此, 你 make test 的 NQP 是可以重新创建自身的 NQP。

通常, 我们使用最新版本来更新 stage0

How Rakudo uses NQP

Rakudo 本身不是一个自举编译器, 这使得它的开发容易一点。大部分 Rakudo 是用 NQP 编写的。这包括:

  • 编译器本身的核心, 它解析 Perl 6 源代码, 构建 QAST, 管理声明并进行各种优化
  • 元对象, 它指定了不同类型(类, roles, 枚举, subsets)是如何工作的
  • bootstrap, 它将足够的 Perl 6 核心类型组合在一起, 以便能够在 Perl 6 中编写内置的类, 角色和例程

因此, 虽然一些 Rakudo 是可访问的, 如果你知道 Perl 6, 知道 NQP - 既作为一种语言又作为一种编译器工具链 - 是和大部分 Rakudo 的其他部分工作的入口。

NQP 语言

它不完全是 Perl 6(Not Quite Perl 6), 但是能很好地构建 Perl 6

设计目标

NQP 被设计为……

  • 理想的编写编译器相关的东西
  • 几乎是 Perl 6 的一个子集
  • 比 Perl 6 更容易编译和优化

注意, 它避免了

  • 赋值
  • Flattening and laziness
  • 操作符的多重分派(因此没有重载)
  • 有很多 built-ins

字面量

整数字面量

0       42      -100

浮点字面量 (NQP 中没有 rat!)

0.25    1e10    -9.9e-9

字符串字面量

'non-interpolating'         "and $interpolating"
q{non-interpolating}        qq{and $interpolating}
Q{not even backslashes}

Sub 调用

在 NQP 中这些总是需要圆括号:

say('Mushroom, mushroom');

像 Perl 6 中一样, 这为子例程的名字添加 & 符号并对该例程做词法查询。

然而, 没有列表操作调用语法:

plan 42;    # "Confused" parse error
foo;        # Does not call foo; always a term

这可能是 NQP 初学者最常见的错误。

变量

可以是 my (lexical) 或 our (package) 作用域的:

my $pony;
our $stable;

常用的符号集也是可用的:

my $ark;                # Starts as NQPMu
my @animals;            # Starts as []
my %animal_counts;      # Starts as {}
my &lasso;              # Starts as 

也支持动态变量

my @*blocks;

绑定

NQP 没有提供 = 赋值操作符。只提供了 := 绑定操作符。这使 NQP 免于 Perl 6 容器语义的复杂性。

下面是一个简单的标量示例:

my $ast := QAST::Op.new( :op('time_n') );

绑定与数组

注意绑定拥有item 赋值优先, 所以你不可以这样写:

my @states := 'start', 'running', 'done';    # Wrong!

相反, 这应该被表示为下面的其中之一:

my @states := ['start', 'running', 'done'];  # Fine
my @states := ('start', 'running', 'done');  # Same thing
my @states := ;          # Cutest

原生类型化变量

目前, NQP 并不真正支持对变量的类型约束。唯一的例外是它会注意原生类型

my int $idx := 0;
my num $vel := 42.5;
my str $mug := 'coffee'; 

注意: 在 NQP 中, 绑定用于原生类型!这在 Perl 6 中是非法的, Perl 6 中原生类型只能被赋值。尽管这是非常武断的, 目前 Perl 6 中原生类型的赋值实际上被编译到 nqp::bind(...) op 中!

控制流

大部分 Perl 6 条件结构和循环结构也存在于 NQP 中。就像在真实的 Perl 6 中一样, 条件的周围不需要圆括号, 并且还能使用 pointy 块。循环结构支持 next/last/redo.

if $optimize {
    $ast := optimize($ast);
}
elsif $trace {
    $ast := insert_tracing($ast);
}

可用的: if, unless, while, until, repeat, for

还没有的: loop, given/when, FIRST/NEXT/LAST phasers

子例程

与 Perl 6 中子例程的声明很像, 但是 NQP 中即使没有参数, 参数列表也是强制的。你可以 return 或使用最后一个语句作为隐式返回值。

sub mean(@numbers) {
    my $sum;
    for @numbers { $sum := $sum + $_ }
    return $sum / +@numbers;
}

Slurpy 参数也是可用的, 就像 | 用来展开参数列表那样。

注意: 参数可以获得类型约束, 但是与变量一样, 当前只有原生类型。(例外:多重分派;以后会有更多。)

Named arguments and parameters

支持命名参数:

sub make_op(:$name) {
    QAST::Op.new( :op($name) )
}

make_op(name => 'time_n');  # 胖箭头语法
make_op(:name);     # Colon-pair 语法
make_op(:name('time_n'));   # The same

注意: NQP 中没有 Pair 对象!Pairs - colonpairs 或 fat-arrow 对儿 - 仅在参数列表上下文中有意义。

Blocks 和 pointy blocks

尖尖块提供了熟悉的 Perl 6 语法:

sub op_maker_for($op) {
    return -> *@children, *%adverbs {
        QAST::Op.new( :$op, |@children, |%adverbs )
    }
}

从这个例子可以看出, 它们有闭包语义。

注意: 普通块也可用作闭包, 但不像 Perl 6 那样使用隐式的 $_ 参数。

Built-ins 和 nqp::ops

NQP 具有相对较少的内置函数。但是, 它提供了对 NQP 指令集的完全访问。这里有几个常用的指令, 知道它们会很有用。

# On arrays
nqp::elems, nqp::push, nqp::pop, nqp::shift, nqp::unshift

# On hashes
nqp::elems, nqp::existskey, nqp::deletekey

# On strings
nqp::substr, nqp::index, nqp::uc, nqp::lc

我们将在课程中发现更多。

异常处理

可以使用 nqp::die 指令抛出一个异常:

nqp::die('Oh gosh, something terrible happened');

tryCATCH 指令也是可用的, 尽管不像完全的 Perl 6, 你没有期望去智能匹配 CATCH 的内部。一旦你到了那儿, 就认为异常被捕获到了(弹出一个显式的 nqp::rethrow)。

try {
    something();
    CATCH { say("Oops") }
}

类, 属性和方法

就像在 Perl 6 中一样, 使用 class, hasmethod 关键字来声明。类可以是词法(my)作用域的或包(our)作用域的(默认)。

class VariableInfo {
    has @!usages;
    
    method remember_usage($node) {
        nqp::push(@!usages, $node)
    }
    
    method get_usages() {
        @!usages
    }
}

self 关键字也是可用的, 方法可以具有像 subs 那样的参数。

More on attributes

NQP 没有自动的存取器生成, 所以你不能这样做:

has @.usages; # 不支持

支持原生类型的属性, 并且将直接有效地存储在对象体中。任何其他类型都被忽略。

has int $!flags;

与 Perl 6 不同, 默认构造函数可用于设置私有属性, 因为这是我们所拥有的。

my $vi := VariableInfo.new(usages => @use_so_far);

Roles (1)

NQP 支持 roles. 像类那样, roles 可以拥有属性和方法。

role QAST::CompileTimeValue {
    has $!compile_time_value;
    
    method has_compile_time_value() {
        1
    }
    
    method compile_time_value() {
        $!compile_time_value
    }
    
    method set_compile_time_value($value) {
        $!compile_time_value := $value
    }
}

Roles (2)

role 可以使用 does trait 组合到类中:

class QAST::WVal is QAST::Node does QAST::CompileTimeValue {
    # ...
}

或者, MOP 可用于将 role 混合到单个对象中:

method set_compile_time_value($value) {
    self.HOW.mixin(self, QAST::CompileTimeValue);
    self.set_compile_time_value($value);
}

多重分派

支持基本多重分派。它是 Perl 6 语义的一个子集, 使用更简单(但兼容)的候选排序算法版本。

与完全的 Perl 6 不同, 你**必须写一个 proto ** sub 或方法; 没有自动生成。

proto method as_jast($node) {*}

multi method as_jast(QAST::CompUnit $cu) {
    # compile a QAST::CompUnit
}

multi method as_jast(QAST::Block $block) {
    # compile a QAST::Block
}

练习 1

有机会熟悉基本的 NQP 语法, 如果你还没有这样做。

还有机会学习常见的错误看起来是什么样的, 所以如果你在实际工作中遇到他们, 你可以认出它们。 :-)

Grammars

虽然在许多领域 NQP 相比完全的 Perl 6 相当有限, 但是 grammar 几乎支持相同的水平。这是因为 NQP 语法必须足够好以处理解析 Perl 6 本身。

Grammars 是一种类, 并且使用 grammar 关键字引入。

grammar INIFile {
}

事实上, grammars 太像类了, 以至于在 NQP 中, 它们是由相同的元对象实现的。区别是它们默认继承于什么, 并且你把什么放在它们里面。

INI 文件

作为一个简单的例子, 我们将考虑解析 INI 文件。

带有值的键, 可能按章节排列。

name = Animal Facts
author = jnthn

[cat]
desc = The smartest and cutest
cuteness = 100000

[dugong]
desc = The cow of the sea
cuteness = -10

整体方法

grammar 包含一组用关键字 token, ruleregex 声明的规则。真的, 他们就像方法一样, 但是用规则语法写成。

token integer { \d+ }       # one or more digits
token sign    { <[+-]> }    # + or - (character class)

更复杂的规则由调用现有规则组成:

token signed_integer { ?  }

这些对其他规则的调用可以被量化, 放在备选分支中, 等等。

旁白:grammar和正则表达式

在这一点上, 你可能想知道 grammar 和正则表达式是如何关联的。毕竟, grammar 似乎是由正则表达式那样的东西组成的。

还有一个 regex 声明符, 可以在 grammar 中使用。

regex email { <[\w.-]>+ '@' <[\w.-]>+ '.' \w+ }

关键的区别是 regex 会回溯, 而 ruletoken 不会。支持回溯涉及保持大量状态, 并且对于复杂的 grammar 解析大量输入, 这将快速消耗大量内存!大语言往往在解析器中避免回溯。

旁白: NQP 中的正则表达式

对于较小规模的东西, NQP 确实也在普通场景中为正则表达式提供支持。

if $addr ~~ /<[\w.-]>+ '@' <[\w.-]>+ '.' \w+/ {
    say("I'll mail you maybe");
}
else {
    say("That's no email address!");
}

这被求值为匹配对象。

解析条目

一个条目有一个键(一些单词字符)和一个值(直到行尾的所有内容):

token key   { \w+ }
token value { \N+ }

合在一起, 它们组成了一个条目:

token entry {  \h* '=' \h*  }

\h 匹配任何水平空白(空格, 制表符等)。 = 号必须加引号, 因为任何非字母数字都被视为 Perl 6 中的正则表达式语法。

TOP 开始

grammar 的入口点是一个特殊的规则, “TOP”。现在, 我们查找整个文件是否含有包含条目的行, 或者只是没有。

token TOP {
    ^
    [
    |  \n
    | \n
    ]+
    $
}

注意在 Perl 6 中, 方括号是非捕获组(Perl 5 的 (?:...)), 而非字符类.

尝试我们的 grammar

我们可以通过在 grammar 上调用 parse 方法来尝试我们的 grammar。这将返回一个匹配对象

my $m := INIFile.parse(Q{
name = Animal Facts
author = jnthn
});
Perl 6 - Rakudo and NQP Internals_第4张图片
example-match-object.png

迭代结果

每个 rule 调用都产生一个 match 对象, 调用语法会把它捕获到 match 对象中。

因为我们匹配了很多 entries, 所以我们在 match 对象中的 entry 键下面得到一个数组。

因此, 我们能够遍历它以得到每一个 entry:

for $m -> $entry {
    say("Key: {$entry}, Value: {$entry}");
}

追踪我们的 grammar

NQP 自带一些内置支持, 用于跟踪 grammars 的去向。它不是一个完整的调试器, 但用它来查看 grammar 在失败之前走的有多远是有用的。它使用 trace-on 函数开启:

INIFile.HOW.trace-on(INIFile);

并且产生的结果像下面这样:

Calling parse
  Calling TOP
    Calling entry
      Calling key
      Calling value
    Calling entry
      Calling key
      Calling value

token vs. rule

当我们使用 rule 代替 token 时, 原子后面的任何空白被转换为对 ws非捕获调用。即:

rule entry {  '='  }

等价于:

token entry {  <.ws> '=' <.ws>  <.ws> } # . = non-capturing

我们继承了一个默认的 ws, 但是我们也能提供我们自己的:

token ws { \h* }

解析 sections (1)

一个 section 有一个标题和许多条目。但是, 顶层也可以有条目。因此, 把这个分解是有意义的。

token entries {
    [
    |  \n
    | \n
    ]+
}

然后 TOP 规则可以变为:

token TOP {
    ^
    
    
+ $ }

解析 sections (2)

最后但并非最不重要的是 section token:

token section {
    '[' ~ ']'  \n
    
}

这个 ~ 语法很漂亮. 第一行就像这样:

'['  ']' \n

然而, 如果找不到闭合 ] 就会产生一个描述性的错误消息, 而不只是失败匹配。

Actions

解析 grammar 可以使用 actions 类; 它的方法具有所匹配 grammar 中的某些或所有规则的名字。

相应规则成功匹配之后, actions 方法被调用。

Perl 6 - Rakudo and NQP Internals_第5张图片
top-down-bottom-up.png

在 Rakudo 和 NQP 编译器中, actions构造QAST树。对于这个例子, 我们将做一些更简单的事情。

Actions 示例: aim

给定像下面这样的 INI 文件:

name = Animal Facts
author = jnthn

[cat]
desc = The smartest and cutest
cuteness = 100000

我们想使用 actions 类来建立一个散列的散列。顶级哈希将包含键 cat_(下划线收集不在 section 中的任何键)。其值是该 section 中键/值对的哈希。

Actions 示例: entries

Action 方法将刚刚匹配过的 rule 的 Match 对象作为参数。
把这个 Match 对象放到 $/ 里很方便, 所以我们能够使用 $ 语法糖 (它映射到 $/ 上)。这个语法糖看起来像普通的标量, 第一眼看上去的时候有点懵, 再看一眼发现它有一对 <> 后环缀, 而这正是散列中才有的, 相当于 {'entry'}, 不过前者更漂亮。

class INIFileActions {
    method entries($/) { # Match Object 放在参数 $/ 中
        my %entries;
        for $ -> $e {
            %entries{$e} := ~$e;
        }
        make %entries;
    }
}

最后, make 将生成的散列附加$/ 上。这就是为什么 'TOP` action 方法能够在构建顶级哈希时检索它。

Actions example: TOP

TOP action 方法在由 entries action 方法创建的散列中构建顶级散列。当 make 将某个东西附加到 $/ 上时, .ast 方法检索附加到其他匹配对象上的东西。

method TOP($/) {
    my %result;
    %result<_> := $.ast;
    for $
-> $sec { %result{$sec} := $sec.ast; } make %result; }

因此, 顶层散列通过 section 名获取到由安装到其中的 entries action 方法产生的散列。

Actions 示例: 用 actions 解析

actions 作为具名参数传递给 parse:

my $m := INIFile.parse($to_parse, :actions(INIFileActions.new));

结果散列可以使用 .ast 从结果匹配对象中获得, 正如我们已经看到的。

my %sections := $m.ast;
for %ini -> $sec {
    say("Section {$sec.key}");
    for $sec.value -> $entry {
        say("    {$entry.key}: {$entry.value}");
    }
}

Actions 示例: 输出

上一张幻灯片上的转储代码产生如下输出:

Section _
    name: Animal Facts
    author: jnthn
Section cat
    desc: The smartest and cutest
    cuteness: 100000
Section dugong
    desc: The cow of the sea
    cuteness: -10

练习 2

使用 gramamrs 和 actions 进行小练习的一次机会。

目标是解析 Perl 6 IRC 日志的文本格式; 例如, 参见 http://irclog.perlgeek.de/perl6/2013-07-19/text

另外一个例子: SlowDB

解析 INI 文件这个例子是一个很好的开端, 但是离编译器还差的远。作为那个方向的进一步深入, 我们会使用查询解释器创建一个小的, 无聊的, 在内存中的数据库。

它应该像下面这样工作:

INSERT name = 'jnthn', age = 28
[
    result: Inserted 1 row
]
SELECT name WHERE age = 28
[
    name: jnthn
]
SELECT name WHERE age = 50
Nothing found

查询解释器 (1)

我们解析 INSERTSELECT 查询的任意之一.

token TOP {
    ^ [  |  ] $
}

如果我们追踪 SELECT 查询的解析, 我们会看到像下面这样的东西:

Calling parse
  Calling TOP
    Calling select
      Calling ws
      Calling keylist

所以它怎么知道不去麻烦尝试 呢?

备选分支和 LTM (2)

答案是可传递的最长Token匹配(Transitive Longest Token Matching). grammar 引擎创建了一个 NFA (状态机), 一旦遇到一个备选分支(alternation), 就按照这个备选分支能够匹配到的字符数对分支进行排序。然后 Grammar 引擎在这些分支中首先尝试匹配最多字符的那个, 而不麻烦那些它认为不可能的分支。

备选分支和 LTM (3)

Gramamr 引擎不会仅仅孤立地看一个 rule。相反, 它 可传递性地考虑 subrule 调用 (considers subrule calls transitively). 这意味着导致某种不可能的整个调用链可以被忽略。

Perl 6 - Rakudo and NQP Internals_第6张图片
ltm-transformation.png

它由非声明性构造(如向前查看, 代码块或对默认ws规则的调用)或递归 subrule 调用界定。

轻微的痛点

令我们讨厌的一件事情就是我们的 TOP action 方法最后看起来像这样:

method TOP($/) {
    make $.ast !! $.ast;
}

显而易见, 一旦我们添加 UPDATEDELETE 查询, 维护起来将会多么痛苦

我们的 value action 方法类似:

method value($/) {
    make $ ?? $.ast !! $.ast;
}

Protoregexes

令我们痛苦的答案是 protoregexes。 它们提供了一个更可扩展的方式来表达备选分支

proto token value {*}
token value:sym { \d+ }
token value:sym  { \' <( <-[']>+ )> \' }

本质上, 我们引入了一个新的语法类别, value, 然后定义这个类别下不同的案例(cases)。像 value 这样的调用会使用 LTM 来对候选者进行排序和尝试 - 就像备选分支所做的那样。

Protoregexes 和 action 方法 (1)

回到 actions 类, 我们需要更新我们的 action 方法来匹配 rules 的名字:

method value:sym($/) { make ~$/ }
method value:sym($/)  { make ~$/ }

然而, 我们不需要 value 自身这个 action 方法。任何查看 $ 的东西会被提供一个来自成功候选者的匹配对象 - 并且 $.ast 因此会获得正确的东西。

Protoregexes and action methods (2)

例如, 在我们重构查询之后:

token TOP { ^  $ }

proto token query {*}
token query:sym {
    'INSERT' :s 
}
token query:sym