Alex Sandro Garzão
Posted on May 26, 2024
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.
Posted on May 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.