Compiladores (LEIC-A – 2o Semestre 2010/2011) Linguagem “at” – Manual de Referência 10 de Maio de 2011 A nota do projecto (40% da nota final) deve ser superior a 9,5 (sem arredondamentos). Fraudes na execução do projecto terão como resultado a classificação de 0 (zero) valores e reprovação à disciplina. Datas: 2011/04/15 12:00 (entrega intermédia); 2011/05/25 12:00 (entrega final); 2011/05/30–2011/06/03 (teste prático). Dúvidas: [email protected] ou presencialmente (consultar horários e locais no Fénix). Advertem-se os alunos contra a utilização de fontes de informação não oficialmente associadas ao corpo docente. A linguagem “at” é uma linguagem imperativa e fortemente tipificada e é apresentada de forma intuitiva neste manual. São apresentadas caracterı́sticas básicas da linguagem (§1); convenções lexicais (§2); estrutura/sintaxe da linguagem (§3); especificação das funções (§4); semântica das instruções (§5); semântica das expressões (§6); e, finalmente, alguns exemplos (§7). 1 Caracterı́sticas Básicas 1.1 Tipos de dados Existem 4 tipos de dados (§3): números inteiros (int) e reais (real), cadeias de caracteres (string) e ponteiros (<tipo>). • Ocupam 4 bytes em complemento para dois, alinhados a 32 bits. Exemplo: int i = 1; • Ocupam 8 bytes em vı́rgula flutuante (norma IEEE 754). Exemplo: real r = 3.14; • São terminadas pelo carácter com o valor 0 ASCII (NULL). As variáveis e literais do tipo string podem apenas ser utilizados em atribuições, em impressões e como argumentos/retornos de funções. Exemplo: string s = "abc"; • Representam endereços de outros objectos (§3), ocupando 4 bytes, alinhados a 32 bits. Podem ser objecto de operações aritméticas (deslocamentos) e permitem aceder ao valor apontado. Exemplo: <int>pi = 0; As operações dependem dos tipos de dados a que são aplicadas. Os tipos suportados por cada operador e a operação a realizar são indicados na definição das expressões (§6). 1.2 Manipulação de nomes As entidades nomeadas (§2.6) são constantes, variáveis e funções. Nos pontos que se seguem, usa-se o termo entidade para as designar indiscriminadamente, usando-se listas explı́citas quando a descrição for válida apenas para um subconjunto. 1.2.1 Espaço de nomes e visibilidade dos identificadores O espaço de nomes global é único, pelo que um nome utilizado para designar uma entidade num dado contexto não pode ser utilizado para designar outras (ainda que de natureza diferente). Os identificadores são visı́veis desde a declaração até ao fim do alcance: ficheiro (globais) ou função (locais). A reutilização de um identificador num contexto inferior encobre possı́veis declarações em contextos superiores (excepto como definido no próximo parágrafo). Em particular, a redeclaração de um identificador numa função encobre o global até ao fim da função. É possı́vel importar sı́mbolos globais nos contextos das funções (visibilidade restringida), mas não é possı́vel defini-los (§3.4.2). É possı́vel definir funções dentro de funções. As funções internas têm acesso às variáveis das funções que as contêm. Se houver colisão de nomes, é possı́vel utilizar a notação @.var, para ver var no nı́vel superior, ou @[email protected], para ver var dois nı́veis acima, etc. (var representa qualquer variável da função contentora – os prefixos podem omitir-se se não houver ambiguidade). 1 1.2.2 Validade das variáveis As entidades globais (declaradas fora de qualquer função), existem durante toda a execução do programa. As contantes e variáveis locais a uma função existem apenas durante a sua execução. Os argumentos formais são válidos enquanto a função está activa. 2 Convenções Lexicais Para cada um dos seis grupos de elementos lexicais (tokens), considera-se a maior sequência de caracteres que constitua um elemento lexical válido. 2.1 Caracteres brancos São considerados caracteres brancos aqueles que, embora servindo para separar os elementos lexicais, não representam nenhum elemento lexical. São considerados caracteres brancos: espaço ASCII SP (0x20, ou ), mudança de linha ASCII LF (0x0A, ou \n), recuo do carreto ASCII CR (0x0D, \r) e tabulação horizontal ASCII HT (0x9, ou \t). 2.2 Comentários Os comentários funcionam como separadores de elementos lexicais. Existem dois tipos de comentários. Explicativos – Começam com // (desde que a sequência não faça parte de uma cadeia de caracteres), e acabam no fim da linha. Operacionais – Começam com /* e terminam com */ (se não fizerem parte de cadeias de caracteres). Podem estar aninhados. 2.3 Palavras chave As seguintes palavras são reservadas e não constituindo identificadores (devem ser escritas exactamente como indicado): const void int real string public use next stop return 2.4 Operadores de expressões São considerados operadores (§6) os seguintes elementos lexicais: - 2.5 + # * / % ˆ = < > == >= <= != || && ˜ [ ] ? Delimitadores e terminadores Os elementos lexicais seguintes são considerados delimitadores/terminadores: , ; ! !! { } : 2.6 Identificadores (nomes) São iniciados por uma letra (maiúscula ou minúscula) ou por _ (carácter sublinhado). O primeiro carácter é seguido por 0 (zero) ou mais letras, dı́gitos ou _. O número de caracteres que constituem um identificador é ilimitado e dois nomes designam identificadores distintos se houver alteração de maiúscula para minúscula, ou vice-versa, de pelo menos um carácter. 2.7 Literais São notações para valores constantes de alguns tipos da linguagem (não confundir com constantes, i.e., identificadores que designam elementos cujo valor não pode sofrer alterações durante a execução do programa). 2 2.7.1 Inteiros Um literal inteiro é um número não negativo (uma constante inteira pode, contudo, ser negativa: números negativos são construı́dos pela aplicação do operador menos unário (-) a um literal positivo). Um literal inteiro em decimal é constituı́do por uma sequência de 1 (um) ou mais dı́gitos decimais (dı́gitos de 0 a 9) em que o primeiro dı́gito não é um 0 (zero), excepto no caso do número 0 (zero). Neste caso, é composto apenas pelo dı́gito único 0 (zero) (em qualquer base de numeração). Um literal inteiro em octal começa sempre pelo dı́gito 0 (zero), sendo seguido de um ou mais dı́gitos de 0 a 7. Um literal inteiro em hexadecimal começa sempre pela sequência 0x, sendo seguido de um ou mais digitos de 0 a 9, de a a f ou de A a F. As letras de a a f, ou de A a F, representam os valores de 10 a 15 respectivamente. Um literal inteiro em binário começa sempre pela sequência 0b, sendo seguido de um ou mais digitos 0 ou 1. Exemplos: 007, 0x07, 0b0111. Se não for possı́vel representar o literal inteiro na máquina, devido a um overflow, deverá ser gerado um erro lexical. 2.7.2 Reais Os literais reais são expressos em notação cientı́fica (tal como em C) ou em notação de engenharia, onde a parte exponencial não existe e o ponto decimal é substituı́do por uma das letras y (yocto/iocto = 10−24 = 1E-24), z (zepto = 10−21 = 1E-21), a (atto/ato = 10−18 = 1E-18), f (femto/fento = 10−15 = 1E-15), p (pico = 10−12 = 1E-12), n (nano = 10−9 = 1E-9), u (micro = 10−6 = 1E-6), m (milli/mili = 10−3 = 1E-3), k (kilo/quilo = 103 = 1E+3), M (mega = 106 = 1E+6), G (giga = 109 = 1E+9), T (tera = 1012 = 1E+12), P (peta = 1015 = 1E+15), E (exa = 1018 = 1E+18), Z (zetta/zeta = 1021 = 1E+21), Y (yotta/iota = 1024 = 1E+24). Quando a notação for ambı́gua, é preferida a interpretação segundo a notação de engenharia. Exemplo: 12y34 = 12.34e-24 = 12.34 × 10−24 2.7.3 Cadeias de caracteres São consituı́das por dois ou mais caracteres iniciadores seguidos, sem separadores, mas constituindo elementos lexicais distintos; ou uma só cadeia de texto (opcionalmente, podem existir caracteres brancos entre os elementos lexicais que compõem a cadeia). Os caracteres iniciadores podem ser valores inteiros (em qualquer base e que representem um só carácter), caracteres individuais ou cadeias de texto. Os caracteres individuais são delimitados com o carácter plica (’) e contêm um só carácter ou uma sequência especial iniciada por (\). Sequências especiais podem ser representada pelos caracteres ASCII LF, CR e HT (\n, \r e \t, respectivamente), plica (\’), barra (\\), ou 1 ou 2 digitos hexadecimais (e.g. \0a ou apenas \A se o carácter seguinte não representar um digito hexadecimal). Uma cadeia de texto, começa e termina com o carácter aspa ("), pode conter qualquer número de caracteres (excepto o 0 ou NULL). Dentro das cadeias de texto, os caracteres utilizados para iniciar ou terminar comentários têm o seu valor normal ASCII, não iniciando ou terminando qualquer comentário. As sequências especiais dos caracteres individuais são válidas nas cadeias de texto, excepto a sequência \’ que é substituı́da pela sequência \". 2.7.4 Ponteiros O único literal admissı́vel para ponteiros é 0 (zero), indicando o ponteiro nulo. 3 Gramática A gramática da linguagem pode ser resumida pelas regras abaixo. Considerou-se que os elementos em tipo fixo são literais, que os parênteses curvos agrupam elementos, que elementos alternativos são separados por uma barra vertical, que elementos opcionais estão entre parênteses rectos, que os elementos que se repetem zero ou mais vezes estão entre e . A barra vertical e os parênteses são elementos lexicais da linguagem quando representados em tipo fixo. 3.1 Tipos, elementos lexicais e definição de expressões A gramática omite as definições dos tipos de dados (§1.1), dos elementos lexicais identificador (§2.6), literal-inteiro (§2.7.1), literal-real (§2.7.2), carácter (§2.7.3) e cadeia (§2.7.3), assim como a definição de expressões (§6). 3 ficheiro declaração variável variáveis função tipo qualificador corpo literal secção-inicial secção secção-final bloco instrução instrução-condicional instrução-de-iteração 3.2 → → → → → → → → → → → → → → → → → → → → → declaração declaração variável ; | função [ const ] [ qualificador ] tipo ident [ = expressão ] variável , variável [ qualificador ] ( tipo | void ) ident ( [ variáveis ] ) [ -> literal ] [ corpo ] int | real | string | < tipo > public | use [ secção-inicial ] secção [ secção-final ] inteiro | real | carácter | cadeia << bloco [ [ expressão ] ] bloco ( [ expressão ] ) bloco bloco >> bloco { declaração instrução } expressão ( ; | ! | !! ) next | stop | return | instrução-condicional | instrução-de-iteração | bloco [ expressão ] # instrução [ expressão ] ? instrução [ expressão ] ? instrução [ : instrução ] [ [ variáveis ] ; [ expressão ] ; [ expressão ] ] instrução [ [ expressão ] ; [ expressão ] ; [ expressão ] ] instrução Left value Os elementos de uma expressão que podem ser utilizados como left-values encontram-se individualmente identificados em §6. Todos os left-values representam posições de memória passı́veis de ser modificadas. 3.3 Ficheiros Os ficheiros que contêm os programas são uniformes. É designado por principal o que contiver a função de arranque (at). 3.4 Declaração de variáveis e constantes Cada declaração permite declarar uma única variável ou constante e inclui os componentes descritos nos pontos seguintes. 3.4.1 Constante A linguagem define identificadores constantes: precedem a declaração pela palavra reservada const, impedindo que o identificador declarado possa ser utilizado em operações que modifiquem o seu valor. Caso um identificador designe uma constante inteira não pública (ver §3.4.2) o seu valor deverá ser directamente substituı́do no código, não ocupando espaço. 3.4.2 Sı́mbolos globais Existem dois qualificadores opcionais, que gerem a utilização de identificadores globais: Sı́mbolos públicos – public – são globalmente visı́veis no módulo actual e acessı́veis a partir de outros módulos. Pré-declarações – use – são constantes, variáveis ou funções, que podem estar declaradas mais à frente no ficheiro ou num outro ficheiro (possivelmente noutra linguagem). Apenas é possı́vel aplicar use a declarações. public pode também ser aplicada a definições. 3.4.3 Inicialização A existir, inicia-se com o operador = seguido de expressão do tipo declarado: inteiro (uma expressão inteira), real (uma expressão real), ponteiro (uma expressão do tipo ponteiro). 4 As cadeia de caracteres são (possivelmente) inicializadas com uma lista não nula de valores sem separadores (opcionalmente, poderão existir caracteres brancos entre os elementos lexicais que compõem a cadeia de caracteres). Os valores podem ser inteiros (§2.7.3), caracteres individuais ou cadeias de texto. Estes valores são sempre constantes, independentemente de o identificador que as designa ser constante ou não. Notar que declarações de constantes não iniciadas só são possı́veis se corresponderem a identificadores pré-declarados, pertencentes a outros módulos (§3.4.2). 4 Funções Uma função permite agrupar um conjunto de instruções num corpo, executado com base num conjunto de parâmetros (os argumentos formais), quando é invocada a partir de uma expressão. 4.1 Declaração As funções são sempre designadas por identificadores constantes precedidos do tipo de dados devolvido pela função. As funções que recebam argumentos devem indicá-los no cabeçalho. Funções sem argumentos definem um cabeçalho vazio. A declaração de uma função sem corpo é utilizada para tipificar um identificador exterior ou para efectuar declarações antecipadas (utilizadas para pré-declarar funções que sejam usadas antes de ser definidas, por exemplo, entre duas funções mutuamente recursivas). Caso a declaração tenha corpo, define-se uma nova função. Quando a declaração de uma função ocorra dentro de outra função, terá de ser necessariamente terminada por ;. 4.2 Invocação A função só pode ser invocada através de um identificador que refira uma função previamente declarada ou definida. O identificador especial @ pode ser utilizado para invocar recursivamente a função actual. Caso existam argumentos, na invocação da função, o seu identificador é seguido de uma lista de expressões delimitadas por parênteses curvos. A lista de expressões é uma sequência, possivelmente vazia, de expressões separadas por vı́rgulas. As expressões são avaliadas da direita para a esquerda antes da invocação da função (convenção Cdecl) e o valor resultante passado por cópia (passagem de argumentos por valor). O número e tipo de parâmetros actuais deve ser igual ao número e tipo dos parâmetros formais da função invocada. Caso existam parâmetros opcionais, iniciados na declaração da função, os seus valores são utilizados na falta de parâmetros actuais e apenas se forem os últimos. A ordem dos parâmetros actuais deverá ser a mesma dos argumentos formais da função a ser invocada. Os parâmetros actuais devem ser colocados na pilha de dados pela ordem inversa da sua declaração (o primeiro no topo da pilha) e o endereço de retorno no topo da pilha. A função chamadora coloca os argumentos na pilha e é também responsável pela sua remoção, após o retorno da função chamada (convenção Cdecl). 4.3 Corpo O corpo de uma função consiste num conjunto de secções condicionais. Existem duas secções especiais: a secção inicial (se existir, é sempre executada e antes de qualquer outra) e a secção final (se existir, é sempre executada e depois de qualquer outra). As outras secções são executadas se a condição que as rege for verdadeira. As condições entre parênteses rectos correspodem a secções exclusivas (depois de seleccionada e executada uma delas, apenas a final é executada). As condições entre parênteses curvos correspondem a secções inclusivas (depois de seleccionada e executada uma delas, é testada a seguinte). Condições vazias ou ausentes correspondem a secções exclusivas com expressões verdadeiras. O valor devolvido por uma função, através de atribuição ao left-value especial @, deve ser sempre do tipo declarado. Variáveis definidas na secção-inicial são visı́veis em toda a função. Variáveis definidas noutras secções são visı́veis apenas nessa secção. Se existir um valor declarado por omissão para o retorno da função (indicado pela notação -> seguindo a assinatura da função), então deve ser utilizado se não for especificado outro. A especificação do valor de retorno por omissão é obrigatoriamente um literal do tipo indicado. É um erro especificar um valor de retorno se o tipo de retorno for void. 5 Uma instrução return numa qualquer secção que não a final, causa a sua interrupção imediata e a execução da secção final (se existir). Se a instrução return ocorrer na secção final, causa a interrupção da função. Um sub-bloco de função (usado, por exemplo, numa instrução condicional ou de iteração) pode definir variáveis ou constantes. As secções do corpo de uma função podem definir outras funções que têm acesso a todas as declarações (prévias ou da secção inicial) da função que a contém. Caso existam ambiguidades na nomenclatura, o prefixo @. pode ser usado para referir os identificadores do nı́vel acima (§1.2.1). 5 Instruções Excepto quando indicado, as instruções são executadas em sequência. 5.1 Instrução condicional Se a expressão contida ente [ e ] for diferente de 0 (zero) então o corpo que segue o ? ou # é executado. Caso existam cadeias entre [ e ] consecutivas, as suas expressões serão sucessivamente executadas, caso todas as condições anteriores sejam nulas (iguais a 0). Assim que uma dessas condições seja diferente de 0 (zero), o seu corpo (depois do ? seguinte) é executado e a instrução termina. Caso exista um conjunto :, o seu corpo só é executado se nenhum dos anteriores corpos da instrução o tenha sido. 5.2 Instruções de iteração Controladas por três grupos de expressões, podendo o primeiro grupo corresponde a declarações de variáveis locais ao ciclo. O comportamento do ciclo é análogo ao for da linguagem C. 5.3 Instrução de retorno Indicada pela palavra reservada return, a existir deverá ser a última instrução do bloco em que se insere. Ver §4.3. 5.4 Instrução de continuação Indicada pela palavra reservada next (quando existe, é a última instrução do seu bloco): reinicia o ciclo mais interior em que a instrução se encontrar, tal como a instrução continue em C. Esta instrução só pode existir dentro de um ciclo. 5.5 Instrução de terminação Indicada pela palavra reservada stop (quando existe, é a última instrução do seu bloco): termina o ciclo mais interior em que a instrução se encontrar, tal como a instrução break em C. Esta instrução só pode existir dentro de um ciclo. 5.6 Expressão como instrução Qualquer expressão pode ser utilizada como instrução, mesmo que não produza qualquer efeito secundário. Caso a expressão seja terminada por ! ou !! (impressão com mudança de linha) o seu valor deve ser impresso. O valor deve ser impresso em decimal para os valores numéricos (inteiros ou reais) e na codificação nativa para valores do tipo string. Os valores do tipo ponteiro não podem ser impressos. 6 Expressões Uma expressão é uma representação algébrica de uma quantidade, i.e., todas as expressões devolvem um valor. As expressões são sempre avaliadas da esquerda para a direita, independentemente da associatividade do operador. 6 A precedência dos operadores é a mesma para operadores na mesma secção, sendo as secções seguintes de menor prioridade que as anteriores. O valor resultante da aplicação da expressão bem como a sua associatividade são indicados para cada operador. A tabela seguinte que resume os operadores, por grupos de precedência decrescente: primária unária potência multiplicativa aditiva comparativa igualdade “não” lógico “e” lógico “ou” lógico atribuição ( ) [ ] - + # ? ˆ * / % + < > <= >= == != ˜ && || = não associativos não associativos da direita para a esquerda da esquerda para a direita da esquerda para a direita da esquerda para a direita da esquerda para a direita não associativo da esquerda para a direita da esquerda para a direita da direita para a esquerda Os operadores são como em C, excepto os operadores de leitura (@), de potência (ˆ), reserva de memória (#) e negação lógica (˜). 6.1 Expressões primárias 6.1.1 Identificadores Um identificador é uma expressão se tiver sido declarado. Um identificador pode denotar uma variável ou uma constante. Um identificador é o caso mais simples de um left-value, ou seja, uma entidade que pode ser utilizada no lado esquerdo (leftvalue) de uma atribuição. O identificador especial @ designa o valor de retorno da função actual (quando usado como left-value). 6.1.2 Literais Os literais são como definidos nas convenções lexicais (§2.7). 6.1.3 Parênteses curvos Uma expressão entre parênteses curvos tem o valor da expressão sem os parênteses e permite alterar a prioridade dos operadores. Uma expressão entre parênteses não pode ser utilizada como left-value (ver também §6.1.4). 6.1.4 Indexação Uma expressão indexação devolve o valor contido numa posição indicada por um ponteiro. Consiste de uma expressão ponteiro seguida do ı́ndice entre parênteses rectos. Se a expressão ponteiro for um left-value, então a expressão indexação poderá também ser um left-value. Exemplo: p[0] (acesso à posição apontada por p). 6.1.5 Invocação Uma função só pode ser invocada através de um identificador que corresponda a uma função previamente declarada ou definida. O identificador especial @ designa a função actual (para chamadas recursivas). 6.1.6 Leitura A operação de leitura de um valor inteiro ou real pode ser efectuado pela expressão @, que devolve o valor lido, de acordo com o tipo esperado (inteiro ou real). Caso se use como argumento dos operadores de impressão (! ou !!), deve ser lido um inteiro. 6.2 Expressões unárias 6.2.1 Identidade e simétrico A expressão identidade (+) devolve o valor do seu argumento inteiro ou real. A expressão simétrico (-) devolve o simétrico do seu argumento inteiro ou real. 7 6.2.2 Reserva de memória A expressão reserva de memória (#) devolve o ponteiro que aponta para a zona de memória na pilha da função actual contendo espaço suficiente para o número de objectos indicados pelo seu argumento inteiro. O tipo de retorno é idêntico ao do left-value utilizado e o espaço deve ser calculado em função do tipo apontado. 6.3 Expressão de potência A operação é apenas aplicável a valores inteiros, devolvendo a multiplicação do primeiro argumento por ele próprio tantas vezes quantas o valor do segundo argumento. 6.4 Expressões aditivas As operações são apenas aplicáveis a valores inteiros e reais, devolvendo o resultado da respectiva operação algébrica. Estas operações podem realizar-se sobre ponteiros, tendo o significado das operações correspondentes em C/C++: (i) deslocamentos, i.e., um dos operandos deve ser do tipo ponteiro e o outro do tipo inteiro; (ii) diferenças de ponteiros, i.e., apenas quando se aplica o operador - a dois ponteiros do mesmo tipo (o resultado é o número de objectos do tipo apontado entre eles). 6.5 Expressões multiplicativas As operações são apenas aplicáveis a valores inteiros e reais, devolvendo o resultado da respectiva operação algébrica. 6.6 Expressões de grandeza As operações são aplicáveis a valores inteiros ou reais devolvendo o valor inteiro 0 (zero) caso seja falsa e um valor diferente de zero caso contrário. 6.7 Expressões de igualdade As operações são aplicáveis a valores inteiros e reais, tal como no caso anterior. 6.8 Expressões de negação lógica A operação é aplicável a valores inteiros, devolvendo o valor inteiro 0 (zero) caso o argumento seja diferente de 0 (zero) e um valor diferente de zero caso contrário. 6.9 Expressões de junção lógica A operação é aplicável a valores inteiros, devolvendo um valor diferente de zero caso ambos os argumentos sejam diferente de 0 (zero) e um valor diferente de zero caso contrário. Se o primeiro argumento for 0 (zero) o segundo argumento não é avaliado. 6.10 Expressões de alternativa lógica A operação é aplicável a valores inteiros, devolvendo o valor inteiro 0 (zero) caso ambos os argumentos sejam iguais a 0 (zero) e um valor diferente de zero caso contrário. Se o primeiro argumento for diferente de zero o segundo argumento não é avaliado. 6.11 Expressões de atribuição O valor da expressão do lado direito do operador é guardado na posição indicada pelo left-value do lado esquerdo do operador de atribuição. Só podem atribuı́dos valores a left-values do mesmo tipo. 8 6.12 Expressão de indicação de posição Expressa pelo operador sufixo ?, indica o endereço do objecto (endereçável) indicado como argumento. Retorna um valor do tipo do ponteiro apropriado para o objecto em causa. Exemplos: a? (indica o endereço de a). 7 Exemplos Os exemplos apresentados não são exaustivos, pelo que não ilustram todos os aspectos da linguagem. O cálculo da função de Ackermann: esta função tem um crescimento muito rápido pelo que nos computadores actuais, mesmo utilizando C, os argumentos não deverão exceder m=3 e n=12 para executar em poucos segundos. public int cnt = 0; public int ackermann(int m, int n) { cnt = cnt + 1; [m == 0] ? @ = n+1; [n == 0] ? @ = @(m-1, 1); : @ = @(m-1, @(m, n-1)); } Exemplo da utilização das funções noutro ficheiro: use use use use int argc() string argv(int n) int ackermann(int m, int n) int getcnt() public int at() -> 0 { int m = 0; int n = 0; "Teste para a função de Ackermann"!! "Introduza M (<3): "! m = @; "Introduza N (<5): "! n = @; "Valor de ackermann("! m! ", "! n! "): "! ackermann(m, n)!! "Calls to ackermann: "! getcnt()!! } Estão disponı́veis outros exemplos na página da disciplina. 8 Omissões e Erros Casos omissos e erros serão corrigidos em futuras versões do manual de referência. 9