Operadores de adição e subtração
Alex Sandro Garzão
Posted on March 23, 2024
Faz mais de um mês desde a última publicação sobre o projeto. Culpa da correria do dia-a-dia :-D
Após a primeira versão, que conseguia compilar e executar o "Hello world!" em Pascal na JVM, fiz alguns avanços neste último mês. Os mais relevantes foram:
- Concatenação de strings
- Soma e subtração de inteiros
Conforme citado em uma publicação anterior, o POJ (Pascal on the JVM) lê um programa Pascal e gera o JASM (Java Assembly) para posteriormente ser transformado em um arquivo class e executado na JVM.
Para exemplificar o que o POJ faz, abaixo temos um exemplo em Pascal que concatena três strings:
program ConcatStrings;
begin
writeln ('Hello ' + 'world ' + 'again!');
end.
Abaixo temos o JASM gerado pelo POJ:
// Code generated by POJ 0.1
public class concat_three_strings {
public static main([java/lang/String)V {
getstatic java/lang/System.out java/io/PrintStream
ldc "Hello "
ldc "world "
invokedynamic makeConcatWithConstants(java/lang/String, java/lang/String)java/lang/String {
invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants(java/lang/invoke/MethodHandles$Lookup, java/lang/String, java/lang/invoke/MethodType, java/lang/String, [java/lang/Object)java/lang/invoke/CallSite
[""]
}
ldc "again!"
invokedynamic makeConcatWithConstants(java/lang/String, java/lang/String)java/lang/String {
invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants(java/lang/invoke/MethodHandles$Lookup, java/lang/String, java/lang/invoke/MethodType, java/lang/String, [java/lang/Object)java/lang/invoke/CallSite
[""]
}
invokevirtual java/io/PrintStream.println(java/lang/String)V
return
}
}
A partir deste assembly utilizamos o JASM (assemblador de Java Assembly) para criar o arquivo class final.
Testes End2End
Para auxiliar no desenvolvimento, além dos testes unitários agora temos uma pasta com exemplos em pascal e a saída esperada em JASM. Com isso temos testes End2End no POJ que validam a entrada (exemplos em Pascal) com a saída esperada (JASM).
Validação de tipos
Outra funcionalidade implementada foi a validação de tipos. Apesar da JVM validar a tipagem dos dados, é extremamente recomendado que o POJ valide o código Pascal para não gerar um arquivo JASM inválido.
Para exemplificar, o código abaixo está sintaticamente correto, mas semanticamente incorreto porque em Pascal não é possível somar strings com inteiros.
program Hello;
begin
writeln ('Hello ' + 1);
end.
Para realizar esta tipagem o parser agora mantém uma pilha com os tipos de dados que estão sendo empilhados na JVM. Com isso, nas operações de soma e subtração o tipo dos dados são validados durante a análise semântica.
Concatenação de strings, soma e subtração de inteiros
Algumas modificações foram realizadas no parser para reconhecer a expressão "expression additiveoperator expression" (trecho da gramática abaixo).
expression
: expression op = relationaloperator expression # RelOp
| expression op = ('*' | '/') expression # MulDivOp
| expression op = additiveoperator expression # AddOp
| signedFactor # ExpSignedFactor
;
Esta expressão é responsável pela derivação tanto da soma bem como da subtração de strings e inteiros.
O "# AddOp" na gramática acima é uma anotação do ANTLR que permite que cada regra da gramática possa ser facilmente identificada durante o parser. Com esta anotação o ANTLR gera um método (ExitAddOp abaixo) que será executado sempre que o parser terminar o reconhecimento da expressão.
func (t *TreeShapeListener) ExitAddOp(ctx *parsing.AddOpContext) {
// Check pascal types.
pt1 := t.pst.Pop()
pt2 := t.pst.Pop()
if pt1 != pt2 {
t.jasm.AddOpcode("invalid types")
return
}
// Get operator.
op := ctx.GetOp().GetText()
switch {
case op == "+":
switch pt1 {
case String:
t.GenAddStrings()
case Integer:
t.GenAddIntegers()
default:
t.jasm.AddOpcode("invalid type in add")
}
case op == "-":
switch pt1 {
case Integer:
t.GenSubIntegers()
default:
t.jasm.AddOpcode("invalid type in sub")
}
}
}
Este método inicia retirando os últimos 2 tipos da pilha e verificando se possuem o mesmo tipo. Após isso é verificado qual o operador (+ ou -) e, baseado no operador e no tipo do dado, executado o método que irá gerar o JASM. Neste ponto o código também indica uma operação inválida (como no caso da subtração de strings, que é uma operação inválida em Pascal).
Como exemplo temos abaixo o método GenAddStrings, responsável por gerar o JASM para concatenar duas string, invocado no trecho acima:
func (t *TreeShapeListener) GenAddStrings() {
t.jasm.StartInvokeDynamic(`makeConcatWithConstants(java/lang/String, java/lang/String)java/lang/String`)
t.jasm.AddOpcode(`invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants(java/lang/invoke/MethodHandles$Lookup, java/lang/String, java/lang/invoke/MethodType, java/lang/String, [java/lang/Object)java/lang/invoke/CallSite`)
t.jasm.AddOpcode(`[""]`)
t.jasm.FinishInvokeDynamic()
t.pst.Push(String)
}
O jasm (em t.jasm) é um objeto que auxilia na geração do JASM, podendo emitir assinaturas de métodos, classes (do Java Assembly) bem como os opcodes.
Vale observar a última linha do trecho acima: t.pst.Push(String)
. "pst" (pascal stack type) é a pilha que guarda os tipos dos dados. Neste caso, como foi reconhecida uma concatenação de strings, e foi emitido código para concatená-las, estamos também inserindo o tipo String na pst.
Até este ponto reconhecemos o operador, o tipo do dado, realizamos uma validação de tipos e geramos código para executar a operação. Mas e as strings e os inteiros? Como eles são carregados para a pilha da JVM?
Na gramática os strings e os inteiros de um programa Pascal são considerados "símbolos terminais" e derivam a partir da regra signedFactor existente na regra expression (vide trecho da gramática acima). E o parser, baseado nas regras instrumentadas em Go, sempre que reconhece estes símbolos terminais automaticamente gera o JASM para carregá-los na pilha da JVM.
No trecho abaixo é possível ver que ao terminar o reconhecimento de uma string o método ExitString gera o opcode ldc (load constant) seguido da string a ser carregada na pilha da JVM. O ctx.GetText()
é disponibilizado pelo runtime Go do ANTLR e permite obter o valor do símbolo terminal, que no nosso caso é a string.
func (t *TreeShapeListener) ExitString(ctx *parsing.StringContext) {
str := ctx.GetText()
t.jasm.AddOpcode("ldc", "\""+str+"\"")
t.pst.Push(String)
}
A "pegadinha" da precedência de operadores
No site do ANTLR tem a gramática pronta de Pascal. Porém, apesar de reconhecer corretamente os programas em Pascal, esta gramática não lida corretamente com a precedência de operadores. A forma recomendada no próprio site do ANTLR é similar ao exemplo abaixo:
grammar Expr;
prog: (expr NEWLINE)* ;
expr: expr ('*'|'/') expr
| expr ('+'|'-') expr
| INT
| '(' expr ')'
;
NEWLINE : [\r\n]+ ;
INT : [0-9]+ ;
Com isso a derivação do parser não respeitava a precedência de operadores e o POJ gerava um JASM errado.
Por exemplo, para o trecho em Pascal abaixo:
writeln (8-4-2);
O correto seria o POJ gerar o seguinte assembly:
getstatic java/lang/System.out java/io/PrintStream
sipush 8
sipush 4
isub
sipush 2
isub
invokevirtual java/io/PrintStream.println(I)V
No exemplo acima atentem para a localização dos opcodes isub.
Basicamente o assembly acima é executado na JVM da seguinte forma:
Instrução | Descrição instrução | Estado pilha após instrução |
---|---|---|
sipush 8 | Empilha o inteiro 8 | [ 8 ] |
sipush 4 | Empilha o inteiro 4 | [ 8, 4 ] |
isub | Retira os 2 últimos elementos da pilha (8 e 4), subtrai e empilha o resultado (4) | [ 4 ] |
sipush 2 | Empilha o inteiro 2 | [ 4, 2 ] |
isub | Retira os 2 últimos elementos da pilha (4 e 2), subtrai e empilha o resultado (2) | [ 2 ] |
No final da execução a pilha contém o resultado esperado de 8-4-2: 2.
Porém por não lidar corretamente com a precedência de operadores, a partir da gramática original era gerado o assembly abaixo. Reparem novamente na localização dos opcodes isub:
getstatic java/lang/System.out java/io/PrintStream
sipush 8
sipush 4
sipush 2
isub
isub
invokevirtual java/io/PrintStream.println(I)V
E o assembly acima é executado na JVM da seguinte forma:
Instrução | Descrição instrução | Estado pilha após instrução |
---|---|---|
sipush 8 | Empilha o inteiro 8 | [ 8 ] |
sipush 4 | Empilha o inteiro 4 | [ 8, 4 ] |
sipush 2 | Empilha o inteiro 2 | [ 8, 4, 2 ] |
isub | Retira os 2 últimos elementos da pilha (4 e 2), subtrai e empilha o resultado (2) | [ 8, 2 ] |
isub | Retira os 2 últimos elementos da pilha (8 e 2), subtrai e empilha o resultado (6) | [ 6 ] |
Com isso o resultado do assembly acima era 6 :-)
O repositório com o código completo do projeto e instruções sobre como criar o executável bem como executar os testes está aqui.
Posted on March 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.