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
ALisPContext
Uma instânciac
deALisPContext
:
É 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_name
ao valorvar_value
.
c.defines(key)
testa se o nomekey
tem 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
name
se 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*args
e, 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
Python
neste 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
eval
nos argumentos:val_args = tuple(eval(x, context, ops) for x in args[:arity])
. - Devolver o valor da função associada a
op
quando 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
:print
parawrite
einput
pararead
. - 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
ALisP
sã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
x
fica 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 quex
tem 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 eVALUE
uma 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 eINSTR
um bloco de instruções.
- Note bem que a
ALisP
tem valores booleanosTrue
eFalse
mas a condição pode ser qualquer valor. Os valores que também valem comoFalse
são0
,0.0
e()
. 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_INSTR
eFALSE_INSTR
sã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
*INSTR
nã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
ALisP
o 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,string
ou 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)