• Nenhum resultado encontrado

Semântica Denotacional Adaptação de A Practical Introduction to Denotational Semantics Por Lloyd Allison Cambridge University Press, 1986

N/A
N/A
Protected

Academic year: 2021

Share "Semântica Denotacional Adaptação de A Practical Introduction to Denotational Semantics Por Lloyd Allison Cambridge University Press, 1986"

Copied!
10
0
0

Texto

(1)

Semântica Denotacional

Adaptação de

A Practical Introduction to Denotational Semantics

Por Lloyd Allison

Cambridge University Press, 1986

1- Introdução

Semântica denotacional é um método formal para definir a semântica de linguagens de programação. Ela é do interesse do projetista de linguagens, quem escreve compiladores e programadores. Estes indivíduos têm critérios diferentes para julgar um método – ele deve ser conciso, não ambíguo, aberto à análise matemática, verificável mecanicamente, executável e legível dependendo do seu ponto de vista. A semântica denotacional não pode ser todas as coisas para todas as pessoas, mas é uma tentativa de satisfazer a estes objetivos diversos. É um método formal pois é baseado em fundamentos matemáticos bem entendidos e usa uma notação rigorosamente definida (meta-linguagem).

A completa definição de uma linguagem de programação é dividida em sintaxe, semântica e algumas vezes, pragmática. Sintaxe define a estrutura das sentenças legais na linguagem. A semântica dá o significado dessas sentenças. A pragmática cobre o uso de uma implementação de uma linguagem e não será mais mencionada neste curso.

No caso da sintaxe, gramáticas livre de contexto expressas em BNF ou em diagramas de sintaxe têm sido de grande benefício para cientistas da computação desde que Backus e Naur especificaram formalmente a sintaxe de Algol-60. Atualmente, todas as linguagens de programação têm suas sintaxes dadas desta forma. O resultado foi uma sintaxe mais limpa, melhores métodos de parsing, geradores de parsers e melhores manuais de linguagem. Até hoje, nenhum formalismo semântico atingiu tal popularidade e a semântica de novas linguagens são invariavelmente dadas em linguagem natural.

O problema típico que o programador enfrenta é escrever um programa que irá transformar dados satisfazendo algumas propriedades ou asserções ´P´ em resultados satisfazendo ´Q´.

{P} programa {Q}

A linguagens das asserções é a lógica de predicados. Esta formulação trata um programa como um transformador de predicados.

Se concentrarmos nos predicados em uma transformação somos levados ao estilo axiomático de semântica. Isto foi sugerido por Floyd e formalizado por Hoare, Dijkstra e vários outros. O método é legível e muito útil para programadores e projetistas de algoritmos. Ele está intimamente conectado com a disciplina de programação estruturada. Ele apresenta algumas dificuldades (não insuperáveis) na definição de algumas características das linguagens de programação, especialmente gotos e efeitos colaterais em expressões. Há uma debate acalorado se esta é uma limitação do método ou uma indicação

(2)

que estas características são difíceis de serem usadas e perigosas. Observe que o interesse em lógica de predicados criou a linguagem de programação Prolog.

Se concentrarmos no programa como uma função mapeando entradas que satisfazem a P em resultados que satisfazem a Q somos levados às semânticas operacionais e denotacionais. Semântica operacional imagina um programa rodando em uma máquina abstrata. A máquina pode ser complemente diferente de qualquer computador real, tanto de baixo nível, simples e fácil de analisar como de alto nível com uma tradução fácil a partir da linguagem de programação. A máquina e a tradução devem ser especificadas. Tal definição é mais útil para um projetista de compilador se a máquina abstrata está próxima da máquina real.

A semântica denotacional reconhece a sutil diferença entre uma função – um conjunto de pares ordenados possivelmente infinito {<entradai, saidai>}- e um algoritmo como uma descrição finita de uma função. Um programa é o algoritmo escrito em uma linguagem particular de programação. Um programa define, ou denota, uma função. Uma semântica denotacional de uma linguagem dá o mapeamento de programas na linguagem para a função denotada.

Exemplo:

Fatorial = {<0,1>,<1,1>,<2,2>,<3,6>,<4,24>,...} Fat = if n =0 then 1 else n * fat(n-1)

Uma boa semântica deve confirmar que o programa denota a função fatorial.

Semântica denotacional é escrita em λ-notação que é o cálculo λ de Church com tipos de dados. Ela possui uma teoria matemática bem desenvolvida e fundamentos que foram completamente investigados. O método é conciso e poderoso o suficiente para descrever as características das linguagens de programação atuais. Definições formais só são legíveis com prática, sendo a notação equivalente a uma linguagem de programação poderosa mas difícil de ler. É mais adequada ao projetista e implementador de uma linguagem de programação que ao programador.

Uma vantagem da semântica denotacional é que ela pode ser mecanizada. Mosses desenvolveu um interpretador para semântica denotacional que permite que uma definição seja executada. Algumas linguagens funcionais tais como ML são muito próximas da notação λ com tipo, e podem ser usadas para escrever e executar definições denotacionais. Parece que uma definição axiomática e uma definição denotacional são bons parceiros. A primeira para o programador e a última para o projetista de linguagem. Por exemplo, a semântica de Montagne tenta dar um estilo de semântica denotacional para um subconjunto de inglês. Lógicos estão preocupados com os objetos que substantivos, variáveis e predicados “significam” e com qual é o significado de uma afirmação ser verdadeira.

(3)

1.1 Um exemplo

Para dar um sabor de semântica denotacional, o exemplo clássico de números decimais é apresentado. Os números decimais formam uma linguagem, Num, sobre o alfabeto {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Ela pode ser definida pela gramática

ν ::= ν δ|δ

δ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

O símbolo ::= pode ser lido como “é” ou “pode ser substituído por”. O “|” pode ser lido como “ou”. Um dígito δ é um 0, ou um 1, ou um 2, e assim por diante. Um numeral é simples dígito ou um numeral seguido de um dígito. As letras gregas ν e δ são variáveis sintáticas sobre partes da linguagem Num.

Os números decimais são normalmente tomados por significarem, ou denotarem, inteiros que são objetos abstratos. Esta interpretação convencional pode ser feita formalmente pela definição de uma função de valoração V:

V: Num → Int V [ν δ] = 10 x V[ν] + V[δ] V[0] =0 V[1] =1 V[2] =3 V[3] =3 V[4] =4 V[5] =5 V[6] =6 V[7] =7 V[8] =8 V[9] =9

V é uma função das sentenças da linguagem Num para os inteiros Int. V é definido por

uma análise caso a caso das alternativas da gramática para Num. Elementos da linguagem estão dentro de chaves para distingui-los da meta linguagem fora. Dentro das chaves estão seqüências de caracteres (strings). Os inteiros fora das chaves estão em itálico. 7 é um caracter que denota o inteiro 7. É impossível escrever alguma coisa sem que usar nomes e consequentemente, somos obrigados a adotar alguma convenção.

O valor que um numeral em particular pode agora ser calculado: V[123] = 10 x V[12] + 3

= 10 x (10x V[1]+2)+3

= 123

Isto pode disparar a pergunta “e daí? Não é óbvio?”. A resposta é que devemos estar satisfeitos pela definição formal concordar com a intuição em casos simples. Esta é uma característica de boas teorias. O formalismo é necessário quando a intuição não é forte o suficiente. O leitor que não percebe que 7 = ‘7’não apenas não é verdadeiro mas é um erro em Pascal, pode ter perdido o sentido. Note que V capturou a essência da notação posicional, e que V[123] = V[0123] e assim por diante.

(4)

A definição de V pose se usada para projetar um trecho de código que existe na maioria dos compiladores:

If ch in [‘0’..’9’] then Begin n:= ord(ch) – ord(‘0’);

Ch:= nextch {retorna o próximo char e avança a entrada} While ch in [‘0’..’9’] do

Begin n:= n*10 + ord[ch()- ord(‘0’); Ch := nextch;

End End

Numerais que ocorrem em um programa devem ter seus valores calculados pelo compilador.

Exercício:

1) Se a definição de V é alterada para

V [ν δ] = - 10 x V[ν] + V[δ]

• Quais são as novas características de V?

• Qual vantagem esta nova interpretação de Num tem?

2- Fundamentos Básicos

Esta seção introduz a notação básica para sintaxe e semântica por exemplos. Desta forma, algumas idéias são usadas antes de sua (adequada) definição; no entanto, o que está acontecendo deve ser intuitivamente claro.

2.1 Sintaxe abtrata

É comum fazermos distinção entre sintaxe abstrata e sintaxe concreta de uma linguagem de programação. A sintaxe concreta define a forma como a linguagem é efetivamente escrita em papel, ou na tela e inclui informação suficiente para fazer o parse. A sintaxe abstrata especifica as relações partes lógicas da linguagem. Consequentemente, pode ser mais simples e pode não conter informação suficiente para fazer o parse da linguagem sem ambiguidade. A semântica denotacional é normalmente baseada na sintaxe abstrata mais simples e mais fundamental, embora não isto não seja essencial.

Como um exemplo, um comando if pode ter a seguinte sintaxe concreta: γ::= if ε then γ else γ fi

mas a sintaxe abstrata pode ser dada como γ::= if ε then γ else γ

(5)

ou mesmo γ::= ε → γ, γ

que dá ênfase nos componentes essenciais que são uma expressão de controle e dois subcomandos (γ é a variável sintática para comando).

A sintaxe concreta de uma expressão pode ser : ε ::= ε + τ | τ

τ ::= τ x ψ | ψ

ψ ::= (ε) | a | b | c | ...

Isto especifica que a + b x c deve ser parsed como a + (b x c) e não como (a + b) x c. A seqüência de regras dá aos operadores +, x e ( ) em ordem crescente de prioridade ou de força de ligação. Esta informação pode ser perdida na sintaxe abstrata

Exp:

ε ::= ε + ε | ε x ε | a | b | c | ...

que especifica apenas a informação crucial que uma expressão é ou um operando ou um operador aplicado a duas sub-expressões. Esta é uma gramática ambígua e permitiria a + b x C ser parsed das duas formas se usada para este propósito. Os parênteses ( ) não precisam aparecer já que servem apenas para controlar o parsing.

No nível prático, sintaxe concreta pode ser usada para codificar um parser descendente-recursivo. Procedure Exp; Procedure term; Procedure opd; Begin {opd} If ch = ‘( Then begin{sub-exp}

Insymbol {sy obtem prox símbolo} Exp;

Check([‘)’]) {chama insymbol de sy está no conjunto, senão erro} end

else check ([‘a’..’z’])

end; begin {term} opd; while ch = ‘x’do begin insymbol; opd; end; begin {exp} term;

(6)

while ch = ‘+’do begin insymbol; term; end; end;

A sintaxe abstrata pode definir a estrutura de dados, ou árvore de parse, que pode ser produzida por um parser

Type expntype = (opd, plus, times);

Xpn = ↑node;

Node = record cast t: expntype of

Opd: (id:ch);

Plus, times: (left, right:expn)

End;

Definições semânticas são geralmente baseadas em sintaxe abstrata. Assume-se que em algum lugar a sintaxe concreta existe e que um parser pode analisar os programas corretamente. O resultado do parser corresponde a sintaxe abstrata e as definições denotacionais são dadas em termos dele.

2.2 Execução seqüencial

É óbvio que numerais sensatamente correspondem a números. Os objetos que programadores denotam são mais complexos. É comum pensar um programa como transformando um estado a cada atribuição.

Um estado define os valores correntes dos identificadores de variáveis no programa S = Ide →Valor

Os estados, S, são o conjunto do tipo de dados de funções para identificadores, Ide, para Valor.

Um estado particular σ :S

é uma função particular de variáveis para valores. Os dois pontos ‘:’ indicam que σ está no tipo de dados S. Suponha σ [x] = 6 e σ [y]=9 então abusando da notação axiomática, poderíamos escrever:

{σ}x := x + 1 {σ’} onde

σ ’[x] = 7, σ’[y] = 9.

(todos os predicados válidos antes da atribuição podem ser inferidos de σe todos que são válidos depois podem ser inferidos de σ’)

(7)

Um comando denota uma transformação de estado em

S → S

A função de valoração para comandos, C, mapeia comandos sobre transformações de estado:

C: Cmd → (S → S)

→ é definido como associando à direita, consequentemente é normalmente escrito C: Cmd → S → S

Um comando, γ:Cmd, denota a transformação de estados C[γ]: S → S

Dado o exemplo acima, C[x := x + 1] σ = σ’

Note que parênteses, por exemplo em volta de σ, são omitidos sempre que possível, e que aplicação de função é associativa à esquerda.

F x y ≡ (f(x))(y)

Uma característica central, na verdade uma das principais características de um grande número de linguagens de programação incluindo Basic, Cobol, Fortran e Pascal é a execução seqüencial. Um programa é “rodado” pela execução do primeiro comando em seguida o segundo comando e assim por diante (assumindo a inexistência de desvios). Todas estas linguagens contêm seqüências de comandos. A sintaxe abstrata a seguir define isto, embora seja próxima de Pascal.

CMD γ ::= γ; γ | ...

O ‘;’ em Pascal pode ser lido como “e então”. Execute comando um e então execute comando dois.

Um comando da forma γ1; γ2 denota transformação de estado C[γ2] ο C[γ1]

Onde ο é composição funcional. Note a ordem; (f οg)(x) = f(g(x)). Exemplo: dado σ [x] =6 e σ [y]=9 C[x:=x+1; y := x] σ = C[y:=x] οC[x:=x+1] σ = C[y:=x] σ’ = σ’’

(8)

onde σ’’[x] = σ’’[y] = 7.

Para estendermos a sintaxe abstrata para incluir comandos compostos adicione: CMD

γ ::= begin γ end | γ; γ | ... C[begin γ end] = C[γ ]

Usando o fato da composição funcional ser associativa, os três esquemas de programa a seguir podem ser mostrado equivalentes:

(1) γ1; γ2; γ3

(2) begin γ1; γ2 end; ;γ3 (3) γ1; begin γ2; γ3 end

Isto vai de acordo com a intuição e aumenta a confiança de que a definição de C é correta. A definição da função de valoração C pode ser dada em muitas notações. Ela não pode ser traduzida diretamente em Pascal padrão por razões não cobertas aqui, mas o trecho seguinte é muito próximo:

Function C(g: cmd; s: state): State; ...

case g↑.tag of

semicolon: C := C(g↑.right, C(g↑.left, s)) ...

end

O segundo comando, g↑.right, é aplicado ao resultado do primeiro comando g↑.left. 2.3 Expressões

A classe de expressões em uma linguagem de programação varia de linguagem para linguagem mas deve incluir palo menos expressões lógicas (booleanas) para comandos de controle.

Exp:

ε ::= ε and ε | ε or ε | not ε | true | false | ξ Ide:

ξ ::= sintaxe para identificadores E: Exp → S → Bool

(9)

E[ε or ε´]σ = E[ε]s v E[ε´]σ E[not ε]σ = ~E[ε]

E[true]σ = true E[false]σ = false E[ξ]σ = σ[ξ]

O valor denotado por uma expressão depende do estado uma vez que pode conter variávies. E mapeia uma expressão sobre uma função de estados para valores, aqui apenas valores lógicos (booleanos). Para uma expressão em particular, e, E[e]: S→Bool é uma função de S par Bool, E[e]s:Bool. Esta definição de E assuma que espressões não têm efeito colateral que podem alterar o estado.

2.4 Comandos de controle

Um comando if pode ser acrescentado à sintaxe de comandos por: Cmd:

γ ::= if ε then γ else γ | ... Sua semântica é dada por C[if ε then γ else γ ´]σ =

= Χ[γ]σ if E[e]σ=true = Χ[γ´]σ caso contrário Uma funão especial Cond é definida

Cond: (S→S)2 → Bool → (S→S) Cond (p,q)true = p

Cont (p,q)false = q

Cond pega duas transformações de estados p e q e produz uma função de uma chave lógica para uma transformação de estado. Se a chave for trueCond pega a primeira transformação, caso contrário a segunda.

C[if e the g else g´]σ

= cond(C[γ], C[γ´])(E[ε]σ)s

Vale a pena examinar isto para ter certeza que todos os tipos estão corretos C: Cmd → S → S

C[if...]: S → S C[if...]σ: S

Cond(C[γ], C[γ´]):Bool → S → S E[ε]: S → Bool

(10)

E[ε]σ: Bool

Cond(...)(E[ε]σ): S → S Cond(...)(E[ε]σ)s: S

A equação usando Cond é muito difícil de ler e é frequentemente escrita na forma infixa que é mais conveniente:

C[if ε then γ else γ ´]σ

= (if E[ε]σ then C[γ] else C[γ´])s

Observe que o if dentro de [] é parte da linguagem sendo definida e o ´if´ fora é parte da meta-linguagem. Note que importante que a definição não seja dada como

Cond(C[γ]σ, C[γ´]σ)(E[e]σ) Ou

If E [ε]σ then C[γ]σ else C[γ´]σ

Esta definição (errada) implica que os dois comandos são executados para produzir dois estados alternativos e um desses estados é selecionado dependendo no resultado da expressão. A definição original diz use o resultado da expressão para pegar umas das

transformações de estado denotados pelos dois comandos e execute apenas este comando.

Note também que uma especificação alternativa de Cond seria necessária na segunda definição para fazer com que os tipos casassem.

Um comando while pode ser especificado sintaticamente como: Cmd:

γ :: while ε do γ | ... O resultado de C[while ε do γ]σ

Depende de E[ε]σ. Se for falso, σ é inalterado. O resultado é apenas σ ou Id(σ) onde Id: S→S é a função identidade. Se o resultado da expressão for verdadeiro o resultado é C[while ε do γ] o C[γ]σ

Ou seja, aplique g à s e repita o laço. Em resumo, C[while ε do γ]

= Cond[while ε do γ] o Χ[γ],Id)(E(ε]σ)σ Ou de forma equivalente

= (if E[ε]σ then C[while ε do γ] o C[γ] else Id)σ

Esta é uma definição recursiva no sentido em que o significado do laço while depende dele próprio. Isso é um causador potencial de problemas uma vez que este tipo de equação pode ter zero, uma ou um número infinito de soluções! Naturalmente a semântica de uma linguagem de programação deveria ter exatamente uma solução ou significado.

Referências

Documentos relacionados

Incidirei, em particular, sobre a noção de cuidado, estruturando o texto em duas partes: a primeira será uma breve explicitação da noção de cuidado em Martin Heidegger (o cuidado

Nos termos da legislação em vigor, para que a mensagem de correio eletrônico tenha valor documental, isto é, para que possa ser aceito como documento original, é necessário existir

O armazenamento da embalagem vazia, até sua devolução pelo usuário, deve ser efetuado em local coberto, ventilado, ao abrigo de chuva e com piso impermeável, no próprio local

Após o barramento, a vazão do açude regularizada mantém o efeito de molhe hidráulico do Rio Pacoti durante o ano todo, favorecendo a sedimentação na praia de

A coadministração de eritromicina em voluntários sadios (18 a 43 anos de idade) não mudou significativamente a exposição sistêmica do anlodipino (22% de aumento

Vinte anos depois de fazer parte de um dia histórico para o Cruzeiro e para o estádio Mineirão, uma família resolve se reencontrar para relembrar toda a emoção vivida no dia 22

De forma mais detalhada, vemos que, na eleição municipal de 2000, o partido político que deteve o maior número de municípios foi o PMDB, com 1.255 prefeituras conquistadas,

Este trabalho foi desenvolvido com o objetivo de avaliar in vitro a resistência adesiva em reparos de resina composta utilizando um sistema adesivo, com uso prévio ou não