Parrot 快速指南



什么是Parrot

当我们将程序输入到传统的Perl中时,它首先被编译成内部表示形式,或字节码;然后将此字节码输入到Perl内部几乎独立的子系统中进行解释。因此,Perl的操作有两个不同的阶段。

  • 编译成字节码和

  • 解释字节码。

这并非Perl独有。遵循此设计的其他语言包括Python、Ruby、Tcl,甚至Java。

我们也知道有一个Java虚拟机(JVM),它是一个与平台无关的执行环境,可以将Java字节码转换为机器语言并执行它。如果您理解这个概念,那么您就会理解Parrot。

Parrot是一个虚拟机,旨在高效地编译和执行解释语言的字节码。Parrot是最终Perl 6编译器的目标,并用作Pugs以及其他各种语言(如Tcl、Ruby、Python等)的后端。

Parrot是用最流行的语言“C”编写的。

Parrot安装

在开始之前,让我们下载Parrot的最新副本并将其安装到我们的机器上。

Parrot下载链接可在Parrot CVS快照中找到。下载Parrot的最新版本,并按照以下步骤进行安装

  • 解压缩下载的文件。

  • 确保您的机器上已安装Perl 5。

  • 现在执行以下操作

% cd parrot
% perl Configure.pl
Parrot Configure
Copyright (C) 2001 Yet Another Society
Since you're running this script, you obviously have
Perl 5 -- I'll be pulling some defaults from its configuration.
...
  • 然后会问您一些关于本地配置的问题;对于每一个问题,您几乎总是可以按回车键/Enter键。

  • 最后,系统会提示您键入 - make test_prog,Parrot将成功构建测试解释器。

  • 现在您应该运行一些测试;因此键入“make test”,您应该看到如下所示的读数

perl t/harness
t/op/basic.....ok,1/2 skipped:label constants unimplemented in
assembler
t/op/string....ok, 1/4 skipped:  I'm unable to write it!
All tests successful, 2 subtests skipped.
Files=2, Tests=6,......

当您阅读本文时,可能会有更多测试,并且一些跳过的测试可能不会跳过,但请确保没有一个测试应该失败!

安装Parrot可执行文件后,您可以查看Parrot“示例”部分中提供的各种示例。您也可以查看Parrot存储库中的examples目录。

Parrot指令格式

Parrot目前可以接受四种形式的执行指令。PIR(Parrot中间表示)旨在由人员编写和编译器生成。它隐藏了一些底层细节,例如参数传递给函数的方式。

PASM(Parrot汇编)位于PIR之下 - 它仍然是人类可读/可写的,并且可以由编译器生成,但是作者必须注意调用约定和寄存器分配等细节。PAST(Parrot抽象语法树)使Parrot能够接受抽象语法树样式的输入 - 这对于编写编译器的人员非常有用。

所有上述输入形式都会在Parrot内部自动转换为PBC(Parrot字节码)。这很像机器码,但是Parrot解释器可以理解。

它并非旨在供人类阅读或编写,但与其他形式不同的是,执行可以立即启动,无需组装阶段。Parrot字节码与平台无关。

指令集

Parrot指令集包括算术和逻辑运算符、比较和分支/跳转(用于实现循环、if...then结构等)、查找和存储全局和词法变量、使用类和对象、调用子程序和方法及其参数、I/O、线程等等。

Parrot中的垃圾回收

与Java虚拟机一样,Parrot也可以让您无需担心内存释放。

  • Parrot提供垃圾回收。

  • Parrot程序不需要显式释放内存。

  • 分配的内存将在不再使用(即不再被引用)时释放。

  • Parrot垃圾收集器定期运行以处理不需要的内存。

Parrot数据类型

Parrot CPU有四种基本数据类型

  • IV

    整数类型;保证足够宽,可以容纳指针。

  • NV

    与体系结构无关的浮点类型。

  • STRING

    抽象的、与编码无关的字符串类型。

  • PMC

    标量。

前三种类型几乎是不言自明的;最后一种类型 - Parrot魔术Cookie,则比较难以理解。

什么是PMC?

PMC代表Parrot魔术Cookie。PMC表示任何复杂的数据结构或类型,包括聚合数据类型(数组、哈希表等)。PMC可以为对其执行的算术、逻辑和字符串操作实现自己的行为,从而允许引入特定于语言的行为。PMC可以内置到Parrot可执行文件中,也可以在需要时动态加载。

Parrot寄存器

当前的Perl 5虚拟机是堆栈机。它通过将值保存在堆栈中来在操作之间进行通信。操作将值加载到堆栈上,执行它们需要执行的操作并将结果放回堆栈上。这很容易使用,但速度很慢。

要将两个数字相加,您需要执行三次堆栈压入和两次堆栈弹出。更糟糕的是,堆栈必须在运行时增长,这意味着在您不想分配内存时分配内存。

因此,Parrot将打破虚拟机的既定传统,并使用寄存器架构,更类似于实际硬件CPU的架构。这还有另一个优点。我们可以将所有关于如何为基于寄存器的CPU编写编译器和优化器的现有文献用于我们的软件CPU!

Parrot为每种类型都有专门的寄存器:32个IV寄存器、32个NV寄存器、32个字符串寄存器和32个PMC寄存器。在Parrot汇编器中,这些分别命名为I1...I32、N1...N32、S1...S32、P1...P32。

现在让我们来看一些汇编代码。我们可以使用set运算符设置这些寄存器

	set I1, 10
	set N1, 3.1415
	set S1, "Hello, Parrot"

所有Parrot操作都具有相同的格式:运算符的名称、目标寄存器,然后是操作数。

Parrot操作

您可以执行各种操作。例如,我们可以打印出寄存器或常量的内容

set I1, 10
print "The contents of register I1 is: "
print I1
print "\n"

上述指令将导致寄存器I1的内容是:10

我们可以对寄存器执行数学运算

# Add the contents of I2 to the contents of I1
add I1, I1, I2
# Multiply I2 by I4 and store in I3
mul I3, I2, I4
# Increment I1 by one
inc I1
# Decrement N3 by 1.5
dec N3, 1.5

我们甚至可以执行一些简单的字符串操作

set S1, "fish"
set S2, "bone"
concat S1, S2       # S1 is now "fishbone"
set S3, "w"
substr S4, S1, 1, 7
concat S3, S4       # S3 is now "wishbone"
length I1, S3       # I1 is now 8

Parrot分支

没有流程控制,代码会变得有点枯燥;首先,Parrot了解分支和标签。branch操作相当于Perl的goto

         branch TERRY
JOHN:    print "fjords\n"
         branch END
MICHAEL: print " pining"
         branch GRAHAM
TERRY:   print "It's"
         branch MICHAEL
GRAHAM:  print " for the "
         branch JOHN
END:     end

它还可以执行简单的测试以查看寄存器是否包含真值

	      set I1, 12
         set I2, 5
         mod I3, I2, I2
         if I3, REMAIND, DIVISOR
REMAIND: print "5 divides 12 with remainder "
         print I3
         branch DONE
DIVISOR: print "5 is an integer divisor of 12"
DONE:    print "\n"
         end

为了比较,以下是Perl中的样子

    $i1 = 12;
    $i2 = 5;
    $i3 = $i1 % $i2;
    if ($i3) {
      print "5 divides 12 with remainder ";
      print $i3;
    } else {
      print "5 is an integer divisor of 12";
    }
    print "\n";
    exit;

Parrot运算符

我们拥有全套数字比较器:eq、ne、lt、gt、le和ge。请注意,您不能对不同类型的参数使用这些运算符;您甚至可能需要向op添加后缀_i或_n,以告诉它您正在使用什么类型的参数,尽管到您阅读本文时,汇编器应该能够为您推断出来。

Parrot编程示例

Parrot编程类似于汇编语言编程,您可以有机会在更低的级别上工作。以下是一些编程示例列表,让您了解Parrot编程的各个方面。

经典的Hello world!

创建一个名为hello.pir的文件,其中包含以下代码

  .sub _main
      print "Hello world!\n"
      end
  .end

然后键入以下命令运行它

  parrot hello.pir

正如预期的那样,这将在控制台上显示文本“Hello world!”,后跟一个换行符(由于\n)。

在上面的示例中,“.sub _main”表示后面的指令构成名为“_main”的子程序,直到遇到“.end”。第二行包含print指令。在这种情况下,我们正在调用接受常量字符串的指令变体。汇编器负责决定为我们使用哪种指令变体。第三行包含“end”指令,这会导致解释器终止。

使用寄存器

我们可以修改hello.pir,首先将字符串Hello world!\n存储在寄存器中,然后使用该寄存器和print指令。

  .sub _main
      set S1, "Hello world!\n"
      print S1
      end
  .end

在这里,我们已经精确地说明了要使用哪个寄存器。但是,通过将S1替换为$S1,我们可以将选择使用哪个寄存器的任务委托给Parrot。也可以使用=表示法而不是编写set指令。

  .sub _main
      $S0 = "Hello world!\n"
      print $S0
      end
  .end

为了使PIR更易于阅读,可以使用命名寄存器。这些稍后将映射到实际的编号寄存器。

  .sub _main
      .local string hello
      hello = "Hello world!\n"
      print hello
      end
  .end

此示例介绍了更多指令和PIR语法。以#开头的行是注释。

求平方和

PIR提供了一些语法糖,使其看起来比汇编更高级。例如

  .sub _main
      # State the number of squares to sum.
      .local int maxnum
      maxnum = 10

      # Some named registers we'll use. 
      # Note how we can declare many
      # registers of the same type on one line.
      .local int i, total, temp
      total = 0

      # Loop to do the sum.
      i = 1
  loop:
      temp = i * i
      total += temp
      inc i
      if i <= maxnum goto loop

      # Output result.
      print "The sum of the first "
      print maxnum
      print " squares is "
      print total
      print ".\n"
      end
  .end

只是另一种更类似于汇编的写法

  temp = i * i

  mul temp, i, i

 if i <= maxnum goto loop

相同

le i, maxnum, loop

  total += temp

相同

add total, temp

通常情况下,每当 Parrot 指令修改寄存器内容时,在汇编形式中编写指令时,该寄存器将是第一个寄存器。

与汇编语言通常一样,循环和选择是根据条件分支语句和标签实现的,如上所示。在汇编编程中,使用 goto 并不是一种不好的形式!

斐波那契数列

斐波那契数列定义如下:取两个数,1 和 1。然后重复将数列中最后两个数加在一起构成下一个数:1、1、2、3、5、8、13,依此类推。斐波那契数 fib(n) 是数列中的第 n 个数。这是一个简单的 Parrot 汇编程序,用于查找前 20 个斐波那契数。

# Some simple code to print some Fibonacci numbers

        print   "The first 20 fibonacci numbers are:\n"
        set     I1, 0
        set     I2, 20
        set     I3, 1
        set     I4, 1
REDO:   eq      I1, I2, DONE, NEXT
NEXT:   set     I5, I4
        add     I4, I3, I4
        set     I3, I5
        print   I3
        print   "\n"
        inc     I1
        branch  REDO
DONE:   end

这是 Perl 中的等效代码。

        print "The first 20 fibonacci numbers are:\n";
        my $i = 0;
        my $target = 20;
        my $a = 1;
        my $b = 1;
        until ($i == $target) {
           my $num = $b;
           $b += $a;
           $a = $num;
           print $a,"\n";
           $i++;
        }

注意: 有趣的是,在 Perl 中打印斐波那契数列最短且无疑最漂亮的方法之一是:`perl -le '$b=1; print $a+=$b while print $b+=$a'`。

递归计算阶乘

在这个例子中,我们定义了一个阶乘函数,并递归调用它来计算阶乘。

 .sub _fact
      # Get input parameter.
      .param int n

      # return (n > 1 ? n * _fact(n - 1) : 1)
      .local int result

      if n > 1 goto recurse
      result = 1
      goto return

  recurse:
      $I0 = n - 1
      result = _fact($I0)
      result *= n

  return:
      .return (result)
  .end


  .sub _main :main
      .local int f, i

      # We'll do factorial 0 to 10.
      i = 0
  loop:
      f = _fact(i)

      print "Factorial of "
      print i
      print " is "
      print f
      print ".\n"

      inc i
      if i <= 10 goto loop

      # That's it.
      end
  .end

让我们首先看看 _fact 子程序。前面略过的一点是为什么子程序的名称都以下划线开头!这样做只是为了表明该标签是全局的,而不是特定子程序的范围。这很重要,因为该标签随后对其他子程序可见。

第一行 .param int n 指定此子程序接受一个整数参数,并且我们希望在子程序的其余部分中使用名称 n 来引用传递给它的寄存器。

除了读取的行之外,大部分后续内容都在之前的示例中见过

result = _fact($I0)

这一行 PIR 实际上代表相当多的 PASM 代码行。首先,寄存器 $I0 中的值被移动到相应的寄存器中,以便它可以作为整数参数被 _fact 函数接收。然后设置其他与调用相关的寄存器,然后调用 _fact。然后,一旦 _fact 返回,_fact 返回的值将被放入名为 result 的寄存器中。

在 _fact 子程序的 .end 之前,使用 .return 指令确保将寄存器中保存的值(名为 result)放入正确的寄存器中,以便调用子程序的代码将其视为返回值。

在 main 中对 _fact 的调用与 _fact 子程序内对 _fact 的递归调用方式相同。唯一剩下的新语法是 :main,它写在 .sub _main 之后。默认情况下,PIR 假设执行从文件中的第一个子程序开始。可以通过标记要从中启动的子程序来更改此行为 :main。

编译成PBC

要将 PIR 编译成字节码,请使用 -o 标志并指定扩展名为 .pbc 的输出文件。

 parrot -o factorial.pbc factorial.pir

PIR与PASM

可以使用以下命令将 PIR 转换为 PASM:

parrot -o hello.pasm hello.pir

最终示例的 PASM 代码如下所示:

  _main:
      set S30, "Hello world!\n"
      print S30
      end

PASM 不处理寄存器分配或提供对命名寄存器的支持。它也没有 .sub 和 .end 指令,而是用指令开头的标签替换它们。

广告