Novos operadores e a sentença if
Tags: compiling, go, antlr, braziliandevs
Published: 2024-04-04 23:48:53

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](https://dev.to/alexgarzao/operadores-de-adicao-e-subtracao-3ikm)) 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_](https://en.wikipedia.org/wiki/Stack_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](https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions) 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.
```

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](https://compileroptimizations.com/category/static.htm) 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 ]     | 
| ~~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.
```

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 ]     | 
| ~~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](https://github.com/roscopeco/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](https://github.com/roscopeco/jasm/issues/56) 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](https://github.com/alexgarzao/poj).