Variáveis • Capítulo 11

Imutabilidade


turtle

Tartaruga Ninja • Não Imutável.

Nos capítulos anteriores fizemos progresso considerável na infraestrutura da nossa linguagem.

Já podemos fazer algumas coisas legais que outras linguagens não fazem, como colocar código dentro de listas. Agora é a hora de começar a adicionar os recursos que tornarão nossa linguagem prática. O primeiro deles vai ser variáveis.

Elas são chamadas variáveis, mas é um nome enganoso, porque nossas variáveis não irão variar. Nossas variáveis são imutáveis, isto é, elas não podem mudar. Tudo em nossa linguagem até agora agiu como se fosse imutável. Quando avaliamos uma expressão, imaginamos que a coisa anterior foi deletada e uma coisa nova foi devolvida. Na implementação, frequentemente é mais fácil reutilizarmos os dados da coisa anterior para construir a próxima coisa, mas conceitualmente é uma boa maneira de pensar a respeito de como nossa linguagem funciona.

Então, na verdade nossas variáveis são meramente uma maneira de nomear valores. Elas nos permitem atribuir um nome para um valor, e então nos deixa obter uma cópia daquele valor mais tarde quando precisarmos.

Para permitir nomear valores, precisamos criar uma estrutura que armazene o nome e valor de tudo que for nomeado em nosso programa. Chamamos isto de ambiente. Quando começamos um novo prompt interativo, queremos criar um novo ambiente para funcionar com ele, em que cada novo trecho da entrada é avaliado. A seguir podemos armazenar e relembrar variáveis enquanto programamos.

O que acontece se reatribuirmos um nome a algo novo? Isto não é o mesmo que mutabilidade?

No nosso Lisp, quando reatribuímos um nome vamos deletar a associação anterior e criar uma nova. Isto dá a ilusão que a coisa atribuída àquele nome mudou e é mutável, mas de fato deletamos a coisa velha e atribuímos uma coisa nova. Isto é diferente de C onde podemos realmente mudar o dado apontado por um apontador ou armazenado em uma struct, sem deletá-lo e criar um novo.

Sintaxe de Símbolo


Agora que vamos permitir nosso usuário definir variáveis, precisamos que a gramática para símbolos seja mais flexível. Em vez de apenas nossas funções builtin, ela deve casar com qualquer símbolo possível válido. Diferentemente de C, onde o nome que pode ser dado a uma variável é relativamente restringido, vamos permitir vários tipos de caracteres no nome de uma variável.

Podemos criar uma expressão regular que expressa a faixa de caracteres disponível como segue.

/[a-zA-Z0-9_+\\-*\\/\\\\=<>!&]+/

À primeira vista, parece que simplesmente batemos com nossas mãos no teclado. Na verdade, é uma expressão regular usando um especificador de intervalo []. Dentro do especificador de intervalo caracteres especiais perdem seu sentido especial, mas alguns destes caracteres ainda precisam ser escapados com barras invertidas /. Como isto é uma parte duma string C, precisamos colocar duas barras invertidas para representar um único caractere barra invertida na entrada.

Esta regra permite símbolos terem qualquer um entre os caracteres normais de identificadores C a-zA-Z0-9_, os caracteres de operação aritmética +\\-*\\/, o caractere barra invertida \\\\, os caracteres do operador de comparação =<>! ou um ampersand &. Isto nos dará toda a flexibilidade que precisamos para definir símbolos novos e existentes.

mpca_lang(MPCA_LANG_DEFAULT,
  "                                                     \
    number : /-?[0-9]+/ ;                               \
    symbol : /[a-zA-Z0-9_+\\-*\\/\\\\=<>!&]+/ ;         \
    sexpr  : '(' <expr>* ')' ;                          \
    qexpr  : '{' <expr>* '}' ;                          \
    expr   : <number> | <symbol> | <sexpr> | <qexpr> ;  \
    lispy  : /^/ <expr>* /$/ ;                          \
  ",
  Number, Symbol, Sexpr, Qexpr, Expr, Lispy);

Apontador para função


Tendo introduzido variáveis, símbolos não mais representarão funções em nossa linguagem, mas um nome para buscarmos em nosso ambiente e obter um valor de volta.

Portanto, precisamos um novo valor para representar funções em nossa linguagem, que possamos devolver quando um dos nossos símbolos builtins seja encontrado. Para criar este novo tipo de lval vamos usar algo chamado um apontador para função (ou, ponteiro para função).

Apontadores para funções são um grande recurso de C que permite armazenar e repassar apontadores para funções. Não faz sentido alterar o dado apontado por estes apontadores. Em vez disso, usamo-los para chamar a função para a qual apontam, como se fossem uma função normal.

Como apontadores normais, apontadores para funções têm algum tipo associado à eles. Este tipo especifica o tipo da função sendo apontada, não o tipo de dado que ele aponta. Isto permite o compilador identificar se foi chamado corretamente.

No capítulo anterior, nossas funções builtins receberam um lval* como entrada e devolveram um lval* como saída. Neste capítulo nossas funções builtins receberão um apontador extra para o ambiente lenv* como entrada. Podemos declarar a nova função um novo tipo apontador de função chamado lbuiltin, para este tipo de função, da seguinte forma:

typedef lval*(*lbuiltin)(lenv*, lval*);

Por que esta sintaxe tão estranha?

Algumas vezes a sintaxe de C pode parecer particularmente estranha. Pode ajudar se entendermos exatamente por que a sintaxe é dessa forma. Vamos desconstruir a sintaxe no exemplo acima parte por parte.

Primeiro o typedef. Ele pode ser colocado antes de qualquer declaração de variável normal. Ele resulta no nome da variável, sendo declarado como um novo tipo, correspondendo ao que seria o tipo inferido para aquela variável. Por isso, na declaração acima, o que parece um nome de função se torna o nome do novo tipo.

A seguir, todos aqueles *. Tipos apontadores em C foram concebidos para serem escritos com o asterisco * ao lado esquerdo do nome da variável, não ao lado direito do tipo int *x;. Isto é porque a sintaxe de tipos de C funciona com um esquema de inferência. Em vez de ler "Crie um novo apontador int apontador x", é feita para ler "Crie uma nova variável x onde dereferenciar x resulta em um int". Portanto, x é inferido como sendo um apontador para int.

Esta ideia é expandida em apontadores para função. Podemos ler a declaração acima como: "Para obter um lval*, dereferenciamos lbuiltin e a chamamos com um lenv* e um lval*." Portanto, lbuiltin precisa ser um apontador para função que recebe um lenv* e um lval* e devolve um lval*.

Tipos Cíclicos


O tipo lbuiltin referencia o tipo lval e o tipo lenv. Isto significa que estes devem ser declarados antes no código fonte.

Mas queremos fazer um campo lbuiltin em nossa struct lval para que possamos criar valores funções. Então, nossa declaração lbuiltin deve ir antes da nossa declaração lval. Isto leva ao que chamamos uma dependência cíclica de tipos, onde dois tipos dependem um do outro.

Nos deparamos com este problema antes com funções que dependem uma da outra. A solução foi criar uma declaração adiantada que declarava uma função mas deixava o corpo dela vazio.

Em C podemos fazer exatamente a mesma coisa com tipos. Primeiro, declaramos os dois tipos struct sem um corpo. Segundo, definimos com typedef os nomes lval e lenv. Em seguida podemos definir nosso tipo apontador para função lbuiltin, e finalmente podemos definir o corpo da nossa estrutura lval. Agora todos nossos problemas com tipos estão resolvidos e o compilador não vai reclamar mais.

/* Declaracoes Adiantadas */

struct lval;
struct lenv;
typedef struct lval lval;
typedef struct lenv lenv;

/* Valor Lisp */

enum { LVAL_ERR, LVAL_NUM,   LVAL_SYM, 
       LVAL_FUN, LVAL_SEXPR, LVAL_QEXPR };

typedef lval*(*lbuiltin)(lenv*, lval*);

struct lval {
  int type;

  long num;
  char* err;
  char* sym;
  lbuiltin fun;

  int count;
  lval** cell;
};

Tipo Função


Como adicionamos um novo tipo possível lval com a enumeração LVAL_FUN. Devemos atualizar todas nossas funções relevantes que funcionam em lvals para lidar corretamente com esta atualização. Na maioria dos casos isto significa apenas inserir novos casos em comandos switch.

Podemos começar fazendo uma nova função construtora para este tipo.

lval* lval_fun(lbuiltin func) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_FUN;
  v->fun = func;
  return v;
}

Na deleção não precisamos fazer nada especial para apontadores de função.

case LVAL_FUN: break;

Na impressão podemos simplesmente imprimir uma string nominal.

case LVAL_FUN:   printf("<function>"); break;

Também vamos adicionar uma nova função para copiar um lval. Isto vai ser útil quando colocarmos e tirarmos coisas dentro do ambiente. Para números e funções podemos simplesmente copiar diretamente os campos relevantes. Para strings, precisamos copiar usando malloc e strcpy. Para copiar listas, precisamos alocar a quantidade correta de espaço e a seguir copiar cada elemento individualmente.

lval* lval_copy(lval* v) {
  
  lval* x = malloc(sizeof(lval));
  x->type = v->type;
  
  switch (v->type) {
    
    /* Copia funcoes e numeros diretamente */
    case LVAL_FUN: x->fun = v->fun; break;
    case LVAL_NUM: x->num = v->num; break;
    
    /* Copia strings usando malloc e strcpy */
    case LVAL_ERR:
      x->err = malloc(strlen(v->err) + 1);
      strcpy(x->err, v->err); break;
    
    case LVAL_SYM:
      x->sym = malloc(strlen(v->sym) + 1);
      strcpy(x->sym, v->sym); break;

    /* Copia listas copiando cada sub-expressao */
    case LVAL_SEXPR:
    case LVAL_QEXPR:
      x->count = v->count;
      x->cell = malloc(sizeof(lval*) * x->count);
      for (int i = 0; i < x->count; i++) {
        x->cell[i] = lval_copy(v->cell[i]);
      }
    break;
  }
  
  return x;
}

Ambiente


Nossa estrutura de ambiente precisa codificar uma lista de relacionamentos entre nomes e valores. Existem muitas maneiras de construir uma estrutura que pode fazer este tipo de coisa. Vamos usar o método mais simples possível que funciona bem. Isto é, vamos usar duas listas do mesmo tamanho. Uma é uma lista de lval*, e a outra é uma lista de char*. Cada entrada em uma lista tem um valor correspondente na outra lista na mesma posição.

Já declaramos adiantado nossa estrutura lenv, então agora podemos defini-la como segue.

struct lenv {
  int count;
  char** syms;
  lval** vals;
};

Precisamos de algumas funções para criar e deletar esta estrutura. Elas são bem simples. A criação inicializa os campos do struct, enquanto a deleção itera pelos itens em ambas listas e deleta ou libera eles.

lenv* lenv_new(void) {
  lenv* e = malloc(sizeof(lenv));
  e->count = 0;
  e->syms = NULL;
  e->vals = NULL;
  return e;
}
void lenv_del(lenv* e) {
  for (int i = 0; i < e->count; i++) {
    free(e->syms[i]);
    lval_del(e->vals[i]);
  }
  free(e->syms);
  free(e->vals);
  free(e);
}

A seguir podemos criar duas funções que ou obtém valores do ambiente ou colocam valores nele.

Para obter um valor do ambiente, percorremos todos os itens no ambiente e checamos se o símbolo dado casa qualquer uma das strings armazenadas. Se achamos alguma que corresponde, podemos devolver uma cópia do valor armazenado. Se nenhum valor é encontrado, devemos retornar um erro.

A função para colocar novas variáveis no ambiente é um pouco mais complexa. Primeiro queremos checar se uma variável com o mesmo nome já existe. Se este for o caso, devemos substituí-la com a nova. Para fazer isso, iteramos todas as variáveis existentes no ambiente e checamos seus nomes. Se achamos uma correspondência, deletamos o valor armazenado naquela localização e armazenamos lá uma cópia do valor da entrada.

Se nenhum valor existente foi encontrado com esse nome, precisamos alocar mais espaço para colocá-lo. Para isso, podemos usar realloc e armazenar uma cópia do lval e seu nome nas localizações recém alocadas.

lval* lenv_get(lenv* e, lval* k) {

  /* Itera por todos os items no ambiente */
  for (int i = 0; i < e->count; i++) {
    /* Verifica que a string armazenada casa com a string do simbolo */
    /* Se casar, retorna uma copia do valor do mesmo */
    if (strcmp(e->syms[i], k->sym) == 0) {
      return lval_copy(e->vals[i]);
    }
  }
  /* Caso nenhum simbolo for encontrado, retorna erro */
  return lval_err("unbound symbol!");
}
void lenv_put(lenv* e, lval* k, lval* v) {

  /* Itera por todos os items no ambiente */
  /* Isto eh para ver se a variavel ja existe*/
  for (int i = 0; i < e->count; i++) {

    /* Se a variavel for encontrada, deleta o item naquela posicao */
    /* E substitui com a variavel fornecida pelo usuario */
    if (strcmp(e->syms[i], k->sym) == 0) {
      lval_del(e->vals[i]);
      e->vals[i] = lval_copy(v);
      return;
    }
  }

  /* Se nenhum registro for encontrado, aloca espaco para um novo */
  e->count++;
  e->vals = realloc(e->vals, sizeof(lval*) * e->count);
  e->syms = realloc(e->syms, sizeof(char*) * e->count);

  /* Copia o conteudo do lval e a string do simbolo para o novo local */
  e->vals[e->count-1] = lval_copy(v);
  e->syms[e->count-1] = malloc(strlen(k->sym)+1);
  strcpy(e->syms[e->count-1], k->sym);
}

Avaliação de Variáveis


Nossa função de avaliação agora depende de algum ambiente. Devemos passar este como um argumento e usá-lo para obter um valor se encontramos um tipo símbolo. Como nosso ambiente devolve uma cópia do valor, precisamos lembrar de deletar o símbolo de entrada lval.

lval* lval_eval(lenv* e, lval* v) {
  if (v->type == LVAL_SYM) {
    lval* x = lenv_get(e, v);
    lval_del(v);
    return x;
  }
  if (v->type == LVAL_SEXPR) { return lval_eval_sexpr(e, v); }
  return v;
}

Como adicionamos um tipo função, nossa avaliação de S-Expressions também precisa mudar. Em vez de checar por um tipo símbolo, queremos nos certificar que é um tipo função. Se esta condição for verdadeira, podemos chamar o campo fun do lval usando a mesma notação que as chamadas de funções normais.

lval* lval_eval_sexpr(lenv* e, lval* v) {

  for (int i = 0; i < v->count; i++) {
    v->cell[i] = lval_eval(e, v->cell[i]);
  }
  
  for (int i = 0; i < v->count; i++) {
    if (v->cell[i]->type == LVAL_ERR) { return lval_take(v, i); }
  }

  if (v->count == 0) { return v; }  
  if (v->count == 1) { return lval_take(v, 0); }

  /* Certifica-se que primeiro elemento eh uma funcao depois de avaliar */
  lval* f = lval_pop(v, 0);
  if (f->type != LVAL_FUN) {
    lval_del(v); lval_del(f);
    return lval_err("first element is not a function");
  }

  /* Caso sim, chama a funcao para obter o resultado */
  lval* result = f->fun(e, v);
  lval_del(f);
  return result;
}

Builtins


Agora que nossa avaliação usa o novo tipo função, precisamos nos assegurar que podemos registrar todas nossas funções builtins com o ambiente antes de começar o prompt interativo. No momento, nossas funções builtins não são do tipo correto. Precisamos mudar sua assinatura de tipo para que elas recebam um ambiente, e mudá-las onde apropriado para repassar esse ambiente para outras chamadas que o necessitem. Não vou postar o código aqui para isso, então vá em frente e mude as assinaturas de tipo das funções builtins para receber um lenv* como seu primeiro argumento agora. Caso estiver confuso, você pode olhar o código de exemplo para este capítulo.

Como exemplo, podemos usar nossa função builtin_op para definir builtins separados para cada uma das funções matemáticas que nossa linguagem suporta.

lval* builtin_add(lenv* e, lval* a) {
  return builtin_op(e, a, "+");
}

lval* builtin_sub(lenv* e, lval* a) {
  return builtin_op(e, a, "-");
}

lval* builtin_mul(lenv* e, lval* a) {
  return builtin_op(e, a, "*");
}

lval* builtin_div(lenv* e, lval* a) {
  return builtin_op(e, a, "/");
}

Tendo mudado as builtins para o tipo correto, podemos criar uma função que registra todas nossas builtins em um ambiente.

Para cada builtin, queremos criar uma função lval e um símbolo lval com o nome dado. A seguir, registramos estes com o ambiente usando lenv_put. O ambiente sempre recebe ou retorna cópias dos valores, então precisamos lembrar de deletar estes dois lvals depois de registrar, já que não vamos usá-los mais.

Se quebrarmos esta tarefa em duas funções, podemos elegantemente registrar todos nossos builtins com algum ambiente.

void lenv_add_builtin(lenv* e, char* name, lbuiltin func) {
  lval* k = lval_sym(name);
  lval* v = lval_fun(func);
  lenv_put(e, k, v);
  lval_del(k); lval_del(v);
}

void lenv_add_builtins(lenv* e) {  
  /* Funcoes de listas */
  lenv_add_builtin(e, "list", builtin_list);
  lenv_add_builtin(e, "head", builtin_head);
  lenv_add_builtin(e, "tail", builtin_tail);
  lenv_add_builtin(e, "eval", builtin_eval);
  lenv_add_builtin(e, "join", builtin_join);

  /* Funcoes matematicas */
  lenv_add_builtin(e, "+", builtin_add);
  lenv_add_builtin(e, "-", builtin_sub);
  lenv_add_builtin(e, "*", builtin_mul);
  lenv_add_builtin(e, "/", builtin_div);
}

O último passo é chamar esta função antes de criar o prompt interativo. Também precisamos lembrar de deletar o ambiente quando terminarmos.

lenv* e = lenv_new();
lenv_add_builtins(e);

while (1) {

  char* input = readline("lispy> ");
  add_history(input);
  
  mpc_result_t r;
  if (mpc_parse("<stdin>", input, Lispy, &r)) {
    
    lval* x = lval_eval(e, lval_read(r.output));
    lval_println(x);
    lval_del(x);
    
    mpc_ast_delete(r.output);
  } else {    
    mpc_err_print(r.error);
    mpc_err_delete(r.error);
  }
  
  free(input);
  
}
  
lenv_del(e);

Se tudo estiver funcionando corretamente, podemos brincar um pouco no prompt e verificar se as funções são realmente um novo tipo de valor agora, e não símbolos.

lispy> +
<function>
lispy> eval (head {5 10 11 15})
5
lispy> eval (head {+ - + - * /})
<function>
lispy> (eval (head {+ - + - * /})) 10 20
30
lispy> hello
Error: unbound symbol!
lispy>

Função Define


Conseguimos registrar nossos builtins como variáveis, mas ainda não temos uma maneira de nossos usuários definir suas próprias variáveis.

Isto é na verdade um pouco estranho. Precisamos fazer o usuário passar um símbolo para nomear, e também o valor para atribuir a ele. Mas símbolos não aparecem sozinhos. Caso contrário, a função de avaliação tentará recuperar um valor para eles do ambiente.

A única maneira de passarmos símbolos sem que eles sejam avaliados é colocá-los entre {} em uma Q-expression (expressão citada). Então vamos usar esta técnica para nossa função define. Ela receberá como entrada uma lista de símbolos e alguns outros valores, e atribuirá cada um destes valores a cada um dos símbolos.

Esta função deverá agir como qualquer outro builtin. Primeiro, verifica condições de erro, e em seguida executa algum comando e retorna um valor. Neste caso, ela primeiramente checa que os argumentos da entrada são dos tipos corretos. A seguir, itera sobre cada símbolo e valor e os coloca no ambiente. Se há algum erro, podemos devolvê-lo, mas em caso de sucesso, devolvemos a expressão vazia ().

lval* builtin_def(lenv* e, lval* a) {
  LASSERT(a, a->cell[0]->type == LVAL_QEXPR,
    "Function 'def' passed incorrect type!");

  /* O primeiro argumento eh uma lista de simbolos */
  lval* syms = a->cell[0];

  /* Verifica se todos elementos da primeira lista sao simbolos */
  for (int i = 0; i < syms->count; i++) {
    LASSERT(a, syms->cell[i]->type == LVAL_SYM,
      "Function 'def' cannot define non-symbol");
  }

  /* Verifica quantidade correta de simbolos e valores */
  LASSERT(a, syms->count == a->count-1,
    "Function 'def' cannot define incorrect "
    "number of values to symbols");

  /* Atribui copias dos valores para simbolos */
  for (int i = 0; i < syms->count; i++) {
    lenv_put(e, syms->cell[i], a->cell[i+1]);
  }

  lval_del(a);
  return lval_sexpr();
}

Precisamos registrar este novo builtin usando nossa função builtin lenv_add_builtin.

/* Variable Functions */
lenv_add_builtin(e, "def",  builtin_def);

Agora devemos conseguir suportar variáveis definidas pelo usuário. Como nossa função def recebe uma lista de símbolos, podemos fazer algumas coisas legais armazenando e manipulando símbolos em listas antes de passá-los para serem definidos. Brinque um pouco no prompt e verifique que tudo esteja funcionando corretamente. Você deve obter um comportamento como o mostrado a seguir. Explore quais outros métodos complexos são possíveis para a definição e avaliação de variáveis. Conseguindo definir funções, vamos ver de verdade algumas das coisas úteis que podem ser feitas com essa abordagem.

lispy> def {x} 100
()
lispy> def {y} 200
()
lispy> x
100
lispy> y
200
lispy> + x y
300
lispy> def {a b} 5 6
()
lispy> + a b
11
lispy> def {arglist} {a b x y}
()
lispy> arglist
{a b x y}
lispy> def arglist 1 2 3 4
()
lispy> list a b x y
{1 2 3 4}
lispy>

Comunicando erros


Até agora, nossa comunicação de erros não funciona muito bem. Podemos comunicar quando um erro ocorre, e dar uma vaga noção do que o problema foi, mas não damos ao usuário muita informação sobre o que exatamente deu errado. Por exemplo, se há um símbolo não vinculado nós deveríamos ser capaz de reportar exatamente qual símbolo está não vinculado. Isto pode ajudar o usuário a encontrar equívocos, erros de digitação e outros problemas triviais.

eclipses

Eclipses • Igual ellipsis (reticências, em inglês).

Não seria legal se pudéssemos escrever uma função que pode reportar erros de maneira semelhante a como printf funciona? Seria ideal se pudéssemos passar strings, inteiros, e outros dados para tornar nossas mensagens de erros mais ricas.

A função printf é uma função especial em C porque ela recebe um número variável de argumentos. Podemos criar nossas próprias funções com argumentos variáveis, que é o que vamos fazer para tornar melhor nossa comunicação de erros.

Vamos modificar lval_err para agir da mesma maneira como printf, recebendo uma string de formato, e depois alguns argumentos para casar com essa string.

Para declarar que uma função recebe argumentos variáveis na assinatura de tipo, você usa a sintaxe especial de ellipsis (ou, reticências) ..., que representa o restante dos argumentos.

lval* lval_err(char* fmt, ...);

A seguir, dentro da função existem funções da biblioteca padrão que podemos usar para examinar o que o código chamador passou.

O primeiro passo é criar um struct va_list e inicializá-lo usando va_start, passando o último argumento nomeado. Para outros propósitos, é possível examinar cada argumento passado usando va_arg, mas nós vamos passar nossa lista inteira de argumentos variáveis diretamente para a função vsnprintf. Esta função funciona como printf, mas escreve em uma string e recebe um va_list. Tendo feito isso, devemos chamar va_end para liberar quaisquer recursos usados.

A função vsnprintf joga a saída em uma string, que precisamos alocar antes. Como não sabemos o tamanho desta string até rodarmos a função, primeiro vamos alocar um buffer de caracteres com tamanho 512 e então realocar em um buffer menor depois que jogarmos a saída para ele. Se uma mensagem de erro for maior que 512 caracteres vai acabar cortada, mas com sorte isso não vai acontecer.

Colocando tudo junto, nossa nova função de erros se parece com isso.

lval* lval_err(char* fmt, ...) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_ERR;

  /* Cria um va list e inicializa */
  va_list va;
  va_start(va, fmt);

  /* Aloca 512 bytes de espaco */
  v->err = malloc(512);

  /* imprime a string de erro com um maximo de 511 caracteres */
  vsnprintf(v->err, 511, fmt, va);

  /* Realoca para o numero de bytes realmente usado */
  v->err = realloc(v->err, strlen(v->err)+1);

  /* Limpa nossa va list */
  va_end(va);

  return v;
}

Usando isso podemos começar a adicionar mensagens de erros melhores em nossas funções. Como exemplo, podemos olhar para lenv_get. Quando um símbolo não pode ser encontrado, em vez de comunicar um erro genérico, podemos realmente dizer o nome do que não foi encontrado.

return lval_err("Unbound Symbol '%s'", k->sym);

Podemos também adaptar nossa macro LASSERT de maneira que possa receber argumentos variáveis também. Como ela é uma macro e não uma função padrão, a sintaxe é um pouco diferente. No lado esquerdo da definição nós usamos a notação de ellipsis novamente, mas no lado direito usamos uma variável especial __VA_ARGS__ para colar o conteúdo de todos os outros argumentos.

Precisamos prefixar esta variável especial com dois sinais hash (jogo-da-velha) ##. Isto se certifica que ela é colada corretamente quando a macro não recebe nenhum argumento extra. Basicamente, o que isso faz é remover a vírgula do começo , para fazer de conta que nenhum argumento extra foi passado.

Como talvez possamos usar args na construção da mensagem de erro, precisamos nos certificar que não o deletemos até que tenhamos criado o valor de erro.

#define LASSERT(args, cond, fmt, ...) \
  if (!(cond)) { \
    lval* err = lval_err(fmt, ##__VA_ARGS__); \
    lval_del(args); \
    return err; \
  }

Agora podemos atualizar algumas das nossas mensagens de erro para torná-las mais informativas. Por exemplo, se foi passado um número incorreto de argumentos, podemos especificar quantos eram esperados e quantos foram dados.

LASSERT(a, a->count == 1,
  "Function 'head' passed too many arguments. "
  "Got %i, Expected %i.",
  a->count, 1);

Podemos também melhorar nossa comunicação para erros de tipo. Devemos tentar comunicar qual tipo era esperado por uma função e qual tipo realmente recebeu. Antes de fazer isso, seria útil ter uma função que recebesse como entrada uma enumeração de tipo e devolvesse uma representação string do tipo dado.

char* ltype_name(int t) {
  switch(t) {
    case LVAL_FUN: return "Function";
    case LVAL_NUM: return "Number";
    case LVAL_ERR: return "Error";
    case LVAL_SYM: return "Symbol";
    case LVAL_SEXPR: return "S-Expression";
    case LVAL_QEXPR: return "Q-Expression";
    default: return "Unknown";
  }
}
LASSERT(a, a->cell[0]->type == LVAL_QEXPR, 
  "Function 'head' passed incorrect type for argument 0. "
  "Got %s, Expected %s.",
  ltype_name(a->cell[0]->type), ltype_name(LVAL_QEXPR));

Vá em frente e use LASSERT para comunicar erros com mais profundidade no código. Isto vai tornar mais fácil a depuração de muitos dos próximos estágios, à medida que comecemos a escrever códigos complicados usando nossa nova linguagem. Veja se você consegue usar macros para poupar digitação e gerar código automaticamente para maneiras comuns de relatar erros.

lispy> + 1 {5 6 7}
Error: Function '+' passed incorrect type for argument 1. Got Q-Expression, Expected Number.
lispy> head {1 2 3} {4 5 6}
Error: Function 'head' passed incorrect number of arguments. Got 2, Expected 1.
lispy>

Referência


#include "mpc.h"

#ifdef _WIN32

static char buffer[2048];

char* readline(char* prompt) {
  fputs(prompt, stdout);
  fgets(buffer, 2048, stdin);
  char* cpy = malloc(strlen(buffer)+1);
  strcpy(cpy, buffer);
  cpy[strlen(cpy)-1] = '\0';
  return cpy;
}

void add_history(char* unused) {}

#else
#include <editline/readline.h>
#include <editline/history.h>
#endif

/* Forward Declarations */

struct lval;
struct lenv;
typedef struct lval lval;
typedef struct lenv lenv;

/* Lisp Value */

enum { LVAL_ERR, LVAL_NUM,   LVAL_SYM, 
       LVAL_FUN, LVAL_SEXPR, LVAL_QEXPR };

typedef lval*(*lbuiltin)(lenv*, lval*);

struct lval {
  int type;
  long num;
  char* err;
  char* sym;
  lbuiltin fun;
  int count;
  lval** cell;
};

lval* lval_num(long x) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_NUM;
  v->num = x;
  return v;
}

lval* lval_err(char* fmt, ...) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_ERR;
  
  /* Create a va list and initialize it */
  va_list va;
  va_start(va, fmt);
  
  /* Allocate 512 bytes of space */
  v->err = malloc(512);
  
  /* printf the error string with a maximum of 511 characters */
  vsnprintf(v->err, 511, fmt, va);
  
  /* Reallocate to number of bytes actually used */
  v->err = realloc(v->err, strlen(v->err)+1);
  
  /* Cleanup our va list */
  va_end(va);
  
  return v;
}

lval* lval_sym(char* s) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_SYM;
  v->sym = malloc(strlen(s) + 1);
  strcpy(v->sym, s);
  return v;
}

lval* lval_fun(lbuiltin func) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_FUN;
  v->fun = func;
  return v;
}

lval* lval_sexpr(void) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_SEXPR;
  v->count = 0;
  v->cell = NULL;
  return v;
}

lval* lval_qexpr(void) {
  lval* v = malloc(sizeof(lval));
  v->type = LVAL_QEXPR;
  v->count = 0;
  v->cell = NULL;
  return v;
}

void lval_del(lval* v) {

  switch (v->type) {
    case LVAL_NUM: break;
    case LVAL_FUN: break;
    case LVAL_ERR: free(v->err); break;
    case LVAL_SYM: free(v->sym); break;
    case LVAL_QEXPR:
    case LVAL_SEXPR:
      for (int i = 0; i < v->count; i++) {
        lval_del(v->cell[i]);
      }
      free(v->cell);
    break;
  }
  
  free(v);
}

lval* lval_copy(lval* v) {

  lval* x = malloc(sizeof(lval));
  x->type = v->type;
  
  switch (v->type) {
    
    /* Copy Functions and Numbers Directly */
    case LVAL_FUN: x->fun = v->fun; break;
    case LVAL_NUM: x->num = v->num; break;
    
    /* Copy Strings using malloc and strcpy */
    case LVAL_ERR:
      x->err = malloc(strlen(v->err) + 1);
      strcpy(x->err, v->err); break;
      
    case LVAL_SYM:
      x->sym = malloc(strlen(v->sym) + 1);
      strcpy(x->sym, v->sym); break;
    
    /* Copy Lists by copying each sub-expression */
    case LVAL_SEXPR:
    case LVAL_QEXPR:
      x->count = v->count;
      x->cell = malloc(sizeof(lval*) * x->count);
      for (int i = 0; i < x->count; i++) {
        x->cell[i] = lval_copy(v->cell[i]);
      }
    break;
  }
  
  return x;
}

lval* lval_add(lval* v, lval* x) {
  v->count++;
  v->cell = realloc(v->cell, sizeof(lval*) * v->count);
  v->cell[v->count-1] = x;
  return v;
}

lval* lval_join(lval* x, lval* y) {  
  for (int i = 0; i < y->count; i++) {
    x = lval_add(x, y->cell[i]);
  }
  free(y->cell);
  free(y);  
  return x;
}

lval* lval_pop(lval* v, int i) {
  lval* x = v->cell[i];  
  memmove(&v->cell[i], &v->cell[i+1],
    sizeof(lval*) * (v->count-i-1));  
  v->count--;  
  v->cell = realloc(v->cell, sizeof(lval*) * v->count);
  return x;
}

lval* lval_take(lval* v, int i) {
  lval* x = lval_pop(v, i);
  lval_del(v);
  return x;
}

void lval_print(lval* v);

void lval_print_expr(lval* v, char open, char close) {
  putchar(open);
  for (int i = 0; i < v->count; i++) {
    lval_print(v->cell[i]);    
    if (i != (v->count-1)) {
      putchar(' ');
    }
  }
  putchar(close);
}

void lval_print(lval* v) {
  switch (v->type) {
    case LVAL_FUN:   printf("<function>"); break;
    case LVAL_NUM:   printf("%li", v->num); break;
    case LVAL_ERR:   printf("Error: %s", v->err); break;
    case LVAL_SYM:   printf("%s", v->sym); break;
    case LVAL_SEXPR: lval_print_expr(v, '(', ')'); break;
    case LVAL_QEXPR: lval_print_expr(v, '{', '}'); break;
  }
}

void lval_println(lval* v) { lval_print(v); putchar('\n'); }

char* ltype_name(int t) {
  switch(t) {
    case LVAL_FUN: return "Function";
    case LVAL_NUM: return "Number";
    case LVAL_ERR: return "Error";
    case LVAL_SYM: return "Symbol";
    case LVAL_SEXPR: return "S-Expression";
    case LVAL_QEXPR: return "Q-Expression";
    default: return "Unknown";
  }
}

/* Lisp Environment */

struct lenv {
  int count;
  char** syms;
  lval** vals;
};

lenv* lenv_new(void) {

  /* Initialize struct */
  lenv* e = malloc(sizeof(lenv));
  e->count = 0;
  e->syms = NULL;
  e->vals = NULL;
  return e;
  
}

void lenv_del(lenv* e) {
  
  /* Iterate over all items in environment deleting them */
  for (int i = 0; i < e->count; i++) {
    free(e->syms[i]);
    lval_del(e->vals[i]);
  }
  
  /* Free allocated memory for lists */
  free(e->syms);
  free(e->vals);
  free(e);
}

lval* lenv_get(lenv* e, lval* k) {
  
  /* Iterate over all items in environment */
  for (int i = 0; i < e->count; i++) {
    /* Check if the stored string matches the symbol string */
    /* If it does, return a copy of the value */
    if (strcmp(e->syms[i], k->sym) == 0) {
      return lval_copy(e->vals[i]);
    }
  }
  /* If no symbol found return error */
  return lval_err("Unbound Symbol '%s'", k->sym);
}

void lenv_put(lenv* e, lval* k, lval* v) {
  
  /* Iterate over all items in environment */
  /* This is to see if variable already exists */
  for (int i = 0; i < e->count; i++) {
  
    /* If variable is found delete item at that position */
    /* And replace with variable supplied by user */
    if (strcmp(e->syms[i], k->sym) == 0) {
      lval_del(e->vals[i]);
      e->vals[i] = lval_copy(v);
      return;
    }
  }
  
  /* If no existing entry found allocate space for new entry */
  e->count++;
  e->vals = realloc(e->vals, sizeof(lval*) * e->count);
  e->syms = realloc(e->syms, sizeof(char*) * e->count);
  
  /* Copy contents of lval and symbol string into new location */
  e->vals[e->count-1] = lval_copy(v);
  e->syms[e->count-1] = malloc(strlen(k->sym)+1);
  strcpy(e->syms[e->count-1], k->sym);
}

/* Builtins */

#define LASSERT(args, cond, fmt, ...) \
  if (!(cond)) { lval* err = lval_err(fmt, ##__VA_ARGS__); lval_del(args); return err; }

#define LASSERT_TYPE(func, args, index, expect) \
  LASSERT(args, args->cell[index]->type == expect, \
    "Function '%s' passed incorrect type for argument %i. Got %s, Expected %s.", \
    func, index, ltype_name(args->cell[index]->type), ltype_name(expect))

#define LASSERT_NUM(func, args, num) \
  LASSERT(args, args->count == num, \
    "Function '%s' passed incorrect number of arguments. Got %i, Expected %i.", \
    func, args->count, num)

#define LASSERT_NOT_EMPTY(func, args, index) \
  LASSERT(args, args->cell[index]->count != 0, \
    "Function '%s' passed {} for argument %i.", func, index);


lval* lval_eval(lenv* e, lval* v);

lval* builtin_list(lenv* e, lval* a) {
  a->type = LVAL_QEXPR;
  return a;
}

lval* builtin_head(lenv* e, lval* a) {
  LASSERT_NUM("head", a, 1);
  LASSERT_TYPE("head", a, 0, LVAL_QEXPR);
  LASSERT_NOT_EMPTY("head", a, 0);
  
  lval* v = lval_take(a, 0);  
  while (v->count > 1) { lval_del(lval_pop(v, 1)); }
  return v;
}

lval* builtin_tail(lenv* e, lval* a) {
  LASSERT_NUM("tail", a, 1);
  LASSERT_TYPE("tail", a, 0, LVAL_QEXPR);
  LASSERT_NOT_EMPTY("tail", a, 0);

  lval* v = lval_take(a, 0);  
  lval_del(lval_pop(v, 0));
  return v;
}

lval* builtin_eval(lenv* e, lval* a) {
  LASSERT_NUM("eval", a, 1);
  LASSERT_TYPE("eval", a, 0, LVAL_QEXPR);
  
  lval* x = lval_take(a, 0);
  x->type = LVAL_SEXPR;
  return lval_eval(e, x);
}

lval* builtin_join(lenv* e, lval* a) {
  
  for (int i = 0; i < a->count; i++) {
    LASSERT_TYPE("join", a, i, LVAL_QEXPR);
  }
  
  lval* x = lval_pop(a, 0);
  
  while (a->count) {
    lval* y = lval_pop(a, 0);
    x = lval_join(x, y);
  }
  
  lval_del(a);
  return x;
}

lval* builtin_op(lenv* e, lval* a, char* op) {
  
  for (int i = 0; i < a->count; i++) {
    LASSERT_TYPE(op, a, i, LVAL_NUM);
  }
  
  lval* x = lval_pop(a, 0);
  
  if ((strcmp(op, "-") == 0) && a->count == 0) {
    x->num = -x->num;
  }
  
  while (a->count > 0) {  
    lval* y = lval_pop(a, 0);
    
    if (strcmp(op, "+") == 0) { x->num += y->num; }
    if (strcmp(op, "-") == 0) { x->num -= y->num; }
    if (strcmp(op, "*") == 0) { x->num *= y->num; }
    if (strcmp(op, "/") == 0) {
      if (y->num == 0) {
        lval_del(x); lval_del(y);
        x = lval_err("Division By Zero.");
        break;
      }
      x->num /= y->num;
    }
    
    lval_del(y);
  }
  
  lval_del(a);
  return x;
}

lval* builtin_add(lenv* e, lval* a) {
  return builtin_op(e, a, "+");
}

lval* builtin_sub(lenv* e, lval* a) {
  return builtin_op(e, a, "-");
}

lval* builtin_mul(lenv* e, lval* a) {
  return builtin_op(e, a, "*");
}

lval* builtin_div(lenv* e, lval* a) {
  return builtin_op(e, a, "/");
}

lval* builtin_def(lenv* e, lval* a) {

  LASSERT_TYPE("def", a, 0, LVAL_QEXPR);
  
  /* First argument is symbol list */
  lval* syms = a->cell[0];
  
  /* Ensure all elements of first list are symbols */
  for (int i = 0; i < syms->count; i++) {
    LASSERT(a, (syms->cell[i]->type == LVAL_SYM),
      "Function 'def' cannot define non-symbol. "
      "Got %s, Expected %s.",
      ltype_name(syms->cell[i]->type), ltype_name(LVAL_SYM));
  }
  
  /* Check correct number of symbols and values */
  LASSERT(a, (syms->count == a->count-1),
    "Function 'def' passed too many arguments for symbols. "
    "Got %i, Expected %i.",
    syms->count, a->count-1);
  
  /* Assign copies of values to symbols */
  for (int i = 0; i < syms->count; i++) {
    lenv_put(e, syms->cell[i], a->cell[i+1]);
  }
  
  lval_del(a);
  return lval_sexpr();
}

void lenv_add_builtin(lenv* e, char* name, lbuiltin func) {
  lval* k = lval_sym(name);
  lval* v = lval_fun(func);
  lenv_put(e, k, v);
  lval_del(k); lval_del(v);
}

void lenv_add_builtins(lenv* e) {
  /* Variable Functions */
  lenv_add_builtin(e, "def", builtin_def);
  
  /* List Functions */
  lenv_add_builtin(e, "list", builtin_list);
  lenv_add_builtin(e, "head", builtin_head);
  lenv_add_builtin(e, "tail", builtin_tail);
  lenv_add_builtin(e, "eval", builtin_eval);
  lenv_add_builtin(e, "join", builtin_join);
  
  /* Mathematical Functions */
  lenv_add_builtin(e, "+", builtin_add);
  lenv_add_builtin(e, "-", builtin_sub);
  lenv_add_builtin(e, "*", builtin_mul);
  lenv_add_builtin(e, "/", builtin_div);
}

/* Evaluation */

lval* lval_eval_sexpr(lenv* e, lval* v) {
  
  for (int i = 0; i < v->count; i++) {
    v->cell[i] = lval_eval(e, v->cell[i]);
  }
  
  for (int i = 0; i < v->count; i++) {
    if (v->cell[i]->type == LVAL_ERR) { return lval_take(v, i); }
  }
  
  if (v->count == 0) { return v; }  
  if (v->count == 1) { return lval_take(v, 0); }
  
  /* Ensure first element is a function after evaluation */
  lval* f = lval_pop(v, 0);
  if (f->type != LVAL_FUN) {
    lval* err = lval_err(
      "S-Expression starts with incorrect type. "
      "Got %s, Expected %s.",
      ltype_name(f->type), ltype_name(LVAL_FUN));
    lval_del(f); lval_del(v);
    return err;
  }
  
  /* If so call function to get result */
  lval* result = f->fun(e, v);
  lval_del(f);
  return result;
}

lval* lval_eval(lenv* e, lval* v) {
  if (v->type == LVAL_SYM) {
    lval* x = lenv_get(e, v);
    lval_del(v);
    return x;
  }
  if (v->type == LVAL_SEXPR) { return lval_eval_sexpr(e, v); }
  return v;
}

/* Reading */

lval* lval_read_num(mpc_ast_t* t) {
  errno = 0;
  long x = strtol(t->contents, NULL, 10);
  return errno != ERANGE ? lval_num(x) : lval_err("Invalid Number.");
}

lval* lval_read(mpc_ast_t* t) {
  
  if (strstr(t->tag, "number")) { return lval_read_num(t); }
  if (strstr(t->tag, "symbol")) { return lval_sym(t->contents); }
  
  lval* x = NULL;
  if (strcmp(t->tag, ">") == 0) { x = lval_sexpr(); } 
  if (strstr(t->tag, "sexpr"))  { x = lval_sexpr(); }
  if (strstr(t->tag, "qexpr"))  { x = lval_qexpr(); }
  
  for (int i = 0; i < t->children_num; i++) {
    if (strcmp(t->children[i]->contents, "(") == 0) { continue; }
    if (strcmp(t->children[i]->contents, ")") == 0) { continue; }
    if (strcmp(t->children[i]->contents, "}") == 0) { continue; }
    if (strcmp(t->children[i]->contents, "{") == 0) { continue; }
    if (strcmp(t->children[i]->tag,  "regex") == 0) { continue; }
    x = lval_add(x, lval_read(t->children[i]));
  }
  
  return x;
}

/* Main */

int main(int argc, char** argv) {
  
  mpc_parser_t* Number = mpc_new("number");
  mpc_parser_t* Symbol = mpc_new("symbol");
  mpc_parser_t* Sexpr  = mpc_new("sexpr");
  mpc_parser_t* Qexpr  = mpc_new("qexpr");
  mpc_parser_t* Expr   = mpc_new("expr");
  mpc_parser_t* Lispy  = mpc_new("lispy");
  
  mpca_lang(MPCA_LANG_DEFAULT,
    "                                                     \
      number : /-?[0-9]+/ ;                               \
      symbol : /[a-zA-Z0-9_+\\-*\\/\\\\=<>!&]+/ ;         \
      sexpr  : '(' <expr>* ')' ;                          \
      qexpr  : '{' <expr>* '}' ;                          \
      expr   : <number> | <symbol> | <sexpr> | <qexpr> ;  \
      lispy  : /^/ <expr>* /$/ ;                          \
    ",
    Number, Symbol, Sexpr, Qexpr, Expr, Lispy);
  
  puts("Lispy Version 0.0.0.0.7");
  puts("Press Ctrl+c to Exit\n");
  
  lenv* e = lenv_new();
  lenv_add_builtins(e);
  
  while (1) {
  
    char* input = readline("lispy> ");
    add_history(input);
    
    mpc_result_t r;
    if (mpc_parse("<stdin>", input, Lispy, &r)) {
      lval* x = lval_eval(e, lval_read(r.output));
      lval_println(x);
      lval_del(x);
      mpc_ast_delete(r.output);
    } else {    
      mpc_err_print(r.error);
      mpc_err_delete(r.error);
    }
    
    free(input);
    
  }
  
  lenv_del(e);
  
  mpc_cleanup(6, Number, Symbol, Sexpr, Qexpr, Expr, Lispy);
  
  return 0;
}

Metas Bônus


  • › Crie uma Macro para ajudar especificamente relatar erros de tipos.
  • › Crie uma Macro para ajudar especificamente relatar erros de argumentos.
  • › Crie uma Macro para ajudar especificamente relatar erros de lista vazia.
  • › Mude a impressão duma função builtin para que imprime seu nome.
  • › Escreva uma função para imprimir todos os valores nomeados em um ambiente.
  • › Redefina uma das variáveis builtins para algo diferente.
  • › Mude a redefinição de uma das variáveis builtins para algo diferente de um erro.
  • › Crie uma função exit para parar o prompt e sair.

Navegação

‹ Q-Expressions (expressões citadas)

• Conteúdo •

Funções ›