Captura de erros, operadores relacionais para string e procedures

RMAG news

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.