跳转到内容

鹦鹉虚拟机/鹦鹉中间表示

来自维基教科书,开放世界中的开放书籍

鹦鹉中间表示

[编辑 | 编辑源代码]

鹦鹉中间表示 (PIR) 在很多方面类似于C 编程语言:它比汇编语言更高级,但仍然非常接近底层机器。使用 PIR 的好处是它比 PASM 更容易编程,但同时它也暴露了 Parrot 的所有底层功能。

PIR 在 Parrot 世界中具有双重目的。首先,它被用作从高级语言自动生成的代码的目標。高级语言的编译器会发出 PIR 代码,然后可以解释和执行。第二个目的是作为一种低级的可读编程语言,用它可以编写基本组件和 Parrot 库。在实践中,PASM 仅作为 Parrot 字节码的可读直接翻译存在,很少被人类直接用于编程。PIR 几乎完全用于为 Parrot 编写低级软件。

PIR 语法

[编辑 | 编辑源代码]

PIR 语法在很多方面类似于 C 或 BASIC 等旧的编程语言。除了类似 PASM 的操作之外,还有一些控制结构和算术运算,这些运算简化了人类读者的语法。所有 PASM 都是合法的 PIR 代码,PIR 几乎只是在原始 PASM 指令上覆盖了一层精美的语法。如果可以,你应该始终使用 PIR 的语法而不是 PASM 的语法,以方便使用。

虽然 PIR 比 PASM 具有更多功能和更好的语法,但它本身并不是高级语言。PIR 仍然非常低级,并不真正适合用于构建大型系统。Parrot 上为语言和应用程序设计者提供了许多其他工具,PIR 实际上只需要在少数领域使用。最终,可能会创建足够多的工具,以至于不再需要直接使用 PIR。

PIR 和高级语言

[编辑 | 编辑源代码]

PIR 旨在帮助实现 Perl、TCL、Python、Ruby 和 PHP 等高级语言。正如我们之前讨论过的,高级语言 (HLL) 与 PIR 有两种可能的联系方式

  1. 我们使用 NQP 和 Parrot 编译器工具 (PCT) 为 HLL 编写编译器。然后将此编译器转换为 PIR,然后转换为 Parrot 字节码。
  2. 我们在 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

贪婪参数

[编辑 | 编辑源代码]

子例程可以接受任意数量的参数,这些参数可以加载到数组中。可以接受可变数量的输入参数的参数称为 `: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)
参数
  • a = `[1, 2, 3]`
  • b = 4
  • c = 5
  • d = `[]`
  • a = 1
  • b = 2
  • c = 3
  • d = `[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文档

[编辑 | 编辑源代码]

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 中的类包含该类的命名空间、初始化器、构造函数和一系列方法。“方法”与普通子例程完全相同,除了三个区别之外。

  1. 它具有 `method` 标志。
  2. 它是使用“点表示法”调用的:`Object.Method()`
  3. 用于调用方法的对象(在点的左侧)存储在方法中的“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 命名空间

[编辑 | 编辑源代码]

每个 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 会考虑角色和多重继承。这使得它非常强大和灵活。

多方法、MultiSubs 和其他关键字

[编辑 | 编辑源代码]

本页上的词汇可能开始变得有点复杂。在这里,我们将列出一些用于描述 Parrot 中事物的术语。

子例程
具有名称和参数列表的基本代码块。
方法
属于特定类并且可以在该类的对象上调用的基本代码块。方法只是具有额外的隐式 self 参数的子例程。
多重分派
当多个子例程具有相同的名称时,Parrot 会选择最适合调用的子例程。
单一分派
当只有一个具有给定名称的子例程时,Parrot 不需要进行任何复杂的排序或选择。
MultiSub
一种 PMC 类型,它存储可以按名称调用并由 Parrot 排序/搜索的子例程集合。
多方法
与 MultiSub 相同,但它被调用为方法而不是子例程。

PIR 宏和常量

[编辑 | 编辑源代码]

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

请注意,局部变量声明现在是如何唯一的?它们依赖于参数的名称、宏的名称以及文件中的其他信息?这是一个可重用的方法,不会导致任何问题。


上一个 鹦鹉虚拟机 下一个
Parrot_Assembly_Language Parrot_Magic_Cookies
华夏公益教科书