Novos operadores e a sentença if

alexgarzao

Alex Sandro Garzão

Posted on April 4, 2024

Novos operadores e a sentença if

A sensação que tenho é que nestas últimas 2 semanas o projeto evoluiu muito. Espero que gostem :-)

Tenho várias novidades, mas aqui vou comentar apenas sobre os novos operadores implementados (multiplicação, divisão, relacionais e booleanos) e a sentença if.

Após resolver a pegadinha da precedência de operadores existente na gramática original (mais informações aqui) foi relativamente fácil implementar os novos operadores. Além do que escrevi vários exemplos em Java para analisar os opcodes gerados pelo JavaC.

Abaixo detalharei mais sobre o JASM (Java Assembly) gerado pelo POJ.

Operadores de multiplicação e divisão

Após o aprendizado gerando o JASM para soma e subtração, foi relativamente fácil gerar o assembly para multiplicação e divisão.

A partir do programa Pascal abaixo:

program MulDivIntegers;
begin
  writeln (8*4/2);
end.
Enter fullscreen mode Exit fullscreen mode

O POJ gera o seguinte JASM:

// Code generated by POJ 0.1
public class mul_div_integers {
    public static main([java/lang/String)V {
        getstatic java/lang/System.out java/io/PrintStream
        ;; multiplica 8 * 4
        sipush 8
        sipush 4
        imul
        ;; divide o resultado por 2
        sipush 2
        idiv
        ;; desempilha e imprime um inteiro
        invokevirtual java/io/PrintStream.println(I)V
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

Vale aqui relembrar que a JVM é uma stack based machine, ou seja, é uma máquina baseada em pilha. Isso quer dizer que suas instruções utilizam uma pilha para realizar as suas operações.

No assembly acima getstatic, sipush, imul, idiv, invokevirtual e return são mnemônicos do Java Assembly. Possuem a seguinte finalidade:

  • getstatic: obtém uma referência de um método estático de uma classe
  • sipush <N>: empilha o inteiro N
  • imul: desempilha dois inteiros e empilha a multiplicação destes valores
  • idiv: desempilha dois inteiros e empilha a divisão destes valores
  • invokevirtual: desempilha dois valores (método a ser invocado e parâmetro) e executa
  • return: finaliza a execução do método

Na prática o par getstatic/invokevirtual serve para invocar o println, responsável por enviar dados para a saída padrão. O "I" em println(I) indica que será exibido um inteiro. E para saber qual método println invocar, o POJ precisa fazer o tracking dos tipos de dados que estão sendo empilhados/desempilhados na JVM.

Na tabela abaixo é possível ver as instruções geradas a partir da expressão "8*4/2" bem como o estado da pilha da JVM após a execução de cada instrução:

Instrução Estado pilha após instrução
sipush 8 [ 8 ]
sipush 4 [ 8, 4 ]
imul [ 32 ]
sipush 2 [ 32, 2 ]
idiv [ 16 ]

No final da execução a pilha contém o resultado esperado de 8*4/2: 16.

Operadores relacionais e a sentença if

Para corretamente implementarmos os operadores relacionais (>, <, >=, <=, = e <>) foi necessário introduzir as instruções da JVM relacionadas à condicionais (ifs) e saltos (goto e labels). Sim, o famigerado goto existe na JVM, e faz todo sentido existir :-)

Vamos a um exemplo com if/then/else. A partir do programa Pascal abaixo:

program IfWithIntegers;
begin
  if ( 111 < 222 ) then
    write('true')
  else
    write('false');
end.
Enter fullscreen mode Exit fullscreen mode

O POJ gera o seguinte JASM:

// Code generated by POJ 0.1
public class if_with_integers {
    public static main([java/lang/String)V {
        ;; se 111 >= 222 salta para L1
        sipush 111
        sipush 222
        if_icmpge L1
        ;; imprime true
        getstatic java/lang/System.out java/io/PrintStream
        ldc "true"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
        goto L2
    L1: ;; imprime false
        getstatic java/lang/System.out java/io/PrintStream        
        ldc "false"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
    L2: return
    }
}
Enter fullscreen mode Exit fullscreen mode

Em relação ao exemplo anterior agora estamos utilizando novas instruções:

  • if_icmpge <label>: desempilha dois inteiros e salta para o label indicado se o primeiro valor for maior ou igual ao segundo
  • labels: no exemplo acima "L1:" e "L2:"
  • goto <label>: salta para o label indicado

A instrução if_icmpge faz parte de um conjunto amplo de instruções de teste e salto. O "ge" (de if_icmpge) é a abreviatura de "greater or equal". Outras variações são gt (greater than), eq (equal), ne (not equal), lt (less than) e le (less or equal).

Na tabela abaixo é possível ver as instruções geradas a partir do assembly acima bem como o estado da pilha da JVM após a execução de cada instrução:

Label Instrução Estado pilha após instrução
sipush 111 [ 111 ]
sipush 222 [ 111, 222 ]
if_icmpge L1 [ ]
getstatic ... [ getstatic ]
ldc "true" [ getstatic, "true" ]
invokevirtual ...
goto L2
L1 getstatic ...
ldc "false"
invokevirtual ...
L2 return

As instruções marcadas desta forma não são executadas durante o fluxo de execução deste programa.

No final da execução o programa exibe a saída esperada: "true".

Vale ressaltar que a condição utilizada na geração do assembly (ge) é a inversa do programa em pascal (<). Esta técnica facilita a geração do assembly para os casos onde a sentença if somente tem o então (then), sem o senão (else).

Vamos a um exemplo com if/then (sem o else). A partir do programa Pascal abaixo:

program IfWithIntegers;
begin
  if ( 111 < 222 ) then
    write('true');
end.
Enter fullscreen mode Exit fullscreen mode

O POJ gera o seguinte JASM:

// Code generated by POJ 0.1
public class if_with_integers {
    public static main([java/lang/String)V {
        ;; se 111 >= 222 salta para L1
        sipush 111
        sipush 222
        if_icmpge L1
        ;; imprime true
        getstatic java/lang/System.out java/io/PrintStream
        ldc "true"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
        goto L2
  L1:L2:return
    }
}
Enter fullscreen mode Exit fullscreen mode

Os mais atentos podem ter notado que o último goto (goto L2) é desnecessário. O código gerado está funcionalmente correto, apesar de não otimizado. Não só esta otimização bem como várias outras podem ser implementadas futuramente no POJ.

Operadores booleanos and e or

Para implementarmos os operadores booleanos (and e or) foi necessário introduzir as seguintes instruções:

  • iand: desempilha dois inteiros, executa o and e empilha o resultado
  • ior: desempilha dois inteiros, executa o or e empilha o resultado
  • ifeq <label>: desempilha o último valor, e caso igual a 0 salta para o label indicado
  • iconst <N>: empilha o inteiro N

Vamos a um exemplo. A partir do programa Pascal abaixo:

program IfWithAnd;
begin
  if ( 111 < 222) and ( 222 < 333 ) then
    write('true')
  else
    write('false');
end.
Enter fullscreen mode Exit fullscreen mode

O POJ gera o seguinte JASM:

// Code generated by POJ 0.1
public class if_with_and {
    public static main([java/lang/String)V {
        sipush 111    ;; se 111 >= 222 salta para L1
        sipush 222
        if_icmpge L1
        iconst 1      ;; carrega true (1) na pilha
        goto L2
    L1: iconst 0      ;; carrega false (0) na pilha
    L2: sipush 222    ;; se 222 >= 333 salta para L3
        sipush 333
        if_icmpge L3
        iconst 1      ;; carrega true (1) na pilha
        goto L4
    L3: iconst 0      ;; carrega false (0) na pilha
    L4: iand          ;; se algum teste falhou salta para L5
        ifeq L5
        ;; imprime true
        getstatic java/lang/System.out java/io/PrintStream
        ldc "true"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
        goto L6
    L5: ;; imprime false
        getstatic java/lang/System.out java/io/PrintStream
        ldc "false"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
    L6: return
    }
}
Enter fullscreen mode Exit fullscreen mode

Vale aqui ressaltar que a JVM não possui os tipos booleanos true e false. Estes tipos são emulados através dos inteiros 1 e 0, respectivamente. Para tal utilizamos as instruções "iconst 1" e "iconst 0".

Na tabela abaixo é possível ver as instruções geradas a partir do assembly acima bem como o estado da pilha da JVM após a execução de cada instrução:

Label Instrução Estado pilha após instrução
sipush 111 [ 111 ]
sipush 222 [ 111, 222 ]
if_icmpge L1 [ ]
iconst 1 [ 1 ]
goto L2 [ 1 ]
L1 iconst 0
L2 sipush 222 [ 1, 222 ]
sipush 333 [ 1, 222, 333 ]
if_icmpge L3 [ 1 ]
iconst 1 [ 1, 1 ]
goto L4
L3 iconst 0
L4 iand [ 1 ]
ifeq L5 [ ]
getstatic ... [ getstatic ]
ldc "true" [ getstatic, "true" ]
invokevirtual ...
goto L6
L5 getstatic ...
ldc "false"
invokevirtual ...
L6 return

No final da execução o programa exibe a saída esperada: "true".

Operador booleano not

Até aqui falamos sobre os operadores and e or. E o operador not? Vamos a um exemplo.

A partir do programa Pascal abaixo:

program IfWithAndNot;
begin
  if not(( 111 < 222) and ( 222 < 333 )) then
    write('true')
  else
    write('false');
end.
Enter fullscreen mode Exit fullscreen mode

O POJ gera o seguinte JASM:

// Code generated by POJ 0.1
public class if_with_and_not {
    public static main([java/lang/String)V {
        sipush 111   ;; se 111 >= 222 salta para L1
        sipush 222
        if_icmpge L1
        iconst 1     ;; carrega true (1) na pilha
        goto L2
    L1: iconst 0     ;; carrega false (0) na pilha
    L2: sipush 222   ;; se 222 >= 333 salta para L3
        sipush 333
        if_icmpge L3
        iconst 1     ;; carrega true (1) na pilha
        goto L4
    L3: iconst 0     ;; carrega false (0) na pilha
    L4: iand         ;; se testes foram ok salta para L5
        ifne L5
        iconst 1     ;; carrega true (1) na pilha
        goto L6
    L5: iconst 0     ;; carrega false (0) na pilha
    L6: ifeq L7
        ;; imprime true
        getstatic java/lang/System.out java/io/PrintStream
        ldc "true"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
        goto L8
    L7: ;; imprime false
        getstatic java/lang/System.out java/io/PrintStream
        ldc "false"
        invokevirtual java/io/PrintStream.print(java/lang/String)V
    L8: return
    }
}
Enter fullscreen mode Exit fullscreen mode

Vale aqui ressaltar que apesar da JVM ter as instruções iand e ior, ela não possui uma instrução como inot. Esta "negação" da operação deve ser implementada com ifne e labels. O ifne (if not equal) salta para o label indicado caso o último valor da pilha não seja zero.

Na tabela abaixo é possível ver as instruções geradas a partir do assembly acima bem como o estado da pilha da JVM após a execução de cada instrução:

Label Instrução Estado pilha após instrução
sipush 111 [ 111 ]
sipush 222 [ 111, 222 ]
if_icmpge L1 [ ]
iconst 1 [ 1 ]
goto L2 [ 1 ]
L1 iconst 0
L2 sipush 222 [ 1, 222 ]
sipush 333 [ 1, 222, 333 ]
if_icmpge L3 [ 1 ]
iconst 1 [ 1, 1 ]
goto L4 [ 1, 1 ]
L3 iconst 0
L4 iand [ 1 ]
ifne L5 [ ]
iconst 1
goto L6
L5 iconst 0 [ 0 ]
L6 ifeq L7
getstatic ...
ldc "true"
invokevirtual ...
goto L8
L7 getstatic ... [ getstatic ]
ldc "false" [ getstatic, "false" ]
invokevirtual ...
L8 return

No final da execução o programa exibe a saída esperada: "false".

"Perrengue" com JASM e uma grata surpresa

No início do projeto tive que decidir entre gerar o arquivo class diretamente ou utilizar um assemblador de Java Assembly. Optei por utilizar o JASM, um assemblador Java em que rapidamente consegui rodar alguns exemplos.

Eis que em pleno feriado (29/3, sexta-feira santa), após gerar o assembly para o operador not, ao executar o JASM ele simplesmente gerava um "NullPointerException". Fiz alguns testes mas não identifiquei nada estranho no assembly gerado pelo POJ.

Como é o usual entrei no github do JASM e tentei identificar algum bug relacionado, mas não havia nada. Como o projeto é pouco movimentado eu já estava esperando por alguma mudança drástica no POJ: ter que escolher outro assemblador ou gerar o arquivo class diretamente.

Antes que alguém diga "o JASM é open source, contribui lá". Até pensei nisso, mas eu não sei programar em Java, e sendo bem sincero, não é uma das linguagens que eu tenha interesse em aprender :-)

Bom, abri uma issue no JASM, e para minha surpresa, em menos de 1 hora o mantenedor respondeu e identificou o problema. Inicialmente eu estava utilizando a instrução "iconst_1" (ao invés de "iconst 1"). A JVM tem a instrução "iconst_1", mas como é uma otimização da instrução "iconst 1", o JASM não a implementa. Enfim, ajustei para "iconst 1" e tudo funcionou.

Maiores informações

O repositório com o código completo do projeto e a sua documentação está aqui.

💖 💪 🙅 🚩
alexgarzao
Alex Sandro Garzão

Posted on April 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Lendo dados da entrada padrão (stdin)
Novos operadores e a sentença if
compiling Novos operadores e a sentença if

April 4, 2024