鹦鹉虚拟机/鹦鹉中间表示
鹦鹉中间表示 (PIR) 在很多方面类似于C 编程语言:它比汇编语言更高级,但仍然非常接近底层机器。使用 PIR 的好处是它比 PASM 更容易编程,但同时它也暴露了 Parrot 的所有底层功能。
PIR 在 Parrot 世界中具有双重目的。首先,它被用作从高级语言自动生成的代码的目標。高级语言的编译器会发出 PIR 代码,然后可以解释和执行。第二个目的是作为一种低级的可读编程语言,用它可以编写基本组件和 Parrot 库。在实践中,PASM 仅作为 Parrot 字节码的可读直接翻译存在,很少被人类直接用于编程。PIR 几乎完全用于为 Parrot 编写低级软件。
PIR 语法在很多方面类似于 C 或 BASIC 等旧的编程语言。除了类似 PASM 的操作之外,还有一些控制结构和算术运算,这些运算简化了人类读者的语法。所有 PASM 都是合法的 PIR 代码,PIR 几乎只是在原始 PASM 指令上覆盖了一层精美的语法。如果可以,你应该始终使用 PIR 的语法而不是 PASM 的语法,以方便使用。
虽然 PIR 比 PASM 具有更多功能和更好的语法,但它本身并不是高级语言。PIR 仍然非常低级,并不真正适合用于构建大型系统。Parrot 上为语言和应用程序设计者提供了许多其他工具,PIR 实际上只需要在少数领域使用。最终,可能会创建足够多的工具,以至于不再需要直接使用 PIR。
PIR 旨在帮助实现 Perl、TCL、Python、Ruby 和 PHP 等高级语言。正如我们之前讨论过的,高级语言 (HLL) 与 PIR 有两种可能的联系方式
- 我们使用 NQP 和 Parrot 编译器工具 (PCT) 为 HLL 编写编译器。然后将此编译器转换为 PIR,然后转换为 Parrot 字节码。
- 我们在 HLL 中编写代码并进行编译。编译器会将代码转换为名为 PAST 的树状中间表示,再转换为名为 POST 的另一种表示,最后转换为 PIR 代码。从这里,PIR 可以直接解释,或者可以进一步编译为 Parrot 字节码。
因此,PIR 具有有助于编写编译器的功能,它还具有支持使用这些编译器编写的 HLL 的功能。
与 Perl 类似,PIR 使用 "#
" 符号作为注释的开始。注释从 #
运行到当前行的末尾。PIR 还允许在文件中使用 POD 文档。我们稍后将详细讨论 POD。
子程序从 .sub
指令开始,以 .end
指令结束。我们可以使用 .return
指令从子程序中返回值。以下是一个不带参数并返回 π 的近似值的函数的简短示例
.sub 'GetPi' $N0 = 3.14159 .return($N0) .end
请注意,子程序名称是用单引号括起来的。这不是必需的,但它非常有用,应该尽可能地这样做。我们将在下面讨论这样做的原因。
有两种方法可以调用子程序:直接和间接。在直接调用中,我们按名称调用特定的子程序
$N1 = 'GetPi'()
但是,在间接调用中,我们使用包含该子程序名称的字符串来调用子程序
$S0 = 'GetPi' $N1 = $S0()
当我们开始使用命名变量时(我们将在下面更详细地讨论),问题就出现了。考虑以下代码片段,其中我们有一个名为 "GetPi" 的局部变量
GetPi = 'MyOtherFunction' $N0 = GetPi()
在此代码片段中,我们调用 "GetPi" 函数(因为我们执行了 GetPi()
调用)还是调用 "MyOtherFunction" 函数(因为 GetPi 变量包含值 'MyOtherFunction')?简短的回答是,我们将调用 "MyOtherFunction" 函数,因为局部变量名称在这类情况下优先于函数名称。但是,这有点令人困惑,不是吗?为了避免这种混乱,人们使用了一些标准来使它更容易
$N0 = GetPi() |
仅用于间接调用 |
$N0 = 'GetPi'() |
用于所有直接调用 |
通过坚持这种约定,我们避免了以后的所有可能混淆。
可以使用 .param
指令声明子程序的参数。以下是一些示例
.sub 'MySub' .param int myint .param string mystring .param num mynum .param pmc mypmc
在参数声明中,.param
指令必须位于函数的顶部。你不能在 .sub
和 .param
指令之间放置注释或其他代码。以下是上面的相同示例
.sub 'MySub' # These are my params: .param int myint .param string mystring .param num mynum .param pmc mypmc |
错误! |
---|
此问题将来可能会更改,以允许注释与参数列表交织在一起。 |
像上面一样按严格顺序传递的参数称为位置参数。位置参数通过它们在函数调用中的位置来区分。将位置参数放在不同的顺序会导致不同的效果,或者可能会导致错误。Parrot 支持第二种类型的参数,命名参数。参数不是按它们在字符串中的位置传递,而是按名称传递,可以按任意顺序。以下是一个示例
.sub 'MySub' .param int yrs :named("age") .param string call :named("name") $S0 = "Hello " . call $S1 = "You are " . yrs $S1 = $S1 . " years old print $S0 print $S1 .end .sub main :main 'MySub'("age" => 42, "name" => "Bob") .end
在上面的示例中,我们也可以轻松地颠倒顺序
.sub main :main 'MySub'("name" => "Bob", "age" => 42) # Same! .end
命名参数非常有用,因为你无需担心变量的确切顺序,尤其是在参数列表变得很长时。
函数可以声明可选参数,调用者可以指定也可以不指定。为此,我们使用 `:optional` 和 `:opt_flag` 修饰符。
.sub 'Foo' .param int bar :optional .param int has_bar :opt_flag
在这个例子中,如果调用者提供了 `bar`,参数 `has_bar` 将被设置为 1,否则为 0。下面是一些示例代码,它们接收两个数字并将其加在一起。如果未提供第二个参数,则第一个数字加倍。
.sub 'AddTogether' .param num x .param num y :optional .param int has_y :opt_flag if has_y goto ive_got_y y = x ive_got_y: $N0 = x + y .return($N0) .end
然后我们将使用以下方式调用此函数:
'AddTogether'(1.0, 1.5) #returns 2.5 'AddTogether'(3.0) #returns 6.0
请注意,`:opt_flag` 变量的类型始终为 `int`。` :optional` 参数可以是任何类型。 |
子例程可以接受任意数量的参数,这些参数可以加载到数组中。可以接受可变数量的输入参数的参数称为 `:slurpy` 参数。贪婪参数将加载到数组 PMC 中,您可以在函数内部循环遍历它们,如果您愿意的话。以下是一个简短的示例。
.sub 'PrintList' .param list :slurpy print list .end .sub 'PrintOne' .param item print item .end .sub main :main PrintList(1, 2, 3) # Prints "1 2 3" PrintOne(1, 2, 3) # Prints "1" .end
贪婪参数吸收了所有函数参数的剩余部分。因此,贪婪参数应该只作为函数的最后一个参数。任何在贪婪参数之后的参数都不会接收任何值,因为为它们传递的所有参数将被贪婪参数吸收。
如果您有一个包含函数数据的数组 PMC,您可以传入数组 PMC。该数组本身将成为单个参数,它将加载到函数中的单个数组 PMC 中。但是,如果您在使用数组调用函数时使用 `:flat` 关键字,它将把数组的每个元素传递到不同的参数中。以下是一个示例函数。
.sub 'ExampleFunction' .param pmc a .param pmc b .param pmc c .param pmc d :slurpy
我们有一个名为 `x` 的数组,它包含三个整型 PMC:`[1, 2, 3]`。下面是两个例子。
函数调用 | 'ExampleFunction'(x, 4, 5) |
'ExampleFunction'(x :flat, 4, 5) |
---|---|---|
参数 |
|
|
可以使用 `local` 指令定义局部变量,使用类似于参数使用的语法。
.local int myint .local string mystring .local num mynum .local pmc mypmc
除了局部变量之外,您还可以使用 PIR 中的寄存器来存储数据。
**命名空间**是允许重用函数和变量名称而不会与以前的化身发生冲突的构造。命名空间还用于将类的所有方法放在一起,而不会与其他命名空间中相同名称的函数发生命名冲突。它们是在促进代码重用和减少命名污染方面宝贵的工具。
在 PIR 中,命名空间使用 `namespace` 指令指定。命名空间可以使用键结构嵌套。
.namespace ["Foo"] .namespace ["Foo";"Bar"] .namespace ["Foo";"Bar";"Baz"]
根命名空间可以使用一对空括号指定。
.namespace [] #Right! Enters the root namespace .namespace #WRONG! Brackets are required!
字符串是 PIR 中的基本数据类型,非常灵活。字符串可以指定为带引号的文字或代码中的“Here文档”文字。
Here文档字符串文字已成为现代编程语言中常用的工具,用于指定非常长的多行字符串文字。Perl 程序员会熟悉它们,但大多数 shell 程序员甚至现代 .NET 程序员也会熟悉它们。以下是 Here文档在 PIR 中的工作原理。
$S0 = << "TAG"
This is part of the Heredoc string. Everything between the '<< "TAG"' is treated as a literal string constant. This string ends when the parser finds the end marker.
TAG
Here文档允许输入长多行字符串,而无需使用大量凌乱的引号和连接操作。
可以指定带引号的字符串文字在特定字符集或编码中进行编码。
您可以使用 `include` 指令将外部 PIR 文件包含到当前文件中。例如,如果我们要将文件“MyLibrary.pir”包含到当前文件中,我们将编写以下内容:
.include "MyLibrary.pir"
请注意,`include` 指令是一个原始文本替换函数。PIR 代码文件不是像您从某些其他语言中期望的那样自包含的。例如,新用户相对常见的问题是命名空间溢出概念。考虑两个文件 A.pir 和 B.pir。
A.pir | B.pir |
---|---|
.namespace ["namespace 2"] |
.namespace ["namespace 1"] #here, we are in "namespace 1" .include "A.pir" #here we are in "namespace 2" |
文件 A 的 `namespace` 指令溢出到文件 B 中,这对大多数程序员来说违反直觉。
我们将在本书的后面花很多时间讨论类和面向对象编程。但是,由于我们已经稍微讨论过命名空间和子例程,因此我们可以为那些后面的讨论奠定一些基础。
PIR 中的类包含该类的命名空间、初始化器、构造函数和一系列方法。“方法”与普通子例程完全相同,除了三个区别之外。
- 它具有 `method` 标志。
- 它是使用“点表示法”调用的:`Object.Method()`
- 用于调用方法的对象(在点的左侧)存储在方法中的“self”变量中。
要创建类,我们首先需要为该类创建一个命名空间。在最简单的类中,我们创建方法。我们稍后会讨论初始化器和构造函数,但现在我们将坚持使用既不使用这些函数的简单类。
.namespace ["MathConstants"] .sub 'GetPi' :method $N0 = 3.14159 .return($N0) .end .sub 'GetE' :method $N0 = 2.71828 .return($N0) .end
使用此类(我们可能将其存储在“MathConstants.pir”中并包含到我们的主文件中),我们可以编写以下内容。
.local pmc mathconst mathconst = new 'MathConstants' $N0 = mathconst.'GetPi'() #$N0 contains the value 3.14159 $N1 = mathconst.'GetE'() #$N1 contains the value 2.71828
我们将在稍后解释更多混乱的细节,但这足以帮助您入门。
PIR 是一种低级语言,因此它不支持程序员可能习惯的任何高级控制结构。PIR 支持两种类型的控制结构:条件分支和无条件分支。
**无条件分支**由 **goto** 指令处理。
**条件分支**也使用 goto 命令,但与 **if** 或 **unless** 语句一起使用。只有当 if 条件为真或 unless 条件为假时,才执行跳转。
.HLL 指令尚未完全集成到 Parrot 中,因此此处关于它的许多内容要么是推测性的,要么是基于设计文档。由于所有这些内容都可能随着 Parrot 的发展而改变,因此我们将根据需要对本节进行更改。 |
每个 HLL 编译器都有一个命名空间,该命名空间与该 HLL 的名称相同。例如,如果我们要为 Perl 编写一个编译器,我们将创建命名空间 .namespace ["Perl"]
。如果我们不编写编译器,而是用纯 PIR 编写程序,我们将位于默认命名空间 .namespace ["Parrot"]
中。要创建新的 HLL 编译器,我们将使用 .HLL
指令来创建当前默认的 HLL 命名空间。
.HLL "mylanguage", "mylanguage_group"
HLL 命名空间中的所有内容对用该 HLL 编写的程序都是可见的。例如,如果我们有一个位于“PHP”命名空间中的 PIR 函数“Foo”,那么用 PHP 编写的程序可以像调用常规 PHP 函数一样调用 Foo 函数。这听起来可能有点复杂。以下是一个简短的示例
PIR 代码 | Perl 6 代码 |
---|---|
.namespace ["perl6"] .sub 'AddTwo' .param int a .param int b $I0 = a + b .return($I0) .end |
$x = AddTwo(4 + 5); |
为了简化,我们可以简单地编写 .namespace
(不带括号)以返回当前的 HLL 命名空间。
多方法是共享相同名称的一组子例程。例如,子例程“Add”可能具有不同的行为,具体取决于传递给它的参数是 Perl 5 浮点数、Parrot BigNum PMC 还是 Lisp Ratio。多重分派子例程与 PIR 中的任何其他子例程的声明方式相同,除了它们还具有 :multi
标志。当调用 Multi 时,Parrot 会加载具有相同名称的 MultiSub PMC 对象,并开始比较参数。与接受的参数列表最匹配的子例程将被调用。“最佳匹配”例程相对来说比较高级。Parrot 使用曼哈顿距离按子例程与给定列表的接近程度对其进行排序,然后调用列表顶部的子例程。
在排序时,Parrot 会考虑角色和多重继承。这使得它非常强大和灵活。
本页上的词汇可能开始变得有点复杂。在这里,我们将列出一些用于描述 Parrot 中事物的术语。
- 子例程
- 具有名称和参数列表的基本代码块。
- 方法
- 属于特定类并且可以在该类的对象上调用的基本代码块。方法只是具有额外的隐式
self
参数的子例程。 - 多重分派
- 当多个子例程具有相同的名称时,Parrot 会选择最适合调用的子例程。
- 单一分派
- 当只有一个具有给定名称的子例程时,Parrot 不需要进行任何复杂的排序或选择。
- MultiSub
- 一种 PMC 类型,它存储可以按名称调用并由 Parrot 排序/搜索的子例程集合。
- 多方法
- 与 MultiSub 相同,但它被调用为方法而不是子例程。
PIR 允许使用文本替换宏功能,其概念类似于(但实现不同于)C 的预处理器中使用的宏功能。PIR 没有支持条件编译的预处理器指令。
可以使用 .macro_const
关键字定义常数值。以下是一个示例
.macro_const PI 3.14 .sub main :main print .PI #Prints "3.14" .end
.macro_const
可以是整数常量、浮点数常量、字符串文字或寄存器名称。以下还有另一个示例
.macro_const MyReg S0 .macro_const HelloMessage "hello world!" .sub main :main .MyReg = .HelloMessage print .MyReg .end
这允许您为常见常量、字符串或寄存器命名。
可以使用 .macro
和 .endm
关键字创建基本的文本替换宏,分别标记宏的开始和结束。以下是一个快速示例
.macro SayHello print "Hello!" .endm .sub main :main .SayHello .SayHello .SayHello .end
这个示例,正如应该显而易见的那样,打印了三遍“Hello!”。我们也可以为我们的宏提供参数,以便将它们包含在文本替换中
.macro CircleCircumference(r) $N0 = r * 3.1.4 $N0 = $N0 * 2 print $N0 .endm .sub main :main .CircleCircumference(5) .CircleCircumference(10) .end
如果我们想在宏中定义一个临时变量怎么办?以下是一个想法
.macro PrintSomething .local string something something = "This is a message" print something .endm .sub main :main .PrintSomething .PrintSomething .end
进行文本替换后,我们将得到以下结果
.sub main :main .local string something something = "This is a message" print something .local string something something = "This is a message" print something .end
在替换后,我们声明了变量 something
两次!与其这样,我们可以使用 .macro_local
声明来创建一个对宏本地的具有唯一名称的变量
.macro PrintSomething .macro_local something something = "This is a message" print something
现在,相同的功能在进行文本替换后会转换为以下内容
.sub main :main .local string main_PrintSomething_something_1 main_PrintSomething_something_1 = "This is a message" print main_PrintSomething_something_1 .local string main_PrintSomething_something_2 main_PrintSomething_something_2 = "This is a message" print main_PrintSomething_something_2 .end
为 .macro_local 变量创建唯一名称的确切方案尚未最终确定,因此 Parrot 生成的实际名称可能会有所不同。.macro_local 指令尚未在 IMCC(Parrot 的 PIR 解析器)中实现。我们将发布其实现后的更新。 |
请注意,局部变量声明现在是如何唯一的?它们依赖于参数的名称、宏的名称以及文件中的其他信息?这是一个可重用的方法,不会导致任何问题。