Interpretador
A análise sintática (da ALisP) fica isolada na função parse(text), que devolve uma árvore com a estrutura de text de acordo com a gramática ALisP. Por exemplo, parse("(+ 2 (* 3 4))") devolve ('+', 2, ('*', 3, 4)).
O passo seguinte consistem em associar um valor (a semântica) a cada representação (à sintaxe), de forma a construir a ALisP com as caraterísticas pretendidas. Nomeadamente, pretende-se que a ALisP tenha:
- Expressões e Variáveis.
- Instruções e Sequências.
- Condicionais.
- Ciclos.
- Funções.
- Entrada/Saída.
além de, é claro, certas funções base.
Para correr da forma esperada, um programa precisa de um certo "contexto" onde é mantida e atualizada informação sobre as variáveis. Por exemplo, no programa
(
(set a 42)
(set b (- a 2))
(write (b a))
)
na primeira instrução é criada uma variável com nome a e valor 42 e na segunda instrução o valor de a é consultado, operado e o resultado define o valor da recém-criada variável b. Por fim, tanto o valor de b como de a são consultados para definir a lista (b a) que será escrita na consola.
Funções Base
As funções base definem cálculos "diretos" que dependem apenas dos valores dos argumentos e não afetam o contexto do programa.
Entre as funções base estão incluidas as operações aritméticas +, -, * e /; as relações de ordem <, >, <= e >=; de igualdade == e !=; as operações booleanas and, or e not; as funções trigonométricas sin e asin; e as funções para decompor listas head, tail e nth. A escolha das funções básicas deve corresponder às necessidades que levam ao desenvolvimento da linguagem de programação. Por exemplo, o octave é uma linguagem de manipulação de matrizes numéricas e tem pré-definidas as respetivas operações.
No âmbito da ALisP é suficiente definir as funções base como um dicionário Python onde o nome da função está associado a um tuple com a aridade e uma expressão lambda (isto é, a versão Python das funções anónimas):
import math
BASE_FUNCTIONS = {
#
# Aritméticas
#
'+': (2, lambda x, y: x + y),
'-': (2, lambda x, y: x - y),
'*': (2, lambda x, y: x * y),
'/': (2, lambda x, y: x / y),
#
# Ordem
#
'>': (2, lambda x, y: x > y),
'>=': (2, lambda x, y: x >= y),
'<': (2, lambda x, y: x < y),
'<=': (2, lambda x, y: x <= y),
#
# Igualdade
#
'==': (2, lambda x, y: x == y),
'!=': (2, lambda x, y: x != y),
#
# Booleanas
#
'not': (1, lambda x: not x),
'and': (2, lambda x, y: x and y),
'or': (2, lambda x, y: x or y),
#
# Trigonométricas
#
'sin': (1, lambda x: math.sin(x)),
'asin': (1, lambda x: math.asin(x)),
#
# Listas
#
'head': (1, lambda x: x[0]),
'tail': (1, lambda x: x[1:]),
'nth': (2, lambda n, x: x[n]),
}
Esta representação das funções base pode ser facilmente modificada de forma a proporcionar, à partida, instruções adequadas ao domínio que se pretende trabalhar. Por exemplo, para computação gráfica 2D as funções base podem incluir os controlos de uma "tartaruga", forward, turn, up, down e set_color.
Contexto
O contexto é, essencialmente, um dicionário que associa o nome de cada variável ao respetivo valor. Além disso, é necessário considerar que as funções definem variáveis locais mas também têm acesso às variáveis externas.
Em
(seq
(set x 21)
(set dobro (fn (x)
(* x 2)
))
(set x (- x 1))
(x (dobro 10))
)
a variável x é local e externa em dobro. Na última linha, (x (dobro 10)), tem o valor 20 no primeiro elemento e, quando a função dobro é evocada, é usado o valor local, 10, passado como argumento. O papel da instrução seq é esclarecido mais tarde.
A implementação do contexto é:
class ALisPContext:
def __init__(self, outer):
self.outer = outer
self.local = dict()
def defines(self, key):
return key in self.local.keys() or key in self.outer.keys()
def set(self, var_name, var_value):
self.local[var_name] = var_value
def keys(self):
return self.local.keys() | self.outer.keys()
def __getitem__(self, key):
return self.value(key)
def value(self, name):
result = None
#
if name in self.local.keys():
result = self.local[name]
elif name in self.outer.keys():
result = self.outer[name]
elif name == 'True':
return True
elif name == 'False':
return False
elif is_atomic(name): # 42, -4.2, ola
result = atom_value(name)
#
return result
ALisPContextUma instânciacdeALisPContext:
É construída em relação a um contexto "externo" (que pode ser o dicionário vazio,
{}).
c.set(var_name, var_value)define uma associação da variávelvar_nameao valorvar_value.
c.defines(key)testa se o nomekeytem uma associação, local ou externa.
c.keys()devolve o conjunto das chaves, locais ou externas, conhecidas.
c[atom]ouc.value(atom)devolve, por ordem:
- O valor associado a
namese existe essa associação a) local ou b) no contexto externo.- Se
nameé"True"ou"False"os valores booleanos correspondentes.- Se
nameé atómico (de acordo comis_atom) o respetivo valor calculado poratom_value.None.
O método interessante de ALisPContext é value que, além de obter os valores associados a uma variável local ou externa, ainda proporciona os valores dos átomos e das constantes booleanas.
Valoração
A valoração consiste em calcular o valor de uma representação, isto é, definir uma semântica da (representação) sintaxe.
Esse cálculo depende de um contexto, que mantém e atualiza os valores das variáveis.
Por exemplo, A palavra 2 + (3 * 4) tem representação ('+', 2, ('*', 3, 4)), obtida pelo analisador sintático. A esta representação corresponde o valor 14, que resulta de aplicar as respetivas operações aritméticas em cada nó, a começar pelas folhas.
Resumidamente, 14 é a semântica da sintaxe 2 + (3 * 4) e é calculada usando a representação intermédia ('+', 2, ('*', 3, 4)).
Em cada nó o conteúdo (daqui em diante representado por op) define a operação e os descendentes vão definir os valores dos argumentos (daqui em diante, args) dessa operação:
eval(('+', 2, ('*', 3, 4))) =
soma(eval(2), eval(('*', 3, 4))) =
soma(2, mult(eval(3), eval(4))) =
soma(2, mult(3, 4)) =
soma(2, 12) =
14
A valoração é uma função recursiva que, em cada nó
(op, *args), calcula o valor dos argumentos*argse, depois, conforme o valor deop, combina esses valores no resultado correspondente.Porém, um programa a correr define e consulta variáveis, escolhe ramos num condicional, etc.
O cálculo da valoração depende do contexto e das funções base. Um esquema incompleto da implementação segundo esses pressupostos será, por exemplo:
def eval(expr, context=None, ops=BASE_FUNCTIONS):
if context is None:
context = ALisPContext({})
#
# expr is ATOM
#
context_value = context.value(expr)
if context_value is not None:
return context_value
#
# expr is LIST
#
elif is_list(expr) and expr != NIL:
#
# expr = (op args...)
#
op = head(expr)
args = tail(expr)
#
# Use op and len(args) to do the right step
#
if op in ops.keys():
# BASE FUNCTIONS
elif op == 'write' and len(args) == 1:
# WRITE: (write MESSAGE)
elif op == 'read' and len(args) == 0:
# READ: (read)
elif op == 'fn' and len(args) == 2:
# FUNCTIONS: (fn ARGS INSTR)
elif op == 'set' and len(args) == 2:
# DEFINE/UPDATE VARIABLES: (set VAR VALUE)
elif op == 'while' and len(args) == 2:
# CYCLES: (while GUARD INSTR)
elif op == 'if' and len(args) == 3:
# CONDITIONALS: (if COND TRUE_INSTR FALSE_INSTR)
elif op == 'seq':
# SEQUENCE OF INSTRUCTIONS (LAST VALUE)
# (seq *INSTR)
elif is_list(op):
# DEFER: (OP *ARGS)
elif context.defines(op):
# CONSULT VARIABLE: VAR
else:
# FALLBACK: (TERMINAL *REST)
#
# expr is ALIEN... maybe raise error?
#
else:
return expr
Funções Base
As funções base proporcionam alguma funcionalidade inicial e também uma ponte à linguagem hospedeira, o
Pythonneste caso.
Para calcular o valor de uma função base como, por exemplo, (+ 2 3), fica op == "+", args == (2, 3) e os passos são:
- Verificar se
op(neste caso,+) é uma chave no dicionário das funções base (ops). - Validar a aridade (
if len(args) >= arity). - Chamar recursivamente
evalnos argumentos:val_args = tuple(eval(x, context, ops) for x in args[:arity]). - Devolver o valor da função associada a
opquando aplicada aos valores dos argumentos:return func(*val_args).
#
# BASE FUNCTIONS
#
if op in ops.keys():
arity, func = ops[op]
if len(args) >= arity:
val_args = tuple(eval(x, context, ops) for x in args[:arity])
return func(*val_args)
else:
return None
Entrada/Saída
As funções de entrada e saída permitem interações com o utilizador via uma consola.
A sintaxe é:
(write MESSAGE)em queMESSAGEé um valor qualquer a ser escrito na consola.(read)para ler um valor da consola.
- A "real" interação ocorre via as funções correspondentes no
Python:printparawriteeinputpararead. - No caso da saída (
write):- É admitido exatamente um argumento.
- Recursivamente, é calculado o valor desse argumento.
- O valor, que é um
tuple, é enviado para a consola (print) numa sintaxe compatível comALisP(expr_repr).
- Para a entrada (
read):- Não são admitidos argumentos.
- É lido o texto da consola via
input. - Esse texto é analisado (
parse). - Recursivamente, é calculado o valor da representação obtida na análise anterior.
#
# WRITE
#
elif op == 'write' and len(args) == 1:
value = eval(args[0], context, ops)
print(expr_repr(value))
return value
#
# READ
#
elif op == 'read' and len(args) == 0:
user_input = input()
user_prog = parse(user_input)
value = eval(user_prog, context, ops)
return value
Funções
As funções, em
ALisPsão valores como as listas, números ou textos.A sintaxe é
(fn ARGS INSTR)em quefné a instrução,ARGSé uma lista de variáveis eINSTRé uma lista de instruções.Depois de criada uma função pode ser guardada numa variável para uso posterior.
Por exemplo:
(
(set succ (fn (x) (+ x 1)))
(succ 41)
(succ 0)
)
As funções são o único caso em que eval não é recursiva. No exemplo acima:
- Em
(fn (x) (+ x 1))é definido um certo valor. - No interpretador esse valor é uma instância da classe
Lambda. - Essa instância é construída com os argumentos
(x,)e('+', 'x', 1)que são as representações de"(x)"e"(+ x 1)"respetivamente. - O valor do passo anterior é guardado na variável
succ. - Mais tarde, na linha seguinte,
(succ 41):- É criado um contexto local, onde
xfica com o valor41. - Nesse contexto é calculado o valor de
(+ x 1), usandoeval.
- É criado um contexto local, onde
- Na linha
(succ 0)estes passos são repetidos, mas num novo contexto local em quextem valor0.
A criação do valor Lambda ocorre nas seguintes linhas:
#
# FUNCTIONS
#
elif op == 'fn' and len(args) == 2:
fn_args = args[0]
fn_expr = args[1]
return Lambda(fn_args, fn_expr)
O cálculo das evocações é tratado mais tarde, com as variáveis.
Copiar
Um valor é copiado para uma variável que fica registada no contexto atual. Mais tarde esse valor pode ser recuperado como o valor da variável.
A sintaxe é
(set VAR VALUE)em queVARé um identificador eVALUEuma qualquer expressãoALisP.
- Por exemplo, o seguinte programa tem valor
42:
(seq
(set bad-answer 20)
(set almost-good-answer (* 2 bad-answer))
(+ almost-good-answer 2)
)
No lado do interpretador as instruções set atualizam o contexto da seguinte forma:
#
# DEFINE/UPDATE VARIABLES
#
elif op == 'set' and len(args) == 2:
var_name = uq(args[0]) # Inelegant "dequotation" :(
var_value = eval(args[1], context, ops)
context.set(var_name, var_value)
return var_value
Ciclos
Um ciclo repete um bloco de instruções enquanto uma certa condição é verdadeira.
A sintaxe é
(while GUARD INSTR)em queGUARDé uma condição eINSTRum bloco de instruções.
- Note bem que a
ALisPtem valores booleanosTrueeFalsemas a condição pode ser qualquer valor. Os valores que também valem comoFalsesão0,0.0e(). Todos os restantes valores valem comoTrue. - Por exemplo, o ciclo seguinte escreve a "tabela" dos quadrados de 0 a 10:
(
(set i 0)
(while (<= i 10) (
(write (i (* i i)))
(set i (+ i 1))
))
)
A implementação dos ciclos ALisP usa os ciclos do Python:
#
# CYCLES
#
elif op == 'while' and len(args) == 2:
guard = args[0]
statements = args[1]
last = NIL
while eval(guard, context, ops):
last = eval(statements, context, ops)
return last
Condicionais
Um condicional corre um certo bloco de instruções, ou outro, conforme uma condição é verdadeira ou falsa.
A sintaxe é
(if COND TRUE_INSTR FALSE_INSTR)em queCONDé uma condição eTRUE_INSTReFALSE_INSTRsão blocos de instruções.
- Note bem que a sintaxe "força" três argumentos.
- Por exemplo, o valor de
(seq (set a 10) (if (> a 0) positivo negativo))épositivo.
Tal como nos ciclos, os condicionais do ALisP usam os do Python:
#
# CONDITIONALS
#
elif op == 'if' and len(args) == 3:
cond, seq_true, seq_false = args
if eval(cond, context, ops):
return eval(seq_true, context, ops)
else:
return eval(seq_false, context, ops)
Com esta implementação apenas é percorrido, e calculado o valor do bloco que corresponde ao valor da condição.
Sequências
Uma sequência de instruções corre cada uma, por ordem, e devolve o valor da última instrução. A sequência vazia,
(seq)vale a lista vazia,(), designada NIL.A sintaxe é
(seq *INSTR)em que*INSTRé uma sequência de instruções.
- Note bem que
*INSTRnão é uma lista, mas os seus elementos. - Por exemplo:
- O valor de
(seq 0 1 2 3)é3. - O valor de
(seq (0 1 2 3))é(0 1 2 3). - O valor de
(seq (set a 40) (+ a 2))é42.
- O valor de
A implementação do valor para as sequências é:
#
# SEQUENCE OF INSTRUCTIONS (RETURNS ONLY THE LAST VALUE)
#
elif op == 'seq':
if len(args) == 0:
return NIL
else:
seq = eval(args, context, ops)
return seq[-1]
Diferimento
Na
ALisPo primeiro elemento de uma lista determina como é calculado o valor dessa lista. Pode-se evocar uma função, executar uma instrução ou apenas definir a lista dos valores.Quando o primeiro elemento é também uma lista é preciso começar por determinar o seu valor para seguir no cálculo do valor da lista parente.
Isto é, cálculo do valor da lista parente é diferido pelo valor da lista que é o primeiro elemento.
Por exemplo, o valor de:
(1 2 3)é(1 2 3).(+ 2 3)é5.(if True 42 24)é42.((1 2 3) 2 3)é((1 2 3) 2 3).((if True + *) 2 3)é5.((if False + *) 2 3)é6.
Nos três últimos exemplos, 4., 5. e 6., o primeiro elemento da lista é uma lista que vai definir o modo de calcular o valor da lista parente.
No caso
((1 2 3) 2 3)o valor do primeiro elemento é(1 2 3)que não é nem uma função nem uma instrução. Tal como no exemplo 1., a lista parente tem apenas valores.Nos exemplos 5. e 6. o valor do primeiro elemento é, respetivamente,
+e*. Portanto, o valor da lista parente vai ser diferido, respetivamente, para(+ 2 3)e(* 2 3).
A implementação do cálculo diferido é:
#
# LIST OF INSTRUCTIONS (RETURNS ALL THE VALUES)
#
elif is_list(op):
op_val = eval(op, context, ops)
args_vals = tuple([eval(xi, context, ops) for xi in args])
expr2 = (op_val,) + args_vals
if op_val == op:
return expr2
else:
return eval(expr2, context, ops)
O teste if op_val == op deteta se o cálculo (não) vai ser diferido.
Variáveis e Evocação de Funções
Uma variável pode referir qualquer valor
ALisP, guardado no contexto atual.Quando esse valor é um tipo básico, inteiro,
double, lista,stringou booleano, basta consultar o respetivo valor, guardado no contexto.Quando a variável refere uma função é preciso calcular o valor da função com os argumentos dados.
Por exemplo, o valor de:
(seq (set a 2) (+ a 40)é42.(seq ((set a +)) (a 2 40)é42. O duplo((set a +))é um hack para isolar o valor de(set a +), que é+, uma operação.(seq (set a (fn (x y) (+ x y))) (a 2 40))é42. Esse valor resulta de aplicar a função(fn (x y) (+ x y))aos argumentos(2 40). O corpo e os argumentos desta função forma guardados no contexto, com nomea.
A implementação:
#
# CONSULT VARIABLE
#
elif context.defines(op):
val = eval(op, context, ops)
#
# Function evocation
#
if isinstance(val, Lambda):
arity = len(val.args)
if arity <= len(args):
local_context = AlispContext(context)
for i in range(arity):
local_context.set(
val.args[i],
eval(args[i], context, ops))
return eval(val.expr, local_context, ops)
else:
return None
#
# Other types (number, text, list...)
#
else:
expr2 = (val, ) + args
return eval(expr2, context, ops)
Fallback
Se a "operação" não é um dos casos anteriores deve ser um terminal.
Por exemplo, (4 (+ 1 1)). Neste caso o resultado será (4 2).
A implementação:
#
# FALLBACK
#
else:
return (op,) + eval(args, context, ops)