PCS2508
Linguagens e Compiladores
Entrega final complementada
Fábio Rachid Baptista No. USP 7209871
Índice
Introdução……….3
Especificações técnicas………..4
Autômato de reconhecimento léxico………....5
Funcionamento do programa………8
Linguagem………..10
Autômato de reconhecimento sintático………..12
Analisador semântico………....17
Simulação………32
Conclusão………39
Introdução
Esta entrega constitui no detalhamento e formalização da linguagem que será utilizada no compilador de acordo com as especificações do enunciado.
Especificações técnicas
O analisador léxico, bem como as outras partes do compilador, serão desenvolvidos na linguagem Python. O ambiente utilizado será o Linux Ubuntu.Autômato de reconhecimento léxico
Abaixo, a representação diagramática do autômato para reconhecimento léxico. Os círculos representam os estados; círculos com contorno mais forte representam estados finais. Os retângulos próximos aos círculos representam as classes associadas aos tokens recebidos pelo autômato. Para classificar os tokens em uma classe, foram seguidas as instruções presentes no enunciado. Abaixo, exemplos do funcionamento do autômato.
Exemplo Cadeia de entrada: a Entrase com uma letra ‘a’, vamos para o estado inicial q0 para q1, que é de aceitação. Como resultado, teremos que esse token pertencerá à classe id. Como saída dessa entrada, teremos o par (id, 1), ou seja, a classe a que pertence o token e seu índice (segundo o enunciado, deve ser associado um índice ao token pertence à classe id).
Cadeia de entrada: a = b Nesse caso, teremos 3 tokens: ‘a’, ‘=’ e ‘b’. O primeiro token a ser processado é ‘a’. Depois de processado, o par (‘id’, 1), correspondente a ‘a’, é guardado numa lista. O token seguinte é ‘=’, pertencente à classe ‘igualdade’. O par (igualdade, =) também é armazenado. Finalmente, ‘b’ é classificado como ‘id’ e o par (id, 2) é armazenado.
Funcionamento do programa
Estruturas de dados utilizadas
> Tuplas Na linguagem Python, uma tupla constitui um conjunto de valores armazenados e imutáveis. Para o analisador léxico, foram utilizadas de dimensão 2, utilizadas da seguinte maneira: (classe, valor) Desse modo, depois do processamento do token no autômato, há o armazenamento, na tupla, da classe do token e de seu valor. Cada tupla é inserida numa lista (explicado adiante). Por exemplo, o token ‘=’, depois de seu processamento, é armazenado numa tupla (igualdade, =). > Lista No programa, foi utilizada uma lista para armazenar os pares (classe, valor), para que esses pares fossem mostrados no final do programa, como mostrado a seguir: Lista = {(classe x, valor a), (classe x, valor b), (classe y, valor c), … , (classe z, valor t)} Essa lista é utilizada para, no final do programa, as tuplas com as classes e valores sejam impressas na tela e armazenadas num arquivo de saída. O programa, na linguagem Python, foi feito de forma a permitir modificações mais facilmente, modularizando o programa.
> Algoritmo da execução do programa
O programa recebe, como entrada, uma cadeia de caracteres que constituem os tokens. O arquivo de entrada é lido linha a linha, onde cada linha é armazenada em uma string que será percorrida, caracter a caracter. O programa entra em um loop que percorre cada caracter da linha armazenada naO caracter em análise é verificado em cada um dos casos especificados no autômato acima: se é letra, é identificado como ‘id’ e a ele é atribuído um valor; se é número, é identificado como ‘int’; se é o símbolo ‘+’, é identificado como ‘soma’ e assim em diante. O estado do autômato é atualizado de acordo com o símbolo consumido. Depois de um token ser processado (entendase depois que o autômato chega a um estado final), ele é armazenado numa tupla de acordo com sua classe no formato citado acima. Depois de consumido, o processo se inicia novamente, analisando o token seguinte, voltando o autômato ao seu estado inicial. Depois que todos os tokens da entrada forem consumidos, o programa deve imprimir na tela as tuplas resultantes e deve passar o resultado para um arquivo.
> Estruturamento do analisador léxico
Para tornar a implementação mais flexível e mais fácil de ser modificada, o analisador léxico, com seus métodos e atributos, foi implementado em uma classe separada da main. Portanto, para a execução do analisador léxico, devemos usar três arquivos: analisadorLexico.py main.py entrada.py No arquivo analisadorLexico.py, temos a função de transição do autômato, uma lista que armazena as tuplas (classe, valor) e uma lista com os símbolos dos identificadores. O arquivo main.py executa a função de transição e mostra os tokens obtidos associados às suas classes. No arquivo entrada.py, temse o arquivo de texto a ser lido e processado no analisador léxico.
Linguagem
Como especificado no enunciado, devese definir uma linguagem que será utilizada pelo compilador, que será denominada convenientemente de KCF. Segue, abaixo, a linguagem definida, na notação de Wirth: program = block "."
block =["const" ident "=" number {"," ident "=" number}";"]
["var" ident {"," ident}";"]
{"procedure" ident ";" block ";"} statement
statement =[ ident ":=" expression |"call" ident |"?" ident |"!" expression
|"begin" statement {";" statement }"end"
|"if" condition "then" statement |"while" condition "do" statement ]
condition ="odd" expression |
expression ("="|"#"|"<"|"<="|">"|">=") expression
expression =["+"|"-"] term {("+"|"-") term}.
term = factor {("*"|"/") factor}
factor = ident | number |"(" expression ")"
Exemplo
Um exemplo do código que utiliza a linguagem KCF:
VAR x, squ; PROCEDURE square; BEGIN squ:= x * x END; BEGIN x :=1; WHILE x <=10 DO BEGIN CALL square; ! squ; x := x +1 END END.
Automato de reconhecimento sintático
O analisador sintático tem a função de verificar se as regras, definidas na seção anterior, são seguidas. Dessa forma, por exemplo, no caso da linguagem KCF, ao se escrever END esperase que, em seguida, venha um pontoevírgula.
Para realizar tal tarefa, o analisador sintático é composto por um autômato de pilha estruturado e por um analisador léxico.
O autômato principal seria a raiz da linguagem. No caso, seria a máquina definida pelo não terminal Program. Os demais nãoterminais formam a gama de submáquinas que podem ser chamadas para tratar ocorrências em específico. Para obter tais autômatos a partir das expressões obtidas da notação de wirth, é possível utilizar o método visto em sala ou até mesmo seguindo a lógica e montar de forma livre já o minimizando. A interação entre o analisador sintático e o léxico se dá através dos tokens, onde o analisador sintático irá solicitar um token ao analisador léxico e, então, obtido o token, irá transitar em sua máquina. Além disso, o analisador sintático deve ter acesso à tabela de símbolos contida no analisado léxico.
Para estruturar tal organização foi novamente utilizado o paradigma orientado a objetos de tal modo que a troca de mensagens entre os objetos realizará as funções desejadas.
O autômato de reconhecimento sintático está representado nos diagramas abaixo.
Analisador semântico
Como descrito no tópico anterior, o analisador sintático não é capaz de determinar se uma dada extensão faz sentido lógico, ele somente determina se a sentença segue a regra da linguagem. Assim, é fundamental o uso de outro analisador que verifique, por exemplo, se uma atribuição está correta (não pode permitir que uma variável int guarde um char, por exemplo). Além disso, ele será a ferramenta que irá ser utilizar para gerar o código compilado (em assembly). O analisador semântico será chamado através do analisador sintático e possuirá diversas funções que estão associadas a transições do autômato do analisador sintático. O código gerado, detalhado a seguir, foi dividida em quatro partes: uma parte básica, que ocorre sempre que o compilador é chamado (não é influenciada pela sintaxe do programa), a parte referente ao autômato de reconhecimento sintático program, a parte referente ao autômato de reconhecimento sintático statement e a parte referente ao autômato de reconhecimento sintático expression.
> Básico
Antes de qualquer processamento das subrotinas semânticas associadas ao analisador léxico, é impresso um código padrão que ajusta o ponteiro para a pilha, chama a subrotina main e termina a execução quando main retorna. Também são geradas as subrotinas de Push e Pop juntamente com as constantes por elas utilizadas. Ao fim do processamento das subrotinas semânticas, também é gerado código: uma label indicando que o espaço de memória a seguir é a pilha de execução e o “commando” “#”, que indica ao montador que o código de montagem chegou ao fim. Código padrão de inícioinit LV stack_area ; adjusts the stack pointer
MM stack_pointer ; adjusts the stack pointer
SC sub_main ; calls main
HM init ; halts the machine when main returns
Código padrão de final
stack_area K /0000 ; stack fromthis address and on
# init ;
Subrotina Push
push_tmp K /0000 ; temporary
Push JP /0000 ;return address
MM push_tmp ; stores the value in a temporary
LD MM_code ; loads the "move to memory" code
+ stack_pointer ; composes it with the top of
stack address
MM push_write ; writes the instruction to
push_write
LD push_tmp ; loads the value to be pushed
push_write K /0000 ; writes the value on the stack
LD stack_pointer ; loads the stack pointer
+ two ; increments it
MM stack_pointer ; saves
LD push_tmp ; returns the pushed value
RS Push ; returns
Subrotina Pop
Pop JP /0000 ;return address
LD stack_pointer ; loads the stack pointer
- two ; decrements it
MM stack_pointer ; saves
LD LD_code ; loads the "load code"
+ stack_pointer ; composes it with the top of
stack address
MM pop_read ; writes the instruction to pop_read
pop_read K /0000 ; reads the value from the stack
> Program
O funcionamento de program consiste basicamente em gerar labels de início da subrotinas (onde é guardado o endereço de retorno desta), gerar um endereço de memória que age como variável temporária para a resolução de expressões e, para cada parâmetro de uma subrotina, gerar endereços de memória e desempilhálos para estes endereços (os parâmetros são passados pela pilha), de forma a facilitar consideravelmente a realização de operações com esses parâmetros. É o menor autômato e o que possui rotinas semânticas mais simples. Exemplo: PROCEDURE soma; BEGIN (...) END; Gera:sub_soma JP /0000 ;return address
SC Pop ; gets the parameter from the stack
SC Pop ; gets the parameter from the stack
(...)
RS sub_soma ; returns
> Statement
As rotinas semânticas de command variam muito entre cada “ramo” do autômato e, portanto, serão descritas separadamente a seguir: VAR id; : é gerado um endereço de memória da forma var_ + <nome da subrotina atual> + <id> Exemplo: VAR x; Gera: (...) var_x K /0000 ;var x := expression; : são rodadas as rotinas de expression, que, ao final, deixam o resultado a expressão no acumulador (detalhado mais adiante). Em seguida é gerado um comando move para a variável id (var_ + <nome da subrotina atual> + <id>). Exemplo: VAR a; BEGIN a := 2 END; Gera: (...) LV /0002MM var_main_a ; assings the expression to the
PROCEDURE id; : cada expressão é avaliada por expression (resultado no acumulador) e empilhada (Push). A chamada de função (sub + <id>) é, então, gerada Exemplo: VAR a, b, c; (…) PROCEDURE func; Gera: (…) ; a
SC Push ; pushes the routine parameter to
the stack
(…) ; b
SC Push ; pushes the routine parameter to
the stack
(…) ; c
SC Push ; pushes the routine parameter to
the stack
SC sub_func ; calls the routine
IF condition THEN statement: a expressão é avaliada (resultado no acumulador) e é gerado um jump if zero pra sair do if, se ela for avaliada para falso. Antes do label de saída do if, é gerado um load de 1 (que é usado para avaliar o else). Exemplo: (...) IF a =1 THEN BEGIN CALL print; END;
Gera:
(…) ; 1
JZ out_if_1 ;if the "if" condition fails, jumps
out of the "if"
(…) ;print(1)
LV one ; loads "true"
out_if_1 + zero ;thisisout of the "if"(NOP)
IF ODD condition THEN statement: se o if não foi executado, ou seja, a expressão do if foi avaliada para 0, este zero continua no acumulador; se o if foi executado, o “LV one” colocou 1 no acumulador. Assim, o else é executado apenas se houver um 0 no acumulador. Exemplo: IF ODD a :=2 THEN BEGIN CALL print; END; Gera:
JZ else_2 ; if we didn't pass in the "if",
pass in the "else"
JP out_else_2 ; but if we did, don't go throught
the "else"
else_2 + zero ;thisiswhere the "else" begins
(NOP)
(…) ;print(2);
out_else_2 + zero ;thisisout of the "else"(NOP)
WHILE expression DO statement: um token de inicio é gerado, a expressão é
for avaliada para falso. Antes do label de saída do while, é gerado um jump para o token de início (para testar a expressão novamente). Exemplo: WHILE 1 DO BEGIN CALL print; END; Gera:
while_1 + zero ;while starts here (NOP)
(...) ; 1
JZ out_while_1 ;if the expression evaluates to 0,
go out
(…) ;print(1);
JP while_1 ; goes to the start
out_while_1 + zero ;while ends here (NOP)
> Expression
As subrotinas semânticas de expression são as que geram o código responsável pela resolução de expressões lógicoaritméticas (não foi feita distinção clara entre as duas: valores diferentes de zero foram tratados como verdadeiros e o valor zero foi tratado como falso). Sua operação consiste em empilhar todos os operandos e fazer com que os operadores calculem o resultado sobre os termos do topo da pilha. Ou seja, no caso de uma adição, por exemplo, são desempilhados os dois operandos do topo da pilha e o resultado é empilhado. Ao fim de uma expressão, seu resultado dever ser desempilhado para o acumulador. Esse modo de operação exige que a ordem de realização das operações seja similar à da resolução de expressões pósfixas. Como a cadeia de entrada contém expressões infixas, muito do código se assemelha ao algoritmo de conversão entre as duas formas de apresentar uma expressão.> Observação importante Ao final do reconhecimento de expression, nada garante sobre um id não processado ou operadores na pilha. Por isso, toda chamada de uma submáquina expression deve verificar se esse id existe (e, se existir, ele deve ser tratado como uma variável de expr, e portanto colocado na pilha) e, no caso da pilha não estar vazia, remover um a um os operadores da pilha, gerando o código referente a eles. > Código referente aos diferentes operadores O código a ser gerado referente aos diferentes operadores deve pegar os parâmetros da pilha e colocar o resultado na pilha. Será detalhado apenas o código do +, já que todos são análogos (podem ser obtidos apenas trocando a última linha por uma linha que realize operação desejada ou, no caso de operações mais complexas, por um conjunto de linhas que realize essa operação).
SC Pop ; pops one parameter
MM <tmp> ;and moves it to a temporary
SC Pop ; pops the other parameter
+ <tmp> ;and sums them both
SC Push ;finally, put the result back to the stack
> Programa exemplo
O programa a seguir calcula os 10 primeiros números da sequência de Fibonacci: VAR a, b, i, tmp; BEGIN a := 1 b := 1 i := 0 WHILE i <10 DO BEGIN
a := a + b b := tmp i := i + 1 END; END. Código gerado
init LV stack_area ; adjusts the stack pointer
MM stack_pointer; adjusts the stack pointer
SC sub_main ; calls main
HM init ; halts the machine when main
returns
stack_pointer K /0000 ; points to the stack
LD_code K /8000 ; load code
MM_code K /9000 ; move to memory code
one_hundred K =100 ;100 ten K =10 ;10 two K =2 ; 2 one K =1 ; 1 zero K =0 ; 0 ascii_zero K /0030 ;'0'
ascii_lf K /000A ; line feed
not K /0000 ; inverts the accumulator logically
JZ not_zero ;if it's false, jumps to not_zero
LD zero ; it was true, loads 0
RS not ; returns
not_zero LD one ; it was false, loads 1
logic K /0000 ; adequates true to 1
JZ logic_zero ;if it's false, jumps to not_zero
LD one ; it was true, loads 1
RS logic ; returns
logic_zero LD zero ; it was false, loads 0
RS logic ; returns
push_tmp K /0000 ; temporary
Push JP /0000 ;return address
MM push_tmp ; stores the value in a temporary
LD MM_code ; loads the move to memory code
+ stack_pointer; composes it with the top of stack
address
MM push_write ; writes the instruction to
push_write
LD push_tmp ; loads the value to be pushed
push_write K /0000 ; writes the value on the stack
LD stack_pointer; loads the stack pointer
+ two ; increments it
MM stack_pointer; saves
LD push_tmp ; returns the pushed value
RS Push ; returns
Pop JP /0000 ;return address
LD stack_pointer; loads the stack pointer
- two ; decrements it
MM stack_pointer; saves
+ stack_pointer; composes it with the top of stack address
MM pop_read ; writes the instruction to pop_read
pop_read K /0000 ; reads the value from the stack
RS Pop ; returns
Print_var K /0000 ; stores the print parameter
Print_hundreds K /0000 ; hundreds of the parameter Print_tens K /0000 ; tens of the parameter
Print_units K /0000 ; units of the parameter
sub_print JP /0000 ;return address
SC Pop ; gets the parameter from the stack
MM Print_var ; makes a copy
/ one_hundred ;/100
* one_hundred ;*100
MM Print_hundreds ;= hundreds
LD Print_var ; loads the original value
- Print_hundreds ;- hundreds (= tens + units)
/ ten ;/10
* ten ;*10
MM Print_tens ;= tens
LD Print_var ; loads the original value
- Print_hundreds ;- hundreds - Print_tens ;- tens MM Print_units ;= units LD Print_hundreds ; hundreds / one_hundred ;/100 + ascii_zero ;+'0'
PD /0100 ; writes on the screen
/ ten ;/10
+ ascii_zero ;+'0'
PD /0100 ; writes on the screen
LD Print_units ; units
+ ascii_zero ;+'0'
PD /0100 ; writes on the screen
LD ascii_lf ; line feed
PD /0100 ; writes on the screen
LD Print_var ; returns the parameter value
RS sub_print ; returns
sub_main JP /0000 ; reservado p/end de retorno
LV =1 ; loads the value
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_a ; assings the expression to the
variable
LV =1 ; loads the value
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_b ; assings the expression to the
variable
LV =0 ; loads the value
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
for_1 + zero ;for starts here (NOP)
LD var_main_i ; loads the var
SC Push ;and pushes it to the stack
LV =10 ; loads the value
SC Push ;and pushes it to the stack
SC Pop ; pops one parameter
MM tmp_main_expr;and moves it to a temporary
SC Pop ; pops the other parameter
- tmp_main_expr;and subtracts them both
JN isless_2 ; jumps to isless if it's really
less
LD zero ; loads falseif it's not less
JP operator_end_2 ; goes to end
isless_2 LD one ; loads trueif it's really less
operator_end_2 SC Push ;finally, put the result
back to the stack
SC Pop ;final result of the expression ->
accumulator
JZ out_for_1 ;if expression evaluates to 0, go
out of the for
JP for_command_1; does the for commands
for_post_1 + zero ;for post (3rd term) starts here
(NOP)
LD var_main_i ; loads the var
SC Push ;and pushes it to the stack
LV =1 ; loads the value
SC Push ;and pushes it to the stack
SC Pop ; pops one parameter
SC Pop ; pops the other parameter
+ tmp_main_expr;and sums them both
operator_end_3 SC Push ;finally, put the result
back to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_i ; assings the expression to the
variable
JP for_1 ; does the for again
for_command_1 + zero ;for command starts here
(NOP)
LD var_main_b ; loads the var
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
SC Push ; pushes the routine parameter to
the stack
SC sub_print ; calls the routine
LD var_main_a ; loads the var
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_tmp; assings the expression to the
variable
LD var_main_a ; loads the var
SC Push ;and pushes it to the stack
LD var_main_b ; loads the var
SC Push ;and pushes it to the stack
SC Pop ; pops one parameter
+ tmp_main_expr;and sums them both
operator_end_4 SC Push ;finally, put the result
back to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_a ; assings the expression to the
variable
LD var_main_tmp; loads the var
SC Push ;and pushes it to the stack
SC Pop ;final result of the expression ->
accumulator
MM var_main_b ; assings the expression to the
variable
JP for_post_1 ; does the for post
out_for_1 + zero ;for ends here (NOP)
RS sub_main ; termino da subrotina
tmp_main_expr K /0000 ;var
var_main_a K /0000 ;var
var_main_b K /0000 ;var
var_main_i K /0000 ;var
var_main_tmpK /0000 ;var
stack_area K /0000 ; stack fromthis address and on
# init ;
Simulações Abaixo, seguem as simulações do compilador, envolvendo todas as partes (léxica, sintática e semântica), através do código Cadeia de entrada: PROCEDURE soma; BEGIN END;
Cadeia de entrada: VAR a; BEGIN a := 2 END;
Cadeia de entrada: IF a =1 THEN BEGIN CALL print; END; (abaixo)
Cadeia de entrada: WHILE 1 DO BEGIN CALL print; END;
Cadeia de entrada: VAR a, b, i, tmp; BEGIN a := 1 b := 1 i := 0 WHILE i <10 DO BEGIN tmp := a a := a + b b := tmp i := i + 1 END; END.
(abaixo; utilizar zoom para ampliar)
Conclusão
A construção de um compilador foi interessante, pois se pôde integrar conhecimentos de várias áreas da computação, além de conseguir entender o funcionamento, a fundo, dos compiladores.