You are on page 1of 5

Compiladores

Ana´lise Semaˆntica
versa˜o α − 0.001
Sim˜ao Melo de Sousa
Este documento ´e uma tradu¸c˜ao adaptada do cap´ıtulo ”Analyse S´emantique” da sebenta ”Cours de Compilation” de Christine Paulin-Morhing e Marc Pouzet
(http://www.lri.fr/~paulin).

1

Introdu¸c˜
ao

A an´alise semˆantica trata a entrada sint´actica e transforma-a numa representa¸ca˜o mais
simples e mais adaptada a` gera¸ca˜o de c´odigo. Esta camada do compilador fica igualmente
encarregue de analisar a utiliza¸c˜ao dos identificadores e de ligar cada uma delas a sua
declara¸c˜ao. Nesta situa¸ca˜o verificar-se-´a que o programa respeita as regras de visibilidade
´ tamb´em esperado que esta fase da compila¸ca˜o verifie de porte dos identificadores. E
que que cada express˜ao definida tenha um tipo adequado conforme as regras pr´oprias a`
linguagem.
Neste parte da li¸ca˜o iremos estudar a gest˜ao da tabela dos s´ımbolos que serve para a
liga¸c˜ao dos nomes manipulados aos objectos que estes de facto designam. Estudaremos
igualmente a tipifica¸c˜ao dos programas. Por fim definiremos as no¸co˜es de gram´aticas de
atributos que permitam associar valores aos nodos da a´rvore de deriva¸c˜ao sint´actica.

2
2.1

Tabela dos s´ımbolos
Introdu¸

ao

As linguagens de programa¸ca˜o manipulam identificadores que s˜ao essencialmente s´ımbolos que servem para designar objectos – conte´
udo dum endere¸co mem´oria, no caso por
exemplo duma vari´avel, peda¸cos de c´odigo no caso de nomes de procedimentos, tipos,
etc...
A tabela de s´ımbolos arquiva as informa¸c˜oes sobre os objectos designados por nomes
na linguagem em quest˜ao. Esta ´e actualizada de cada vez que ´e analisada uma declara¸c˜ao
dum novo identificador. De forma semelhante, a tabela ´e consultada de cada vez que ´e
utilizado um identificador no programa analisado.
A tabela de s´ımbolos permite para cada identificador o arquivo das informa¸c˜oes associadas ao objecto identificado. Estas podem ser de natureza diversa, como o tipo do

1

As regras de porte (scope em inglˆes) dos identificadores que permitir´a resolver os conflitos subjacentes.2 Porte dos identificadores Os programas manipulam v´arios identificadores. qualquer identificador deve ter sido previamente declarado numa express˜ao let id = . Se um identificador ´e declarado mais do que uma vez o objecto acedido pelo nome ´ no entanto poss´ıvel aceder a um objecto via o seu qualificado ´e o ultimo declarado. ou ao resto do programa/m´odulo em que est´a definido no segundo caso (ficheiro = m´odulo em OCaml). import em Java) a qualifica¸ca˜o pode ser omitida. as suas utiliza¸co˜es tem lugar no bloco ao qual pertencem). E nome completamente qualificado..objecto.. as linguagens permitam indicar sint´acticamente que a utiliza¸ca˜o de certas vari´aveis ser´a confinada a uma parte bem determinada do programa. n˜ao ser´a necess´ario alocar espa¸co para as vari´aveis atribu´ıdas ao bloco em quest˜ao. Se existirem directivas de abertura de m´odulos (por exemplo open em OCaml. O corpo dum procedimento pode utilizar vari´aveis declaradas em qualquer procedimento que o engloba (ele pr´oprio inclu´ıdo). 2. um valor. procedimento) pode igualmente estar declarado mais do que uma vez na mesma tabela. Durante a an´alise semˆantica. Um mesmo identificador pode ser utilizado para representar diferentes objectos.. fora deste bloco. designado de bloco. os ”packages” do java). os m´odulos e os valores podem ser agrupados em tabelas de s´ımbolos diferentes. por exemplo quando E existem v´arios espa¸cos de nomes. Assim. Um mesmo identificador (vari´avel.. Classicamente. Em linguagens de tipo ML. Por exemplo em C. Fora do m´odulo um identificador exportado (i.. Em ML. Aquando da compila¸ca˜o ser´a necess´ario conhecer precisamente o objecto referenciado por cada identificador.e. ou let id = . cuja visibilidade fora do m´odulo ´e permitida) pode ser acedido atrav´es do que se designa por nome qualificado ou seja Nome_do_m´ odulo. o compilador assegurar-se-´a que todas as vari´aveis utilizadas foram declaradas de forma adequada e s˜ao bem vis´ıveis durante as suas utiliza¸co˜es (i. O porte de tal declara¸c˜ao est´a restrito a express˜ao associada a express˜ao a direita do in no caso da declara¸ca˜o local.. uma vari´avel ´e local ao procedimento em que foi declarada. No caso de um nome completamente qualificado poder 2 . As regras de porte/alcance dos identificadores s˜ao espec´ıficas a cada linguagem. Por raz˜oes de efic´acia.. os tipos.e. Este conhecimento ´e em geral adquirido.identificador. uma posi¸ca˜o na lista das vari´aveis declaradas (com a finalidade de calcular o endere¸co relativo aquando da gera¸ca˜o de c´odigo). qualquer identificador tem de ser declarado de forma centralizada antes da sua utiliza¸ca˜o (da´ı a necessidade da instru¸ca˜o forward para as fun¸co˜es mutuamente recursivas). Este situa¸ca˜o obriga conhecer a natureza do objecto para determinar em que tabela procurar os seus dados. ou ´e global a todo o programa. in .. ´ igualmente poss´ıvel coexistirem v´arias tabelas de s´ımbolos. Este objectos podem estar arquivados ou referenciados em diferentes tabelas de s´ımbolos.. Em Pascal. como ´e o caso para linguagens orientadas a objecto (e. Assim este aparente conflito pode ser facilmente resolvido. Por exemplo arquivaremos nos diferentes espa¸cos o nome das classes e o nome dos m´etodos associados. encontraremos nestas linguagens uma tabela de s´ımbolos para cada espa¸co de nome.g. e com a finalidade de melhorara a robustez do c´odigo.

ent˜ao este fica por designar o u ´ltimo declarado.referir dois objectos diferentes. A escolha do m´etodo por executar ´e geralmente feita de forma dinˆamica (na altura da execu¸c˜ao). constru´ımos uma tabela dos s´ımbolos vis´ıveis T . Em Java. em tempo de compila¸c˜ao) o m´etodo por aplicar. ´e necess´ario gerir uma tabela para a verifica¸ca˜o do porte de cada identificador. o seu tamanho e a frequˆencia da sua utiliza¸c˜ao. O tipo dos parˆametros permite determinar de forma est´atica (i. o corpo dum m´etodo pode utilizar outros m´etodos da mesma classe mesmo se a defini¸c˜ao destes m´etodos ocorre posteriormente.3 Representa¸ c˜ ao da tabela de s´ımbolos Deve ser poss´ıvel ap´os a an´alise dum programa encontrar toda a informa¸ca˜o associada a um identificador. Se um mesmo identificador est´a definido numa classe e redefinido numa das suas sub-classes ent˜ao devese impor coerˆencia entre os tipos dos objectos assim definidos. analisa-se o corpo e da defini¸ca˜o (aqui let y = 2 in y*y + 2*y +1). Consideremos a analise do seguinte programa: 1 let x = ( let y = 2 in y * y + 2* y +1) in x + y Quando come¸camos a analisar esta express˜ao. Para encontrar informa¸ca˜o sobre este identificador. Isto obriga ao processamento em bloco das defini¸c˜oes de classes. 2. Esta associa¸c˜ao pode (costuma) igualmente incluir outras informa¸c˜oes u ´teis como. De uma forma similar.e. v´arias classes podem redefinir os mesmos m´etodos. Ao lado desta estrutura persistente. Isto pode ser feito enfeitando (analogia feita ao enfeito da a´rvore de natal que fica ap´os este trabalho com todo o significado – a semˆantica – de natal) a ´ poss´ıvel associar a cada utiliza¸ca˜o dum identificador a´rvore de sintaxe abstracta (ASA). Mais. Sem cuidado. determinar rapidamente e facilmente se um dado identificador ´e vis´ıvel. E um apontador para parte da ASA que corresponde a declara¸c˜ao do identificador. Para tal ´e preciso juntar a palavra chave rec ao let. deve ser igualmente poss´ıvel juntar novos identificadores. De forma semelhante deve ser poss´ıvel retirar identificadores desta estrutura quando o objecto referenciado deixa de existir ou de ser acess´ıvel (um identificador de uma vari´avel local quando se atinge o fim do bloco em que est´a definido). No primeiro caso ´e assim indicado explicitamente que se pretende invocar o m´etodo da classe m˜ae mesmo se essa foi redefinida na classe activa. a gest˜ao duma tabela de s´ımbolo pode se tornar pesada. Em algumas situa¸co˜es ´e poss´ıvel indicar sint´acticamente que m´etodo se pretende chamar. Uma outra solu¸ca˜o consiste em arquivar as informa¸co˜es sobre um objecto numa tabela e associar um endere¸co para esta tabela a cada utiliza¸ca˜o do identificador. um inteiro ou um nome u ´nico. por exemplo. Nesta situa¸ca˜o devemos ent˜ao analisar o corpo da 3 . Primeiro analisa-se a constru¸ca˜o let x = e in e’ que declara o novo identificador x. Esta tabela deve poder informar em qualquer instante desta fase de an´alise dos dados dos identificadores vis´ıveis. o tipo do objecto referenciado. Se as classes s˜ao disjuntas. podemos definir numa classe v´arios m´etodos com o mesmo nome. type checking). esta ambiguidade de nome pode ser resolvida por uma verifica¸ca˜o dos tipos (i. como no caso dos m´etodos por exemplos. De facto o cuidado por ter aqui ´e o compromisso entre utilidade da informa¸ca˜o arquivada. como no caso do uso das palavras chaves super ou this.e. Este endere¸co pode ser um apontador. Uma declara¸c˜ao de fun¸ca˜o em ML n˜ao ´e por defeito recursiva.

ou seja se a constru¸ca˜o de T 0 (jun¸ca˜o de (x. A copia e arquivo da c´opia da tabela de hash com o objectivo de a repor caso necess´ario seria aqui altamente ineficiente. No fim desta an´alise. Estas tabelas devem ser optimizadas porque o n´ umero de identificadores pode ser importante e o acesso a informa¸ca˜o deve ser r´apido. inteiro. Para retirar um identificador basta eliminar a primeira ocorrˆencia encontrada na tabela (que corresponde ao u ´ltimo inserido). por exemplo.f’) As tabelas de hash com liga¸c˜oes para listas de entradas (onde os identificadores pertencendo a mesma lista correspondem aos valores com a mesma chave de dispers˜ao (hash key)) s˜ao em regra geral boas estruturas para implementar tabelas de s´ımbolos vis´ıveis. ´e assim necess´ario retirar explicitamente os identificadores dos quais pretendemos apagar o registo. f’ = visivel(T’. add x. por exemplo se T for uma tabela de Hash (como ´e o caso em OCaml).e. N˜ao ´e o caso.defini¸c˜ao de y ou seja 2. Ao sair do bloco y*y + 2*y +1. let(x:tip. De facto quando identificadores com o mesmo nome est˜ao declarados o u ´ltimo esconde naturalmente os anteriores. tip)) n˜ao altere de facto a tabela T .f. Utilizando a tabela T 0 analisamos a express˜ao y*y + 2*y +1 que ´e determinada como sendo do tipo inteiro. tip = tipo_de(T. let(x:tip. Com se vˆe na express˜ao por analisar e pelas regras de porte em OCaml a ocorrˆencia de y n˜ao faz referˆencia a` defini¸ca˜o interna a` defini¸ca˜o de x mas sim a uma declara¸c˜ao pr´evia. tip = tipo_de(f). Esta declara¸c˜ao torna qualquer informa¸c˜ao pr´evia sobre y em T invis´ıvel. Assim sendo esta tabela por ser global: function visible_imp : asa -> asa_tipada visival_imp (let(x.f).T). as diferentes utiliza¸co˜es dos identificadores na ASA devem estar associadas a` boa declara¸c˜ao.e).e ’)) = seja f = visivel_imp(e).e’)) = seja f = visivel(T. T’ = add ((x. seja f’ = visivel_imp(e’). Podem ser implementadas de forma imperativa ou funcional No caso duma representa¸c˜ao funcional a analise de visilibilidade poderia se escrever da seguinte forma: function visivel : tabela * asa -> asa_tipada visivel (T.f.e’).tip). Este processo leva assim a actualiza¸ca˜o da tabela T em T 0 na qual se acrescentou a entrada y com a informa¸c˜ao de que este ´e. Isto porque a lista de entradas ´e gerida como uma pilha.f’) Este processo funciona se a representa¸ca˜o da tabela ´e funcional. a entrada y na tabela de s´ımbolos deve desaparecer e assim voltamos de T 0 para T .e.let(x. Se optarmos por uma tabela de hash. del (x). 4 . Neste ponto preciso sabe-se que x ´e inteiro e podemos ent˜ao juntar esta informa¸c˜ao a tabela de s´ımbolos T e proceder a analise de x+y.

isto ´e. Em termos de requisitos. desigualdades se se utiliza a´rvores bin´arias de pesquisa). (a) esta associa¸c˜ao deve ser u ´nica. 5 . E assim necess´ario conservar uma pilha dos blocos abertos com a informa¸ca˜o. um inteiro representa um u ´nico identificador.´ poss´ıvel introduzir em cada bloco do programa analisado um n´ E umero arbitr´ario de identificadores. 2. (b) esta tabela deve poder devolver o identificador representado por um inteiro e viceversa.identificador. dos identificadores que este introduz. Neste caso ´e interessante dispor duma tabela de dispers˜ao (hash table) onde se arquiva a associa¸c˜ao entre inteiro . Na sa´ıda de cada bloco devemos poder identificar e remover todos os ´ identificadores da tabela que nele foram introduzidos (estes deixam de ser vis´ıveis). Este processo pode ser feito de forma funcional arquivando uma pilha de listas de identificadores ou de forma mais imperativa interligando o conjunto de identificadores introduzidos num mesmo bloco na tabela dos s´ımbolos vis´ıveis e guardando num estrutura (uma pilha por exemplo) o endere¸co na tabela do u ´ltimo identificador introduzido no bloco activo. ´e necess´ario fazer numerosas compara¸c˜oes (igualdades.4 Representa¸ c˜ ao dos s´ımbolos Quando se analisa os s´ımbolos do programa. Para tal ´e pertinente utilizar uma representa¸ca˜o eficiente dos identificadores com por exemplo uma codifica¸ca˜o com base em inteiros. (c) a gest˜ao desta tabela n˜ao deve sobrecarregar computacionalmente o processo de analise. para cada bloco aberto.