Parrot - Guia Rápido

Quando alimentamos nosso programa em Perl convencional, ele é primeiro compilado em uma representação interna, ou bytecode; este bytecode é então alimentado em um subsistema quase separado dentro do Perl para ser interpretado. Portanto, existem duas fases distintas de operação do Perl:

  • Compilação para bytecode e

  • Interpretação de bytecode.

Isso não é exclusivo do Perl. Outras linguagens que seguem esse design incluem Python, Ruby, Tcl e até mesmo Java.

Também sabemos que existe uma Java Virtual Machine (JVM) que é um ambiente de execução independente de plataforma que converte bytecode Java em linguagem de máquina e o executa. Se você entender esse conceito, entenderá o Parrot.

Parroté uma máquina virtual projetada para compilar e executar bytecode de maneira eficiente para linguagens interpretadas. Parrot é o alvo para o compilador Perl 6 final e é usado como backend para Pugs, bem como uma variedade de outras linguagens como Tcl, Ruby, Python etc.

Parrot foi escrito usando a linguagem mais popular "C".

Antes de começar, vamos baixar uma cópia mais recente do Parrot e instalá-la em nossa máquina.

O link para download do Parrot está disponível no Parrot CVS Snapshot . Baixe a última versão do Parrot e para instalá-lo siga os seguintes passos:

  • Descompacte e descompacte o arquivo baixado.

  • Certifique-se de que já possui o Perl 5 instalado em sua máquina.

  • Agora faça o seguinte:

% 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.
...
  • Em seguida, você responderá a uma série de perguntas sobre sua configuração local; você quase sempre pode clicar em Enter para cada um.

  • Finalmente, você será instruído a digitar - make test_prog, e o Parrot construirá com sucesso o interpretador de teste.

  • Agora você deve executar alguns testes; então digite 'make test' e você verá uma leitura como a seguinte:

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

No momento em que você ler isto, poderá haver mais testes, e alguns dos que foram ignorados podem não ser ignorados, mas certifique-se de que nenhum deles falhará!

Assim que tiver um executável parrot instalado, você pode verificar os vários tipos de exemplos fornecidos na seção 'Exemplos' do Parrot . Além disso, você pode verificar o diretório de exemplos no repositório do parrot.

Atualmente, o Parrot pode aceitar instruções para executar em quatro formas. PIR (Parrot Intermediate Representation) foi projetado para ser escrito por pessoas e gerado por compiladores. Ele oculta alguns detalhes de baixo nível, como a forma como os parâmetros são passados ​​para as funções.

PASM (Parrot Assembly) está um nível abaixo de PIR - ainda é legível / gravável por humanos e pode ser gerado por um compilador, mas o autor tem que cuidar de detalhes como convenções de chamada e alocação de registro. PAST (Parrot Abstract Syntax Tree) permite que o Parrot aceite uma entrada de estilo de árvore de sintaxe abstrata - útil para quem escreve compiladores.

Todas as formas de entrada acima são automaticamente convertidas dentro do Parrot para PBC (Bytecode do Parrot). É muito parecido com o código de máquina, mas entendido pelo interpretador Parrot.

Não se destina a ser legível ou gravável por humanos, mas ao contrário de outros formulários, a execução pode começar imediatamente sem a necessidade de uma fase de montagem. O bytecode do Parrot é independente de plataforma.

O conjunto de instruções

O conjunto de instruções Parrot inclui operadores aritméticos e lógicos, comparar e desviar / saltar (para implementar loops, se ... então construir, etc.), encontrar e armazenar variáveis ​​globais e lexicais, trabalhar com classes e objetos, chamar sub-rotinas e métodos. com seus parâmetros, I / O, threads e muito mais.

Assim como o Java Virtual Machine, o Parrot também evita que você se preocupe com a desalocação de memória.

  • O Parrot fornece coleta de lixo.

  • Os programas Parrot não precisam liberar memória explicitamente.

  • A memória alocada será liberada quando não estiver mais em uso, ou seja, não for mais referenciada.

  • O Parrot Garbage Collector é executado periodicamente para cuidar da memória indesejada.

A CPU Parrot tem quatro tipos básicos de dados:

  • IV

    Um tipo inteiro; garantidamente largo o suficiente para segurar um ponteiro.

  • NV

    Um tipo de ponto flutuante independente de arquitetura.

  • STRING

    Um tipo de string abstraído e independente de codificação.

  • PMC

    Um escalar.

Os três primeiros tipos são bastante autoexplicativos; o tipo final - Parrot Magic Cookies, são um pouco mais difíceis de entender.

O que são PMCs?

PMC significa Parrot Magic Cookie. PMCs representam qualquer tipo ou estrutura de dados complexos, incluindo tipos de dados agregados (matrizes, tabelas de hash, etc.). Um PMC pode implementar seu próprio comportamento para operações aritméticas, lógicas e de string executadas nele, permitindo que um comportamento específico da linguagem seja introduzido. Os PMCs podem ser integrados ao executável do Parrot ou carregados dinamicamente quando necessário.

A máquina virtual Perl 5 atual é uma máquina de pilha. Ele comunica valores entre as operações, mantendo-os em uma pilha. As operações carregam valores na pilha, fazem o que for necessário e colocam o resultado de volta na pilha. Isso é fácil de trabalhar, mas é lento.

Para somar dois números, você precisa realizar três empilhamento de pilha e dois empilhamento de pilha. Pior, a pilha precisa crescer em tempo de execução, e isso significa alocar memória apenas quando você não quiser alocá-la.

Portanto, o Parrot vai quebrar a tradição estabelecida para máquinas virtuais e usar uma arquitetura de registro, mais semelhante à arquitetura de uma CPU de hardware real. Isso tem outra vantagem. Podemos usar toda a literatura existente sobre como escrever compiladores e otimizadores para CPUs baseadas em registro para nossa CPU de software!

A Parrot tem registros especializados para cada tipo: 32 registros IV, 32 registros NV, 32 registros de string e 32 registros PMC. No montador Parrot, são denominados I1 ... I32, N1 ... N32, S1 ... S32, P1 ... P32, respectivamente.

Agora vamos dar uma olhada em algum montador. Podemos definir esses registros com o operador set:

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

Todas as operações do Parrot têm o mesmo formato: o nome do operador, o registrador de destino e depois os operandos.

Existem várias operações que você pode realizar. Por exemplo, podemos imprimir o conteúdo de um registro ou constante:

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

As instruções acima resultarão em O conteúdo do registro I1 é: 10

Podemos realizar operações matemáticas em registradores:

# 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

Podemos até realizar algumas manipulações de string simples:

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

O código fica um pouco enfadonho sem controle de fluxo; para começar, o Parrot conhece ramificações e rótulos. O branch op é equivalente ao goto do 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

Ele também pode realizar testes simples para ver se um registro contém um valor verdadeiro:

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

Veja como ficaria em Perl, para comparação:

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

Temos uma gama completa de comparadores numéricos: eq, ne, lt, gt, le e ge. Observe que você não pode usar esses operadores em argumentos de tipos diferentes; você pode até precisar adicionar o sufixo _i ou _n ao op, para dizer que tipo de argumento você está usando, embora o montador deva adivinhar isso para você, no momento em que você ler isto.

A programação do Parrot é semelhante à programação em linguagem assembly e você tem a chance de trabalhar em um nível inferior. Aqui está a lista de exemplos de programação para torná-lo ciente dos vários aspectos da Programação do Parrot.

Olá, mundo clássico!

Crie um arquivo chamado hello.pir que contém o seguinte código:

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

Em seguida, execute-o digitando:

parrot hello.pir

Como esperado, isso exibirá o texto 'Olá, mundo!' no console, seguido por uma nova linha (devido ao \ n).

Neste exemplo acima, '.sub _main' afirma que as instruções que seguem formam uma sub-rotina chamada '_main', até que um '.end' seja encontrado. A segunda linha contém a instrução de impressão. Nesse caso, estamos chamando a variante da instrução que aceita uma string constante. O montador se encarrega de decidir qual variante da instrução usar para nós. A terceira linha contém a instrução 'fim', que faz com que o interpretador termine.

Usando registros

Podemos modificar hello.pir para primeiro armazenar a string Hello world! \ N em um registro e então usar esse registro com a instrução de impressão.

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

Aqui declaramos exatamente qual registro usar. No entanto, ao substituir S1 por $ S1, podemos delegar ao Parrot a escolha de qual registro usar. Também é possível usar uma notação = em vez de escrever a instrução de conjunto.

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

Para tornar o PIR ainda mais legível, podem ser usados ​​registradores nomeados. Estes são posteriormente mapeados para registros reais numerados.

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

A diretiva '.local' indica que o registro nomeado só é necessário dentro da unidade de compilação atual (ou seja, entre .sub e .end). Seguir '.local' é um tipo. Isso pode ser int (para registros I), float (para registros N), string (para registros S), pmc (para registros P) ou o nome de um tipo PMC.

Somando quadrados

Este exemplo apresenta mais algumas instruções e sintaxe PIR. As linhas que começam com # são comentários.

.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

O PIR fornece um pouco de açúcar sintático que o faz parecer mais de alto nível do que o assembly. Por exemplo:

temp = i * i

É apenas outra forma de escrever, mais assembly:

mul temp, i, i

E:

if i <= maxnum goto loop

É o mesmo que:

le i, maxnum, loop

E:

total += temp

É o mesmo que:

add total, temp

Via de regra, sempre que uma instrução Parrot modifica o conteúdo de um registro, este será o primeiro registro ao escrever a instrução na forma de montagem.

Como é usual em linguagens assembly, loops e seleções são implementados em termos de declarações e rótulos de desvio condicional, conforme mostrado acima. A programação de montagem é um lugar onde usar goto não é uma forma ruim!

Números de Fibonacci

A série de Fibonacci é definida assim: pegue dois números, 1 e 1. Em seguida, some repetidamente os dois últimos números da série para fazer o próximo: 1, 1, 2, 3, 5, 8, 13 e assim por diante . O número de Fibonacci fib (n) é o enésimo número da série. Aqui está um programa montador Parrot simples que encontra os primeiros 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 é o código equivalente em 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 um bom ponto de interesse, uma das maneiras mais curtas e certamente as mais bonitas de imprimir uma série de Fibonacci em Perl é perl -le '$ b = 1; print $ a + = $ b enquanto print $ b + = $ a '.

Computando recursivamente o fatorial

Neste exemplo, definimos uma função fatorial e a chamamos recursivamente para calcular o fatorial.

.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

Vejamos primeiro o subfacto _fact. Um ponto que foi esquecido anteriormente é porque os nomes das sub-rotinas começam todos com um sublinhado! Isso é feito simplesmente como uma forma de mostrar que o rótulo é global, em vez de ter como escopo uma sub-rotina específica. Isso é significativo porque o rótulo fica então visível para outras sub-rotinas.

A primeira linha, .param int n, especifica que esta sub-rotina leva um parâmetro inteiro e que gostaríamos de nos referir ao registro que foi passado pelo nome n para o resto da sub.

Muito do que se segue foi visto em exemplos anteriores, além da leitura de linha:

result = _fact($I0)

Esta única linha de PIR realmente representa algumas linhas de PASM. Primeiro, o valor no registro $ I0 é movido para o registro apropriado para ser recebido como um parâmetro inteiro pela função _fact. Outros registros relacionados à chamada são configurados, seguidos por _fact sendo invocado. Então, uma vez que _fact retorna, o valor retornado por _fact é colocado no registro dado o nome resultado.

Logo antes do final do sub _fact, uma diretiva .return é usada para garantir o valor mantido no registro; o resultado nomeado é colocado no registro correto para ser visto como um valor de retorno pelo código que chama o sub.

A chamada para _fact em main funciona da mesma maneira que a chamada recursiva para _fact dentro do próprio sub _fact. O único pedaço restante da nova sintaxe é: main, escrito após .sub _main. Por padrão, o PIR assume que a execução começa com a primeira sub-rotina no arquivo. Este comportamento pode ser alterado marcando o sub para começar com: principal.

Compilando para PBC

Para compilar PIR para bytecode, use o sinalizador -o e especifique um arquivo de saída com a extensão .pbc.

parrot -o factorial.pbc factorial.pir

PIR vs. PASM

PIR pode ser transformado em PASM executando:

parrot -o hello.pasm hello.pir

O PASM para o exemplo final se parece com isto:

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

O PASM não controla a alocação de registros nem fornece suporte para registros nomeados. Ele também não tem as diretivas .sub e .end, substituindo-as por um rótulo no início das instruções.