Instituto Superior Técnico Pragmática das Linguagens de Programação 2004/2005 Primeiro Exame/Segundo Teste 17/12/2004 Número: Turma: Nome: Escreva o seu número em todas as folhas do teste. O tamanho das respostas deve ser limitado ao espaço fornecido para cada pergunta. Pode usar os versos das folhas para rascunho. O exame tem 5 páginas e a duração é de 2.0 hora. A cotação de cada questão encontra-se indicada entre parêntesis. Boa sorte. 1. (1.5) Podemos classificar os erros de um programa de acordo com o momento em que são detectados e de acordo com a parte do compilador que os detecta. Mostre fragmentos de programas (escritos nas linguagens que preferir) que demonstrem: (a) (0.3) Um erro léxico. Em C um identificador tem de começar por uma letra: int 1b = 5; (b) (0.3) Um erro sintático. Em C uma divisão necessita de dois operandos: printf("result:%d", a /); (c) (0.3) Um erro semântico estático. visível em compile time. int quo = 123 / 0; Em C uma divisão por zero (d) (0.3) Um erro semântico dinâmico. Em C uma divisão por zero invisível em compile time. int quo = 123 / x; //com x = 0 (e) (0.3) Um erro que o compilador não detecta nem gera código para detectar. Em C a indexação de arrays.
Número: 2 int v[5] = { 1, 3, 5, 7,9 ;... printf("%d", v[x]); //com x > 4 ou x < 0 2. (3.0) Em Fortran 77 as variáveis locais são tipicamente alocadas estaticamente. Em Algol e seus descendentes (Pascal, C, Ada) as variáveis locais são tipicamente alocadas no stack. Em Common Lisp e Scheme as variáveis locais são alocadas no heap (embora possam ser alocadas no stack em certos casos). (a) (1.5) Dê um exemplo de um fragmento de programa em C (ou Pascal ou Ada) que não funcionará correctamente se as variáveis locais forem alocadas estaticamente. int fact (int n) { if (n == 0) { return 1; else { return n * fact(n - 1); (b) (1.5) Dê um exemplo de um fragmento de programa em Common Lisp (ou Scheme) que não funcionará correctamente se as variáveis locais forem alocadas exclusivamente no stack. (define (make-adder n) (lambda (x) (+ x n))) (define add3 (make-adder 3)) (add3 5) 3. (2.0) O compilador GCC da GNU permite compilar programas escritos usando algumas extensões à linguagem C. Uma dessas extensões é a possibilidade de ter funções dentro de outras funções. Eis um resumo da documentação dessa extensão: 5.4 Nested Functions A nested function is a function defined inside another function. The nested function s name is local to the block where it is defined. The nested function can access all the variables of the containing function that are visible at the point of its definition. For example, here we show a nested function which uses an inherited variable named offset:
Número: 3 bar (int *array, int offset, int size) { int access (int *array, int index) { return array[index + offset]; int i; /*... */ for (i = 0; i < size; i++) /*... */ access (array, i) /*... */ It is possible to call the nested function from outside the scope of its name by storing its address or passing the address to another function: hack (int *array, int size) { void store (int index, int value) { array[index] = value; intermediate (store, size); Here, the function intermediate receives the address of store as an argument. If intermediate calls store, the arguments given to store are used to store into array. (a) (1.0) Dadas as características da linguagem C explique as limitações que são expectáveis relativamente à utilização desta extensão. Quando se invoca uma função interna a outra função, é necessário que a função que a contém ainda esteja em execução, caso contrário as referências às variáveis livres da primeira não terão significado. (b) (1.0) Esquecendo as questões de performance, explique as implicações da eliminação das limitações que referiu no ponto anterior. Seria necessário que o ambiente da função interna permanecesse activo mesmo depois da função que a contém ter terminado. Isto implica que esse ambiente passe a ter duração indefinida, só podendo ser eliminado quando se perdem todas as referências para a função interna. Isto sugere ainda a necessidade de ter um mecanismo de recolha de lixo.
Número: 4 4. (3.0) Considere o seguinte programa Scheme: (define (misterio i) (define (f1 i f) (if (> i 1) (f) (f1 2 (f3)))) (define (f3) (define (f2) i) f2) (f1 1 f1)) (a) (1.0) Admitindo que Scheme é uma linguagem de âmbito léxico, qual o valor da expressão (misterio 3)? Justifique. Quando f1 é aplicada como resultado da expressão (misterio 3), o teste do if falha e a função f1 invocase recursivamente usando como argumento o resultado da invocação da função f3 que é a função f2. No momento em que esta função é criada, é-lhe associado o ambiente léxico em que i diz respeito ao parâmetro do procedimento misterio que, como não foi alterado, tem o valor 3. Na invocação recursiva de f1, o teste é bem sucedido e é invocada a função f que designa f2 que devolve o valor de i que era 3. (b) (1.0) Admitindo que Scheme era uma linguagem de âmbito dinâmico com shallow binding, qual o valor da expressão (misterio 3)? Justifique. Com shallow binding, a associação entre uma função e o seu ambiente envolvente é feita no momento da aplicação da função. Neste caso, quando f1 é aplicada, o teste do if falha e a função f1 invoca-se recursivamente usando como argumento o resultado da invocação da função f3 que é a função f2. Nesta segunda invocação recursiva, o teste é bem sucedido e é invocada a função f que designa f2 mas é apenas neste momento que se estabelece a associação entre a função f2 e o ambiente envolvente. Neste ambiente, f2 devolve o valor de i que,
Número: 5 neste momento, é 2. (c) (1.0) Admitindo que Scheme era uma linguagem de âmbito dinâmico com deep binding, qual o valor da expressão (misterio 3)? Justifique. Com deep binding, a associação entre uma função e o seu ambiente envolvente é feita no momento da criação da função. Neste caso, quando f1 é aplicada, o teste do if falha e a função f1 invoca-se recursivamente usando como argumento o resultado da invocação da função f3 que é a função f2 mas com o ambiente associado contendo a ligação i=1 que é o último valor de i ainda visível em f2. Nesta segunda invocação recursiva, o teste é bem sucedido e é invocada a função f que designa f2 mas cujo valor devolvido é o valor de i no momento em que a função tinha sido criada, que era 1. 5. (0.5) Considere a seguinte expressão escrita em notação infixa: (a + (b - c)) * d A mesma expressão escrita em notação prefixa fica com a seguinte forma: * + a - b c d No entanto, as linguagens da família Lisp empregam uma notação prefixa ligeiramente diferente denominada notação prefixa com parêntesis: (* (+ a (- b c)) d) Explique qual a vantagem da notação prefixa com parêntesis das linguagens Lisp sobre a notação prefixa simples e dê um exemplo onde essa vantagem seja visível por comparação com o mesmo exemplo escrito em notação prefixa simples. A vantagem está na utilização de operadores que podem receber um número variável de operandos. Como exemplo, temos: (* (+ a b (- c d e) f) g h) Em notação infixa simples teríamos de escrever: * * + a + b + - - c d e f g h