Novos operadores e a sentença if
Alex Sandro Garzão
Posted on April 4, 2024
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.
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
}
}
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.
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
}
}
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 | ||
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.
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
}
}
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.
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
}
}
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 ] | |
L2 | sipush 222 | [ 1, 222 ] |
sipush 333 | [ 1, 222, 333 ] | |
if_icmpge L3 | [ 1 ] | |
iconst 1 | [ 1, 1 ] | |
goto L4 | ||
L4 | iand | [ 1 ] |
ifeq L5 | [ ] | |
getstatic ... | [ getstatic ] | |
ldc "true" | [ getstatic, "true" ] | |
invokevirtual ... | ||
goto L6 | ||
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.
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
}
}
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 ] | |
L2 | sipush 222 | [ 1, 222 ] |
sipush 333 | [ 1, 222, 333 ] | |
if_icmpge L3 | [ 1 ] | |
iconst 1 | [ 1, 1 ] | |
goto L4 | [ 1, 1 ] | |
L4 | iand | [ 1 ] |
ifne L5 | [ ] | |
L5 | iconst 0 | [ 0 ] |
L6 | ifeq L7 | |
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.
Posted on April 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.