Parrot - Guía rápida

Cuando alimentamos nuestro programa en Perl convencional, primero se compila en una representación interna o código de bytes; este bytecode luego se alimenta a un subsistema casi separado dentro de Perl para ser interpretado. Entonces, hay dos fases distintas de la operación de Perl:

  • Compilación a bytecode y

  • Interpretación de bytecode.

Esto no es exclusivo de Perl. Otros lenguajes que siguen este diseño incluyen Python, Ruby, Tcl e incluso Java.

También sabemos que existe una máquina virtual Java (JVM) que es un entorno de ejecución independiente de la plataforma que convierte el código de bytes de Java en un lenguaje de máquina y lo ejecuta. Si comprende este concepto, comprenderá Parrot.

Parrotes una máquina virtual diseñada para compilar y ejecutar de manera eficiente bytecode para lenguajes interpretados. Parrot es el objetivo del compilador final de Perl 6 y se utiliza como backend para Pugs, así como para una variedad de otros lenguajes como Tcl, Ruby, Python, etc.

Parrot se ha escrito utilizando el lenguaje "C" más popular.

Antes de comenzar, descarguemos una copia más reciente de Parrot e instálela en nuestra máquina.

El enlace de descarga de Parrot está disponible en Parrot CVS Snapshot . Descarga la última versión de Parrot y para instalarla sigue los siguientes pasos:

  • Descomprime y descomprime el archivo descargado.

  • Asegúrese de que ya tiene Perl 5 instalado en su máquina.

  • Ahora haz lo siguiente:

% 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.
...
  • Luego se le hará una serie de preguntas sobre su configuración local; casi siempre puede presionar regresar / ingresar para cada uno.

  • Finalmente, se le pedirá que escriba - make test_prog, y Parrot construirá con éxito el intérprete de prueba.

  • Ahora deberías ejecutar algunas pruebas; así que escriba 'hacer prueba' y debería ver una lectura como la siguiente:

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,......

Para cuando lea esto, podría haber más pruebas, y algunas de las que se saltaron podrían no saltarse, ¡pero asegúrese de que ninguna de ellas falle!

Una vez que tenga instalado un ejecutable de Parrot , puede consultar los distintos tipos de ejemplos que se dan en la sección 'Ejemplos' de Parrot . También puede consultar el directorio de ejemplos en el repositorio de loros.

Parrot actualmente puede aceptar instrucciones para ejecutar en cuatro formas. PIR (Parrot Intermediate Representation) está diseñado para ser escrito por personas y generado por compiladores. Oculta algunos detalles de bajo nivel, como la forma en que los parámetros se pasan a las funciones.

PASM (Parrot Assembly) está un nivel por debajo de PIR: aún es legible / escribible por humanos y puede ser generado por un compilador, pero el autor debe cuidar detalles como convenciones de llamadas y asignación de registros. PAST (Parrot Abstract Syntax Tree) permite a Parrot aceptar una entrada de estilo de árbol de sintaxis abstracta, útil para quienes escriben compiladores.

Todas las formas de entrada anteriores se convierten automáticamente dentro de Parrot a PBC (Parrot Bytecode). Esto es muy parecido al código de máquina, pero el intérprete de Parrot lo entiende.

No está destinado a ser legible o escribible por humanos, pero a diferencia de los otros formularios, la ejecución puede comenzar inmediatamente sin la necesidad de una fase de ensamblaje. El código de bytes de Parrot es independiente de la plataforma.

El conjunto de instrucciones

El conjunto de instrucciones de Parrot incluye operadores aritméticos y lógicos, comparar y bifurcar / saltar (para implementar bucles, si ... luego construye, etc.), encontrar y almacenar variables globales y léxicas, trabajar con clases y objetos, llamar subrutinas y métodos a lo largo de con sus parámetros, E / S, subprocesos y más.

Al igual que Java Virtual Machine, Parrot también evita que se preocupe por la desasignación de memoria.

  • Parrot proporciona recolección de basura.

  • Los programas Parrot no necesitan liberar memoria explícitamente.

  • La memoria asignada se liberará cuando ya no esté en uso, es decir, ya no se haga referencia a ella.

  • Parrot Garbage Collector se ejecuta periódicamente para cuidar la memoria no deseada.

La CPU Parrot tiene cuatro tipos de datos básicos:

  • IV

    Un tipo entero; garantizado para ser lo suficientemente ancho para sostener un puntero.

  • NV

    Un tipo de punto flotante independiente de la arquitectura.

  • STRING

    Un tipo de cadena abstraído, independiente de la codificación.

  • PMC

    Un escalar.

Los primeros tres tipos se explican por sí mismos; el último tipo, Parrot Magic Cookies, son un poco más difíciles de entender.

¿Qué son las PMC?

PMC son las siglas de Parrot Magic Cookie. Los PMC representan cualquier tipo o estructura de datos complejos, incluidos los tipos de datos agregados (matrices, tablas hash, etc.). Un PMC puede implementar su propio comportamiento para operaciones aritméticas, lógicas y de cadenas que se realicen en él, lo que permite introducir un comportamiento específico del lenguaje. Los PMC pueden integrarse en el ejecutable de Parrot o cargarse dinámicamente cuando se necesiten.

La máquina virtual de Perl 5 actual es una máquina de pila. Comunica valores entre operaciones manteniéndolos en una pila. Las operaciones cargan valores en la pila, hacen lo que sea necesario y devuelven el resultado a la pila. Es fácil trabajar con esto, pero es lento.

Para sumar dos números, debe realizar tres empujes de pila y dos estallidos de pila. Peor aún, la pila tiene que crecer en tiempo de ejecución, y eso significa asignar memoria justo cuando no quieres asignarla.

Así que Parrot va a romper la tradición establecida de las máquinas virtuales y utilizará una arquitectura de registro, más parecida a la arquitectura de una CPU de hardware real. Esto tiene otra ventaja. ¡Podemos usar toda la literatura existente sobre cómo escribir compiladores y optimizadores para CPU basadas en registros para nuestra CPU de software!

Parrot tiene registros especializados para cada tipo: 32 registros IV, 32 registros NV, 32 registros de cadena y 32 registros PMC. En el ensamblador Parrot, estos se denominan I1 ... I32, N1 ... N32, S1 ... S32, P1 ... P32 respectivamente.

Ahora veamos algún ensamblador. Podemos configurar estos registros con el operador set:

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

Todas las operaciones de Parrot tienen el mismo formato: el nombre del operador, el registro de destino y luego los operandos.

Hay una variedad de operaciones que puede realizar. Por ejemplo, podemos imprimir el contenido de un registro o una constante:

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

Las instrucciones anteriores darán como resultado El contenido del registro I1 es: 10

Podemos realizar operaciones matemáticas sobre registros:

# 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

Incluso podemos realizar una simple manipulación de cadenas:

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

El código se vuelve un poco aburrido sin control de flujo; Para empezar, Parrot sabe sobre ramificaciones y etiquetas. El branch op es equivalente al goto de Perl:

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

También puede realizar pruebas simples para ver si un registro contiene un valor verdadero:

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

Así es como se vería en Perl, a modo de comparación:

$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;

Operador de loros

Disponemos de la gama completa de comparadores numéricos: eq, ne, lt, gt, le y ge. Tenga en cuenta que no puede utilizar estos operadores en argumentos de tipos dispares; incluso puede que necesite agregar el sufijo _i o _n a la operación, para decirle qué tipo de argumento está usando, aunque el ensamblador debe adivinarlo por usted cuando lo lea.

La programación de Parrot es similar a la programación en lenguaje ensamblador y tienes la oportunidad de trabajar en un nivel inferior. Aquí está la lista de ejemplos de programación para que conozca los diversos aspectos de la programación Parrot.

¡Hola mundo clásico!

Cree un archivo llamado hello.pir que contenga el siguiente código:

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

Luego ejecútelo escribiendo:

parrot hello.pir

Como era de esperar, esto mostrará el texto '¡Hola mundo!' en la consola, seguida de una nueva línea (debido al \ n).

En este ejemplo anterior, '.sub _main' indica que las instrucciones que siguen forman una subrutina llamada '_main', hasta que se encuentra un '.end'. La segunda línea contiene la instrucción de impresión. En este caso, llamamos a la variante de la instrucción que acepta una cadena constante. El ensamblador se encarga de decidir qué variante de la instrucción utilizar para nosotros. La tercera línea contiene la instrucción 'end', que hace que el intérprete termine.

Usando registros

Podemos modificar hello.pir para almacenar primero la cadena ¡Hola mundo! \ N en un registro y luego usar ese registro con la instrucción de impresión.

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

Aquí hemos indicado exactamente qué registro utilizar. Sin embargo, reemplazando S1 con $ S1 podemos delegar la elección de qué registro usar a Parrot. También es posible utilizar una notación = en lugar de escribir la instrucción establecida.

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

Para que PIR sea aún más legible, se pueden usar registros con nombre. Estos se asignan posteriormente a registros numerados reales.

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

La directiva '.local' indica que el registro nombrado solo se necesita dentro de la unidad de compilación actual (es decir, entre .sub y .end). Después de '.local' es un tipo. Puede ser int (para registros I), float (para registros N), string (para registros S), pmc (para registros P) o el nombre de un tipo de PMC.

Sumar cuadrados

Este ejemplo presenta más instrucciones y sintaxis PIR. Las líneas que comienzan con # son comentarios.

.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

PIR proporciona un poco de azúcar sintáctico que lo hace parecer de más alto nivel que el ensamblaje. Por ejemplo:

temp = i * i

Es solo otra forma de escribir más ensamblado:

mul temp, i, i

Y:

if i <= maxnum goto loop

Es lo mismo que:

le i, maxnum, loop

Y:

total += temp

Es lo mismo que:

add total, temp

Como regla, siempre que una instrucción Parrot modifica el contenido de un registro, ese será el primer registro al escribir la instrucción en forma ensamblada.

Como es habitual en los lenguajes ensambladores, los bucles y las selecciones se implementan en términos de declaraciones y etiquetas de ramas condicionales, como se muestra arriba. ¡La programación de ensamblaje es un lugar donde usar goto no es una mala forma!

Números de Fibonacci

La serie de Fibonacci se define así: tome dos números, 1 y 1. Luego sume repetidamente los dos últimos números de la serie para hacer el siguiente: 1, 1, 2, 3, 5, 8, 13, y así sucesivamente. . El número de Fibonacci fib (n) es el número n de la serie. Aquí hay un programa ensamblador de Parrot simple que encuentra los primeros 20 números de Fibonacci:

# 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

Este es el código equivalente en 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++;
        }

NOTE:Como buen punto de interés, una de las formas más cortas y ciertamente más hermosas de imprimir una serie de Fibonacci en Perl es perl -le '$ b = 1; imprimir $ a + = $ b mientras imprime $ b + = $ a '.

Calculando factorial recursivamente

En este ejemplo definimos una función factorial y la llamamos recursivamente para calcular factorial.

.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

Veamos primero el sub _fact. Un punto que se pasó por alto anteriormente es por qué los nombres de las subrutinas, ¡todos comienzan con un guión bajo! Esto se hace simplemente como una forma de mostrar que la etiqueta es global en lugar de estar limitada a una subrutina en particular. Esto es importante ya que la etiqueta es visible para otras subrutinas.

La primera línea, .param int n, especifica que esta subrutina toma un parámetro entero y que nos gustaría hacer referencia al registro que se le pasó con el nombre n para el resto del sub.

Mucho de lo que sigue se ha visto en ejemplos anteriores, además de la lectura de líneas:

result = _fact($I0)

Esta única línea de PIR en realidad representa bastantes líneas de PASM. Primero, el valor en el registro $ I0 se mueve al registro apropiado para que la función _fact lo reciba como un parámetro entero. A continuación, se configuran otros registros relacionados con la llamada, seguidos de la invocación de _fact. Luego, una vez que _fact regresa, el valor devuelto por _fact se coloca en el registro dado el nombre de resultado.

Justo antes del .end del sub _fact, se utiliza una directiva .return para garantizar el valor que se mantiene en el registro; El resultado con nombre se coloca en el registro correcto para que el código que llama al sub lo vea como un valor de retorno.

La llamada a _fact en main funciona de la misma manera que la llamada recursiva a _fact dentro del propio sub _fact. El único bit restante de nueva sintaxis es: main, escrito después de .sub _main. Por defecto, PIR asume que la ejecución comienza con el primer sub del archivo. Este comportamiento se puede cambiar marcando el sub para comenzar con: main.

Compilando a PBC

Para compilar PIR en bytecode, use la marca -o y especifique un archivo de salida con la extensión .pbc.

parrot -o factorial.pbc factorial.pir

PIR frente a PASM

PIR se puede convertir en PASM ejecutando:

parrot -o hello.pasm hello.pir

El PASM para el ejemplo final se ve así:

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

PASM no maneja la asignación de registros ni brinda soporte para registros con nombre. Tampoco tiene las directivas .sub y .end, sino que las reemplaza con una etiqueta al comienzo de las instrucciones.