Para quem não está acompanhando, o POJ (Pascal on the JVM) é um compilador que transforma um subset de Pascal para JASM (Java Assembly) de forma que possamos usar a JVM como ambiente de execução.
Na última postagem foi adicionado suporte às estruturas de repetição repeat, while e for.
Como estamos compilando para a JVM faz-se necessário detalhar o funcionamento de vários pontos desta incrível máquina virtual. Com isso, em vários momentos eu detalho o funcionamento interno da JVM bem como algumas das suas instruções (Java Assembly).
Melhorias na saída de erros
Sempre que existia um erro léxico, sintático ou semântico no código Pascal, o POJ apenas listava os erros gerados sem nenhum tipo de abstração. Além disso, a compilação seguia normalmente.
Para implementar melhorias, as seguintes modificações foram realizadas:
- Neste commit o código foi alterado para que a análise léxica, sintática e semântica retornem os erros encontrados;
- Neste commit foi criada uma classe customizada de erros para ser utilizada pelo runtime do ANTLR. Com isso podemos obter os erros encontrados pelo parser bem como realizar o tratamento adequado;
- Neste commit o código principal do POJ obtém os possíveis erros gerados, lista eles e aborta o processo de compilação quando necessário;
- Neste commit foram introduzidos programas em Pascal inválidos bem como a saída de erros esperada. Com isso os testes automatizados, além de validarem a saída esperada de programas válidos (Java Assembly), também verificam a saída de erros esperada a partir de programas inválidos (lista de erros).
Aqui está o PR completo.
Operadores relacionais para o tipo String
Até o momento tínhamos o suporte aos operadores relacionais apenas para o tipo inteiro (integer).
Neste commit foi implementado um programa em Java para entendermos como a JVM lida com os operadores relacionais para o tipo String. A partir do programa Java abaixo:
public class IfWithStrings { public static void main(String[] args) { String v1 = "aaa"; String v2 = "bbb"; if (v1.compareTo(v2) > 0) System.out.println("v1>v2"); else System.out.println("v1<=v2"); } }
Quando desassemblamos o arquivo class obtemos o assembly abaixo. Trechos irrelevantes foram omitidos, bem como o trecho original (em Java) que deu origem ao assembly foi inserido com ";;":
public class IfWithStrings { ;; public static void main(String[] args) public static main([java/lang/String)V { ;; String v1 = "aaa"; ldc "aaa" astore 1 ;; String v2 = "bbb"; ldc "bbb" astore 2 ;; v1.compareTo(v2) aload 1 aload 2 invokevirtual java/lang/String.compareTo(java/lang/String)I ;; if (v1.compareTo(v2) > 0) ifle label3 ;; System.out.println("v1>v2"); getstatic java/lang/System.out java/io/PrintStream ldc "v1>v2" invokevirtual java/io/PrintStream.println(java/lang/String)V goto label5 ;; System.out.println("v1<=v2"); label3: getstatic java/lang/System.out java/io/PrintStream ldc "v1<=v2" invokevirtual java/io/PrintStream.println(java/lang/String)V label5: return } }
Com este exemplo foi possível identificar que para comparar duas strings a JVM obtém da pilha as strings e executa o método "compareTo" da classe String. Este método compara as strings e empilha o seguinte resultado:
- -1, caso o 1o valor seja menor que o segundo;
- 0, caso os dois valores sejam iguais;
- +1, caso o 2o valor seja maior que o primeiro.
Dito isso, a partir do programa Pascal abaixo:
program IfWithStrings; begin if ( 'aaa' > 'bbb' ) then write('true') else write('false'); end.
O POJ foi ajustado para gerar o seguinte JASM:
// Code generated by POJ 0.1 public class if_with_strings { public static main([java/lang/String)V { ;; if ( 'aaa' > 'bbb' ) then ldc "aaa" ldc "bbb" invokevirtual java/lang/String.compareTo(java/lang/String)I iflt L3 iconst 1 goto L4 L3: iconst 0 L4: ifeq L1 ;; write('true') getstatic java/lang/System.out java/io/PrintStream ldc "true" invokevirtual java/io/PrintStream.print(java/lang/String)V goto L2 L1: ;; write('true') getstatic java/lang/System.out java/io/PrintStream ldc "false" invokevirtual java/io/PrintStream.print(java/lang/String)V L2: return } }
Este commit implementa a chamada ao método String.compareTo bem como a geração do teste (iflt) citados acima.
Aqui está o PR completo.
Chamada de procedures
Até o momento tínhamos que implementar todo o código no bloco principal (main) do programa em Pascal. Neste PR foi implementado o suporte à chamada de procedures. Reforçando que, em Pascal, uma procedure é o equivalente a uma function que não retorna um resultado.
Neste commit foi implementado um programa em Java para entender como a JVM lida com a chamada de procedures (funções sem retorno). A partir do programa Java abaixo:
public class ProcedureCall { public static void main(String[] args) { System.out.println("Hello from main!"); myMethod(); } static void myMethod() { System.out.println("Hello from myMethod!"); } }
Quando desassemblamos o class obtemos o seguinte assembly:
public class ProcedureCall { ;; public static void main(String[] args) public static main([java/lang/String)V { ;; System.out.println("Hello from main!"); getstatic java/lang/System.out java/io/PrintStream ldc "Hello from main!" invokevirtual java/io/PrintStream.println(java/lang/String)V ;; myMethod(); invokestatic ProcedureCall.myMethod()V return } ;; static void myMethod() static myMethod()V { ;; System.out.println("Hello from myMethod!"); getstatic java/lang/System.out java/io/PrintStream ldc "Hello from myMethod!" invokevirtual java/io/PrintStream.println(java/lang/String)V return } }
Com este exemplo foi possível identificar que para invocar uma procedure a JVM utiliza a instrução "invokestatic ProcedureCall.myMethod()V" onde:
- invokestatic é a instrução que recebe como argumento a assinatura completa do método a ser chamado;
- ProcedureCall é o nome da classe;
- myMethod()V é assinatura completa do método com seus parâmetros (neste exemplo nenhum) e o tipo de retorno (neste exemplo V - void - que indica nenhum).
Dito isso, a partir do programa Pascal abaixo:
program procedure_call_wo_params; procedure myprocedure; begin write('Hello from myprocedure!'); end; begin write('Hello from main!'); myprocedure(); end.
O POJ foi ajustado para gerar o seguinte JASM:
// Code generated by POJ 0.1 public class procedure_call_wo_params { ;; procedure myprocedure; static myprocedure()V { ;; write('Hello from myprocedure!'); getstatic java/lang/System.out java/io/PrintStream ldc "Hello from myprocedure!" invokevirtual java/io/PrintStream.print(java/lang/String)V return } ;; bloco principal (main) public static main([java/lang/String)V { ;; write('Hello from main!'); getstatic java/lang/System.out java/io/PrintStream ldc "Hello from main!" invokevirtual java/io/PrintStream.print(java/lang/String)V ;; myprocedure(); invokestatic procedure_call_wo_params.myprocedure()V return } }
Este commit implementa o suporte ao tipo "procedure" na tabela de símbolos.
Este commit implementa o suporte a geração do assembly correto. Para tal, o POJ precisa lidar com contextos (procedure sendo interpretada) para saber quando está interpretando o código de um procedimento ou do bloco principal.
Passagem de argumentos para o procedimento
Até então tínhamos a chamada de procedimentos funcional, mas sem argumentos. Neste commit foi implementado um programa em Java para identificar como a JVM lida com a passagem de argumentos. No exemplo é possível ver que, assim como com outros opcodes, no início de sua execução o procedimento retira seus argumentos da pilha. Com isso basta empilhar os argumentos antes de invocar o procedimento.
Dito isso, a partir do programa Pascal abaixo:
program procedure_call_add_numbers; procedure add(value1, value2: integer); begin write(value1 + value2); end; begin add(4, 6); end.
O POJ gera o seguinte JASM:
// Code generated by POJ 0.1 public class procedure_call_add_numbers { ;; procedure add(value1, value2: integer); static add(I, I)V { ;; write(value1 + value2); getstatic java/lang/System.out java/io/PrintStream iload 0 ;; carrega o parâmetro 0 (value1) iload 1 ;; carrega o parâmetro 1 (value2) iadd invokevirtual java/io/PrintStream.print(I)V return } ;; Bloco principal (main) public static main([java/lang/String)V { ;; add(4, 6); sipush 4 sipush 6 invokestatic procedure_call_add_numbers.add(I, I)V return } }
Para o correto suporte à chamada com argumentos foi necessário acrescentar na tabela de símbolos os tipos dos argumentos dos procedimentos. Por sua vez, para a correta invocação dos procedimentos, o parser teve que validar bem como gerar o assembly corretamente conforme a assinatura do procedimento.
Aqui está o PR completo.
Próximos passos
Na próxima publicação vamos falar sobre funções, entrada de dados e, se possível, concluir um dos objetivos deste projeto: cálculo do fatorial de forma recursiva.
Código completo do projeto
O repositório com o código completo do projeto e a sua documentação está aqui.
Top comments (0)