鹦鹉虚拟机/不完全Perl
鹦鹉上 NQP 编译器的源代码可以在 Parrot 存储库的
compilers/nqp
目录中找到。不完全Perl (NQP) 是 Perl 6 语言的一个子集的实现,最初是为了帮助引导 Perl 6 的实现而设计的。换句话说,Perl 6 开发人员正在使用 Perl 6 语言本身的一个子集来编写 Perl 6 编译器。这个引导是通过首先使用 PIR 编写一个小的 NQP 编译器来完成的。NQP 编译器完成后,就可以用 NQP 编写程序,而无需完全用 PIR 编写。
然而,NQP 不仅仅是为 Perl 6 预留的工具。其他语言也使用 NQP 作为轻量级实现语言。NQP 的一个主要优点是它不依赖于任何可能会随着时间推移而发生变化的外部代码库。但是,由于其占用的空间很小,NQP 往往缺乏高级编程语言的许多功能,并且在最初学习编程时,不使用一些常见的构造可能会很困难。
本节需要事先了解 Perl 6 编程。 |
在这里,我们将讨论 NQP 编程的一些基础知识。有经验的 Perl 程序员,即使是熟悉 Perl 5 但不一定熟悉 Perl 6 的程序员,也会发现其中大部分内容都是简单的复习。
NQP **不是 perl5 或 perl 6**。这一点再怎么强调也不为过。NQP 中缺少了许多 Perl 的功能。有时,这意味着你需要以硬编码的方式完成一些任务。在 NQP 中,我们使用 :=
运算符,称为 **绑定运算符**。与正常的变量赋值不同,绑定不会将值从一个“容器”复制到另一个。相反,它在两个变量之间创建了一个链接,并且从那时起,它们就成为同一个容器的别名。这类似于在 C 中复制指针的方式不会复制所指向的数据。
NQP 中的变量通常具有三种基本类型之一:标量、数组和哈希表。标量是单个值,例如整数、浮点数或字符串。数组是标量的列表,通过整数索引访问。哈希表是使用字符串(称为键)作为索引的标量列表。所有变量名都在前面都有一个 **符号**。符号是一个标点符号,例如 "$"、"@" 或 "%",它表示变量的类型。
标量变量具有 "$" 符号。以下是标量值的示例
$x := 5; $mystring := "string"; $pi := 3.1415;
数组使用 "@" 符号。我们可以像这样使用数组
@myarray[1] := 5; @b[2] := @a[3];
请注意,NQP 没有像 Perl6 那样拥有 列表上下文。这意味着你无法进行列表赋值,例如
@b := (1, 2, 3); # WRONG! $b := (1, 2, 3); # CORRECT
NQP 被设计为精简,尽可能少地支持 Perl6 的开发。上面的行也可以写成
@b[0] := 1; @b[1] := 2; @b[2] := 3;
我们将在页面下面更详细地讨论这一点。哈希表以 "%" 符号为前缀
%myhash{'mykey'} := 7 %mathconstants{'pi'} := 3.1415; %mathconstants{'2pi'} := 2 * %mathconstants{'pi'};
对于不熟悉 Perl 的人来说,哈希表也被称为字典(在 Python 中)或关联数组。基本上,它们类似于数组,但使用字符串索引而不是整数索引。
正如我们之前提到的,NQP 中没有“数组上下文”这样的东西,Perl 5 程序员可能已经期待过。Perl 语言的一个重要功能是它能够感知上下文,并且它会根据你是在标量上下文还是数组上下文中以不同的方式处理事物。如果没有它,它真的就不是 perl。这就是为什么他们称之为 NQP,因为它类似于 perl,但不是 完全的 perl。在 NQP 中,你无法编写以下任何一个
@a := (1, 2, 3); |
错误! |
%b := ("a" => "b", "c" => "d"); |
错误! |
所有变量(哈希表、标量和数组)都可以使用关键字“my”声明为词法变量,或使用关键字“our”声明为全局变量。对于已经阅读过 PIR 部分的读者来说,“my”变量对应于 .lex
指令,以及 store_lex
和 find_lex
指令。“our”变量对应于 set_global
和 find_global
指令。以下是一个示例
这段 NQP 代码 | 翻译成(大致)以下 PIR 代码 |
---|---|
my $x; my @y; my %z; |
set_lex "$x", "" $P1 = new 'ResizablePMCArray' set_lex "@y", $P1 $P2 = new 'Hash' set_lex "%z", $P2 |
同样,对于“our”
这段 NQP 代码 | 翻译成(大致)以下 PIR 代码 |
---|---|
our $x; our @y; our %z; |
set_global "$x", "" $P1 = new 'ResizablePMCArray' set_global "@y", $P1 $P2 = new 'Hash' set_global "%z", $P2 |
NQP 拥有 PIR 中缺少的所有高级控制结构。我们以 PIR 所没有的方式拥有循环和 If/Then/Else 分支。由于这是一种类似 Perl 的语言,NQP 所拥有的循环是多种多样的,并且相对来说是高级的。
在分支方面,我们有
- If/Then/Else
if ($key eq 'foo') { THEN DO SOME FOO STUFF } elsif ($key eq 'bar') { THEN DO THE BAR-RELATED STUFF } else { OTHERWISE DO THIS }
- Unless/Then/Else
- For
- "For" 循环遍历列表并将
$_
设置为当前索引,就像在 perl5 中一样。NQP 中没有带有起始点和步长操作的 c 风格循环,尽管 Perl 5 和 Perl 6 中都有类似的构造。以下是一个基本的 for 循环
for (1,2,3) { Do something with $_ }
- 精确地翻译成以下 PIR 代码
.sub 'for_statement' .param pmc match .local pmc block, past $P0 = match['EXPR'] $P0 = $P0.'item'() $P1 = match['block'] block = $P1.'item'() block.'blocktype'('sub') .local pmc params, topic_var params = block[0] $P3 = get_hll_global ['PAST'], 'Var' topic_var = $P3.'new'('name'=>'$_', 'scope'=>'parameter') params.'push'(topic_var) block.'symbol'('$_', 'scope'=>'lexical') $P2 = get_hll_global ['PAST'], 'Op' $S1 = match['sym'] past = $P2.'new'($P0, block, 'pasttype'=>$S1, 'node'=>match) match.'result_object'(past) .end
你还可以像这样遍历哈希表的键
for (keys %your_hash) { DO SOMETHING WITH %your_hash{$_} }
其中 keys %your_hash
创建了 %your_hash
中所有键的列表,并遍历此列表,将 $_
设置为保存当前键。
- While
- "While" 循环类似于 for 循环。在 NQP 中,while 循环如下所示
while(EXIT_CONDITION) { LOOP_CONTENTS }
- 在大致上变成以下 PIR 代码
loop_top: if(!EXIT_CONDITION) goto loop_end LOOP_CONTENTS goto loop_top loop_end:
- Do/While
- "do/while" 循环类似于 while 循环,除了条件在循环结束时测试,而不是在开始时测试。这意味着循环至少执行一次,并且如果条件不满足,可能会执行更多次。在 NQP 中
do { LOOP_CONTENTS } while(EXIT_CONDITION);
- 在 PIR 中
loop_top: LOOP_CONTENTS if(!EXIT_CONDITION) goto loop_end goto loop_top loop_end:
NQP 支持一小部分用于操作变量的运算符。
运算符 | 用途 |
---|---|
+ , - |
标量加法和减法 |
* , / |
标量乘法和除法 |
% |
整数模 |
$( ... ) |
将参数转换为标量 |
@( ... ) |
将参数视为数组 |
%( ... ) |
将参数视为哈希表 |
~ |
字符串连接 |
eq |
字符串相等比较 |
ne |
字符串不相等比较 |
:= |
绑定 |
> , < , >= , <= , == , != |
相等和不相等运算符 |
当语法规则匹配并且执行 {*}
规则时,会生成一个名为 **匹配对象** 的特殊类型的哈希表对象,并传递给关联的 NQP 方法。此匹配对象被赋予了特殊的名称 $/
。你可以给它取一个不同的名字,但你会失去使 $/
变量如此特殊的许多功能。
通常,当你在哈希表中引用对象时,你会使用 { }
花括号。例如
my %hash; %hash{'key'} = "value";
当您要从哈希引用中调用值时,您需要执行更复杂的操作。
$hashref->{'key'} = "value";
在 NQP(以及 Perl 6)中,尖括号会神奇地“自动引用”它们内部的内容。因此,您可以写
<field>
来代替{'field'}
或<'field'>
。但是,使用特殊的默认匹配对象,您可以使用< >
尖括号。因此,无需编写
$/->{'key'}
我们可以编写更简洁的代码
$<key>
哈希对象的键对应于语法中使用的子规则的名称。因此,如果我们有语法规则
rule my_rule { <first> <second> <third> <andmore> }
我们的匹配对象将具有以下字段
$<first> $<second> $<third> $<andmore>
如果我们对任何一个字段有多个值,例如
rule my_rule { <first> <second> <first> <second> }
现在,$<first>
和$<second>
都是两个元素的数组。此外,我们可以将此行为扩展到语法中的重复运算符
rule my_rule { <first>+ <second>* }
现在,$<first>
和$<second>
都是数组,它们的长度指示每个匹配了多少个项目。您可以使用 + 运算符或scalar()
函数来获取匹配的项目数量。
我们想要创建一个简单的解析器,检测“Hello”或“Goodbye”这两个词语。如果输入了这两个词语中的任何一个,我们想要打印出成功消息和词语。如果两个词语都没有输入,我们打印错误信息。为了从输入中挑选出词语,我们将使用内置的子规则<ident>
。
rule TOP { <ident> $ {*} }
在这个语法规则中,我们寻找一个单个标识符(对于我们的目的,它将是一个词语),后面跟着文件结尾。一旦我们有了这些,我们就创建匹配对象并调用我们的 Action 方法
method TOP($/) { if($<ident> eq "Hello") { say("success! we found Hello"); } elsif($<ident> eq "Goodbye") { say("success! we found Goodbye"); } else { say("failure, we found: " ~ $<ident>); } make PAST::Stmts.new(); }
由于 HLLCompiler 类期望我们的 Action 方法返回一个 PAST 节点,我们必须创建一个空的 stmts 节点并返回它。当我们在输入上运行这个解析器时,它将有三种可能的结果
- 我们收到了一个“Hello”或“Goodbye”,系统将打印出成功方法。
- 我们收到了不同的词语,我们将收到错误消息。
- 我们收到了太多词语,太少词语,或者不是词语的东西。这将导致解析错误。
试试看!
这是一个简单的例子,展示了如何创建一个程序将八进制数转换为二进制数。我们从mk_language_shell.pl
中的基本语言 shell 开始
语法文件
grammar Oct2Bin::Grammar is PCT::Grammar; rule TOP { <octdigit>+ [ $ || <panic: Syntax error> ] {*} } token octdigit {'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'}
动作文件
class Oct2Bin::Grammar::Actions; method TOP($/) { my @table; @table[0] := '000'; @table[1] := '001'; @table[2] := '010'; @table[3] := '011'; @table[4] := '100'; @table[5] := '101'; @table[6] := '110'; @table[7] := '111'; my $string := ""; for $<octdigit> { $string := $string ~ @table[$_]; } say( $string ); make PAST::Stmts.new( ); }
注意,在我们的动作文件中,我们不得不一次实例化一个查找表中的元素?这是因为 NQP 对数组没有完全的理解。还要注意,我们让 TOP 方法返回一个空的 PAST::Stmts 节点,以抑制 PCT 关于没有 PAST 节点的警告。
NQP 不是编写伴随语法的动作方法的唯一方法。它由于很多原因而是一个有吸引力的工具,但它不是唯一的选择。动作方法也可以用 PIR 或 PASM 编写。这就是 NQP 编译器本身的实现方式。这是一个关于 PIR 动作可能是什么样子的例子
.sub 'block' :method .param pmc match .param string key .local pmc past $P0 = get_hll_global ['PAST'], 'Stmts' past = $P0.'new'('node' => match) ... match.'result_object'(past) # make $past; .end