Tratamento de Erros • Capítulo 8

Crashes


Alguns de vocês talvez notaram um problema com o programa do capítulo anterior. Tente digitar o seguinte no prompt e veja o que acontece.

Lispy Version 0.0.0.0.3
Press Ctrl+c to Exit

lispy> / 10 0

Eita! O programa falhou tentando dividir por zero. Não tem problema que um programa falhe durante o desenvolvimento, mas nosso programa final idealmente nunca falharia, e deve sempre explicar ao usuário o que deu errado.

walterwhite

Walter White • Heisenberg

No momento, nosso programa pode produzir erros de sintaxe mas ainda não tem funcionalidade para reportar erros na avaliação das expressões. Precisamos embutir algum tipo de funcionalidade de tratamento de erros para fazer isso. Pode ser um pouco desajeitado em C, mas se começarmos no caminho certo, vai valer a pena mais tarde quando nosso sistema fica mais complicado.

Programas C falhando são coisas da vida. Se alguma coisa dá errado, o sistema operacional os expulsa. Programas podem falhar por causa de muitas razões diferentes, de muitas maneiras diferentes. Você verá pelo menos um Heisenbug (isto é, um bug que parece desaparecer quando você tenta investigá-lo).

Mas não há mágica em como programas C funcionam. Se você encontrar um bug realmente perturbador, não desista ou sente e encare a tela até seus olhos sangrarem. Aproveita esta chance para aprender propriamente como usar gdb e valgrind. Serão mais armas no seu arsenal, e depois do investimento inicial, pouparão bastante tempo e dor.

Valor Lisp


Há muitas maneiras de lidar com erros em C, mas neste contexto meu jeito preferido é tornar erros um possível resultado de avaliar uma expressão. Assim podemos dizer que, em Lispy, uma expressão resultará ou em um número, ou em um erro. Por exemplo, + 1 2 resultará em um número, mas / 10 0 resultará em um erro.

Para isso, precisamos uma estrutura de dados que possa agir como uma coisa ou qualquer outra. Para simplificar, vamos usar uma struct com campos específicos para cada coisa que pode ser representadas, e um tipo especial type para nos dizer exatamente quais campos são significativos para acessar.

Vamos chamar isto de um lval, que significa Lisp Value (valor Lisp).

/* Declara uma struct lval */
typedef struct {
  int type;
  long num;
  int err;
} lval;

Enumerações


Você vai notar que o tipo dos campos type, e err é int. Isto significa que são representados por um único número inteiro.

O motivo que escolhemos int é porque vamos atribuir significado a cada valor inteiro, para codificar o que necessitamos. Por exemplo, podemos fazer uma regra "Se type for 0, então a estrutura é um Number.", ou "Se type for 1 então a estrutura é um Error.". Esta é uma forma simples e efetiva de fazer as coisas.

Mas se enchermos com 0 e 1 perdidos no nosso código, vai se tornar cada vez mais obscuro o que está acontecendo. Em vez disso, podemos usar constantes nomeadas que tenham esses valores inteiros atribuídos. Isto dá ao leitor uma indicação do por que alguém poderia comparar um número com 0 ou 1 e o que quer dizer neste contexto.

Em C, isto se faz usando um enum.

/* Cria Enumeracao dos Possiveis Tipos lval */
enum { LVAL_NUM, LVAL_ERR };

Um enum é uma declaração de variáveis às quais por baixo dos panos são atribuídos valores inteiros constantes automaticamente. O código acima descreve como declararíamos valores enumerados para o campo type.

Também queremos declarar uma enumeração para o campo error. Temos três casos de erro no nosso programa específico. Há divisão por zero, um operador desconhecido, ou um número sendo passado que seja grande demais para ser representado internamente usando um long. Estes casos podem ser enumerados como segue.

/* Create Enumeration of Possible Error Types */
enum { LERR_DIV_ZERO, LERR_BAD_OP, LERR_BAD_NUM };

Funções de valor Lisp


Nosso tipo lval está quase pronto. Diferentemente do tipo long anterior, não temos atualmente uma maneira de criar novas instâncias dele. Para fazer isso, podemos declarar duas funções que constrói um lval a partir de um tipo error ou um tipo number.

/* Cria um novo lval do tipo numero */
lval lval_num(long x) {
  lval v;
  v.type = LVAL_NUM;
  v.num = x;
  return v;
}

/* Cria um novo lval do tipo erro */
lval lval_err(int x) {
  lval v;
  v.type = LVAL_ERR;
  v.err = x;
  return v;
}

Estas funções primeiro criam um lval chamado v, e atribuem os campos antes de retorná-lo.

Como nossa função lval pode agora ser uma de duas coisas, não podemos mais simplesmente usar printf para imprimi-la. Vamos querer se comportar de maneira diferente dependendo do tipo do lval que for dado. Há uma maneira concisa de fazer isto em C usando o comando switch. Ele toma algum valor como entrada, e compara com alguns valores conhecidos, conhecidos como casos. Quando os valores são iguais, ele executa o código que segue até o próximo comando break.

Usando isso podemos construir uma função que imprime um lval de qualquer tipo, assim:

/* Imprime um "lval" */
void lval_print(lval v) {
  switch (v.type) {
    /* Caso o tipo for um numero, imprima-o */
    /* A seguir, sai fora (break) do switch. */
    case LVAL_NUM: printf("%li", v.num); break;

    /* Caso o tipo for um erro */
    case LVAL_ERR:
      /* Checa qual o tipo do erro e imprime-o */
      if (v.err == LERR_DIV_ZERO) {
        printf("Error: Division By Zero!");
      }
      if (v.err == LERR_BAD_OP)   {
        printf("Error: Invalid Operator!");
      }
      if (v.err == LERR_BAD_NUM)  {
        printf("Error: Invalid Number!");
      }
    break;
  }
}

/* Imprime um "lval" seguido duma quebra de linha */
void lval_println(lval v) { lval_print(v); putchar('\n'); }

Avaliando erros


Agora que sabemos como trabalhar com o tipo lval, precisamos mudar nossas funções de avaliação para usá-lo em vez de long.

Precisamos mudar as assinaturas de tipo, precisamos mudar as funções de forma que funcionem corretamente ao encontrar na entrada tanto um error quanto um number.

Em nossa função eval_op, se encontramos um erro devemos retorná-lo imediatamente, e somente fazer alguma computação se ambos os argumentos são números. Devemos modificar nosso código para retornar um erro em vez de tentar dividir por zero. Isso vai corrigir a falha descrita no começo deste capítulo.

lval eval_op(lval x, char* op, lval y) {

  /* Se algum valor for erro, retorna-o */
  if (x.type == LVAL_ERR) { return x; }
  if (y.type == LVAL_ERR) { return y; }

  /* Senao, faz a matematica nos valores numericos */
  if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); }
  if (strcmp(op, "-") == 0) { return lval_num(x.num - y.num); }
  if (strcmp(op, "*") == 0) { return lval_num(x.num * y.num); }
  if (strcmp(op, "/") == 0) {
    /* Se o segundo operando for zero, retorna erro */
    return y.num == 0 
      ? lval_err(LERR_DIV_ZERO) 
      : lval_num(x.num / y.num);
  }

  return lval_err(LERR_BAD_OP);
}

O que aquele ? está fazendo ali?

Você vai notar que para a divisão checar se o segundo argumento é zero usamos um ponto de interrogação ?, seguido de dois pontos :. Este é o chamado operador ternário, e permite escrever expressões condicionais em uma linha.

Ele funciona mais ou menos assim: <condição> ? <então> : <senão>. Em outras palavras, se a condição é verdadeira, ele retorna o que segue o ?, senão ele retorna o que segue o :.

Algumas pessoas não gostam desde operador porque acreditam que ele torna o código obscuro. Se você não está familiarizado com o operador ternário, pode inicialmente achá-lo estranho de usar; mas uma vez que o conhece bem raramente há problemas.

Precisamos dar um tratamento similar para nossa função eval. Neste caso, como definimos eval_op para tratar erros de maneira robusta, precisamos apenas adicionar as condições de erro para nossa função de conversão de números.

Neste caso, usamos a função strtol para converter de string para long. Isto nos permite checar uma variável especial errno para se certificar que a conversão funcionou corretamente. Essa é uma maneira mais robusta de converter números que nosso método anterior usando atoi.

lval eval(mpc_ast_t* t) {
  
  if (strstr(t->tag, "number")) {
    /* Checa se existe algum erro na conversao */
    errno = 0;
    long x = strtol(t->contents, NULL, 10);
    return errno != ERANGE ? lval_num(x) : lval_err(LERR_BAD_NUM);
  }
  
  char* op = t->children[1]->contents;  
  lval x = eval(t->children[2]);
  
  int i = 3;
  while (strstr(t->children[i]->tag, "expr")) {
    x = eval_op(x, op, eval(t->children[i]));
    i++;
  }
  
  return x;  
}

O último pequeno passo é mudar como imprimimos o resultado encontrado pela nossa avaliação para usar nossa nova função de impressão que pode imprimir qualquer tipo de lval.

lval result = eval(r.output);
lval_println(result);
mpc_ast_delete(r.output);

E pronto! Tente rodar este novo programa e certifique-se que não há falhas ao dividir por zero.

lispy> / 10 0
Error: Division By Zero!
lispy> / 10 2
5

Encanamento


plumbing

Encanamento • Mais difícil do que pensa

Alguns de vocês que chegaram nesse ponto do livro podem se sentir desconfortáveis com a maneira que ele está progredindo. Talvez você creia que conseguiu seguir as instruções bem o suficiente, mas não tem um entendimento claro de todos os mecanismos acontecendo por trás dos panos.

Caso seja o seu caso, quero assegurar-lhe que você está indo bem. Caso não entenda todos os detalhes é porque posso não ter explicado tudo em profundidade suficiente. E não tem problema.

Ser capaz de progredir e fazer o código funcionar sob essas condições é uma grande habilidade em programação, e se você chegou até aqui isso mostra que você a tem.

Em programação, chamamos isso de fazer o encanamento (do inglês, plumbing). A grosso modo, isso é seguir instruções tentando amarrar um bando de bibliotecas ou componentes, sem compreender inteiramente como eles funcionam internamente.

Isto requer e intuição. é necessário para para acreditar que se as estrelas se alinharem, e todas as encantações executarem corretamente para essa máquina mágica, a coisa certa vai realmente acontecer. E intuição é necessária para descobrir o que deu errado, e como corrigir coisas quando elas não vão de acordo com o planejado.

Infelizmente essas coisas não podem ser ensinadas diretamente, então se você chegou até aqui então você superou uma ladeira difícil, e nos capítulos seguintes prometo que vamos terminar com o encanamento, e realmente começar a programar coisas que sejam novas e saudáveis.

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

/* Create Enumeration of Possible Error Types */
enum { LERR_DIV_ZERO, LERR_BAD_OP, LERR_BAD_NUM };

/* Create Enumeration of Possible lval Types */
enum { LVAL_NUM, LVAL_ERR };

/* Declare New lval Struct */
typedef struct {
  int type;
  long num;
  int err;
} lval;

/* Create a new number type lval */
lval lval_num(long x) {
  lval v;
  v.type = LVAL_NUM;
  v.num = x;
  return v;
}

/* Create a new error type lval */
lval lval_err(int x) {
  lval v;
  v.type = LVAL_ERR;
  v.err = x;
  return v;
}

/* Print an "lval" */
void lval_print(lval v) {
  switch (v.type) {
    /* In the case the type is a number print it */
    /* Then 'break' out of the switch. */
    case LVAL_NUM: printf("%li", v.num); break;
    
    /* In the case the type is an error */
    case LVAL_ERR:
      /* Check what type of error it is and print it */
      if (v.err == LERR_DIV_ZERO) {
        printf("Error: Division By Zero!");
      }
      if (v.err == LERR_BAD_OP)   {
        printf("Error: Invalid Operator!");
      }
      if (v.err == LERR_BAD_NUM)  {
        printf("Error: Invalid Number!");
      }
    break;
  }
}

/* Print an "lval" followed by a newline */
void lval_println(lval v) { lval_print(v); putchar('\n'); }

lval eval_op(lval x, char* op, lval y) {
  
  /* If either value is an error return it */
  if (x.type == LVAL_ERR) { return x; }
  if (y.type == LVAL_ERR) { return y; }
  
  /* Otherwise do maths on the number values */
  if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); }
  if (strcmp(op, "-") == 0) { return lval_num(x.num - y.num); }
  if (strcmp(op, "*") == 0) { return lval_num(x.num * y.num); }
  if (strcmp(op, "/") == 0) {
    /* If second operand is zero return error */
    return y.num == 0 
      ? lval_err(LERR_DIV_ZERO) 
      : lval_num(x.num / y.num);
  }
  
  return lval_err(LERR_BAD_OP);
}

lval eval(mpc_ast_t* t) {
  
  if (strstr(t->tag, "number")) {
    /* Check if there is some error in conversion */
    errno = 0;
    long x = strtol(t->contents, NULL, 10);
    return errno != ERANGE ? lval_num(x) : lval_err(LERR_BAD_NUM);
  }
  
  char* op = t->children[1]->contents;  
  lval x = eval(t->children[2]);
  
  int i = 3;
  while (strstr(t->children[i]->tag, "expr")) {
    x = eval_op(x, op, eval(t->children[i]));
    i++;
  }
  
  return x;  
}

int main(int argc, char** argv) {
  
  mpc_parser_t* Number = mpc_new("number");
  mpc_parser_t* Operator = mpc_new("operator");
  mpc_parser_t* Expr = mpc_new("expr");
  mpc_parser_t* Lispy = mpc_new("lispy");
  
  mpca_lang(MPCA_LANG_DEFAULT,
    "                                                     \
      number   : /-?[0-9]+/ ;                             \
      operator : '+' | '-' | '*' | '/' ;                  \
      expr     : <number> | '(' <operator> <expr>+ ')' ;  \
      lispy    : /^/ <operator> <expr>+ /$/ ;             \
    ",
    Number, Operator, Expr, Lispy);
  
  puts("Lispy Version 0.0.0.0.4");
  puts("Press Ctrl+c to Exit\n");
  
  while (1) {
  
    char* input = readline("lispy> ");
    add_history(input);
    
    mpc_result_t r;
    if (mpc_parse("<stdin>", input, Lispy, &r)) {
      lval result = eval(r.output);
      lval_println(result);
      mpc_ast_delete(r.output);
    } else {    
      mpc_err_print(r.error);
      mpc_err_delete(r.error);
    }
    
    free(input);
    
  }
  
  mpc_cleanup(4, Number, Operator, Expr, Lispy);
  
  return 0;
}

Metas bônus


  • › Rode o código do capítulo anterior com o gdb e faça-o falhar. Veja o que acontece.
  • › Como você dá um nome a um enum?
  • › O que são tipos de dados union e como eles funcionam?
  • › Quais as vantagens de usar um union em vez de um struct?
  • › É possível usar um union na definição de lval?
  • › Amplie análise sintática e avaliação para suportar o operador resto da divisão %.
  • › Amplie análise sintática e avaliação para suportar tipos decimais usando um campo double.

Navegação

‹ Avaliação

• Conteúdo •

S-Expressions (expressões simbólicas) ›