Departamento de Estatística e Informática Universidade Federal de Sergipe Compiladores Ambientes Giovanny Lucero [email protected] 1 Ambientes • Também conhecidos como tabela de símbolos • Mapeamento de Símbolos a atributos (tipicamente tipos e endereço de memória) – Não confundir símbolos com tokens • Implementam os bindings (amarrações / vínculos) • Dois tipos de ocorrência dos identificadores – definição (declaração) – inserção na tabela – Uso -- lookup • Escopo de um identificador 2 Ambientes 1. function f(a: int, b:int , 2. c:int) = 3. ( print_int(a+c); 4. let var j:= a+b; 5. var a:= “hello”; 6. in print(a); 7. print_int(j); 8. end; 9. print_int(b); 10. ) Suponha ambiente inicial 0 Linha 2: 1 = 0 + {aint, bint, cint} Linha 4: 2 = 1 + {jint} Linha 5: 3 = 2 + {aString} sobrepondo o binding dado para a anteriormente (em 1) Linha 8: Descartamos 3 e voltamos para 1 Linha 10: Voltamos para 0 3 Como implementar “descartar-voltar” • Estilo imperativo – Modificamos 1 até virar 2 – Problema: destruímos 1, como “volto”? – Solução: pilha de ambientes • Estilo funcional – Implementamos a função + – Não há problema • um ambiente novo não é construído “destrutivamente” a partir de um anterior • Implementações eficientes para ambos estilos – Crítico: um programa pode ter milhares de símbolos 4 Ambientes múltiplos structure M = struct package M; structure E = struct class E { val a = 5 static int a = 5; end } structrure N = struct class N { val b = 10; static int b = 10; val a = E.a + b static int a = E.a + b; end } structure D = struct class D { val d = E.a + N.a static int d = E.a + N.a end } end Vários ambientes ativos ao mesmo tempo 5 Implementação imperativa eficiente Tabela de Hash com external chaining – inserimos novos bindings no começo das listas, – implementação eficiente de “deleção” a int b int y real a int b int y real b real Adicionamos o binding {breal} 6 Tabela de Hash com external chaining class Bucket {String key; Object binding; Bucket next; Bucket(String k, Object b, Bucket n) { key = k; binding = b; next = n; } } class HashT { final int SIZE = 256; Bucket table = new Bucket[SIZE] int hash(String s) { int h=0; for (int i = 0; i < s.length(), i++) h= h*65599 + s.charAt(i); return h } 7 void insert(String s, Bind b) { int index = hash(s) % SIZE; table[index] = new Bucket(s,b,table[index]); } void lookup(String s) { int index = hash(s) % SIZE; for (Binding b = table[index]; b!null; b = b.next) if (s.equals(b.key)) return b.binding; return null; } void pop(String s) { int index = hash(s) % SIZE; table[index]=table[index].next; } 8 Implementação funcional eficiente Tabela de Hash não serve (trabalha destrutivamente) Árvores balanceadas – Inserção “cria” uma árvore nova – Não há o problema descartar-voltar data Tree a = ConsT (String,a) Tree Tree | EmptyTree insert :: Tree a (String,a) Tree a insert EmptyTree (s,v) = ConsT (s,v) EmptyTree EmptyTree insert (ConsT (s,v) left right) (s1,v1) | s1 < s = ConsT (s,v) (insert s1 left) right | s1 > s = ConsT (s,v) left (insert s1 right) | s1 = s = ConsT (s,v1) left right • Qual é a complexidade de insert? • insert “cria uma nova árvore”, no entanto: Quantos novos nós são criados realmente? A interface de uma tabela imperativa package lps.simbolo; beginScope e endScope precisam de uma pilha public class Symbol { public String toString(); public static Symbol symbol(String s); } public class Table { public Table(); public void put(Symbol key, Object value) throws AlreadyBound; public Object get(Symbol key); public void beginScope(); public void endScope(); public java.util.Enumeration keys(); } • Ver exercício 5.1 para melhorar a implementação 10 Interface para tabela funcional public class Table { public Table(); public Table put(Symbol key, Object value); public Object get(Symbol key); public java.util.Enumeration keys(); } • Ver exercício 1.1 para fazer uma implementação eficiente 11 Exercício (para o projeto) • Implementar eficientemente a tabela de símbolos – Escolher um estilo 12 Implementação eficiente de Symbol • Implementação de tabela de símbolos é crítica • Operações eficientes necessárias – Comparar dois símbolos por igualdade – Extrair o código de hash (quando se usa hash) – Comparar por “<=” (quando se usam árvores) • Numa implementação ingênua, cada caractere de um string é analisado a cada – Operação hash – Cada comparação com outro string (put e get) • Como evitar comparações caractere por caractere? – Que padrão uso? 13 Padrão Flyweight • Por uniformidade, podemos querer tratar valores “leves” como objetos. Exemplo – Os caracteres de um texo (num editor) – Implementação ingênua traz problemas de espaço – Solução: criamos um pool de objetos leves • No nosso caso podemos tratar símbolos como objetos leves, assim teremos – igualdade por referência 14 Symbol: Padrão Flyweight package lps.simbolo; public class Symbol { private String name; private Symbol (String n) {name=n;} private static java.util.Map dict = new java.util.Hashtable(); public String toString() {return n; } public static Symbol symbol(String n) { String u = n.intern(); // Um string único – precisa? Symbol s = (Symbol) dict.get(u); if (s==null) { s = new Symbol(u); dict.put(u,s); } return s; } } Agora só é preciso comparar referências 15