Não me Interprete Mal: Improvisando Testes para um Interpretador

Estou amando ler o livro Crafting Interpreters. Nele, Bob Nystrom nos ensina como escrever um interpretador implementando uma pequena linguagem de programação chamada Lox. Há muito tempo não me divertia tanto programando! Além de bem escrito, o livro é engraçado e ensina bem mais coisas do que eu esperava. Mas estou tendo um problema.

Os snipptes no livro são escritos para copiar e colar. Contudo, o livro tem desafios ao final de cada capítulo, estes desafios não têm código-fonte e por vezes nos forçam a alterar muito o interpretador. Eu faço todos estes exercícios, e como resultado meu interpretador é diferente demais do código mostrado no livro. Como consequência, várias vezes quebro alguma parte do interpretador.

Como resolver isso?

Testes unitários seriam frágeis, já que a estrutura do código muda frequentemente. Testes de fim a fim parecem mais práticos nesse caso. Assim, para cada nova funcionalidade da linguagem, escrevi um programinha. Por exemplo, meu interpretador deve criar clausuras, e para garantir isso copiei o programa em Lox abaixo para o arquivo counter.lox:

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

O resultado deste programa devem ser os números 1 e 2 impressos em linhas separadas. Então coloquei esses valores em um arquivo chamado counter.lox.out. O programa também não pode falhar, então criei um arquivo vazio chamado counter.lox.err. (Em alguns casos é preciso garantir que o programa Lox falhará. Nesses casos, o arquivo .lox.err deve ter conteúdo.)

Pois bem, escrevi programas e arquivos de saída para vários exemplos. Agora preciso comparar os resultados dos programas com as saídas esperadas. Resolvi então usar a ferramenta que mais me ajuda nos momentos de urgência: shell script. Fiz um script Bash com um laço for iterando sobre todos os exemplos:

for l in examples/*.lox
do

done

Para cada exemplo, executei o programa Lox, redirecionando as saídas para arquivos temporários:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err
done

Agora, comparamos a saída real com a saída esperada com diff. Quando compara dois arquivos, diff returna 0 se não há diferença, 1 se existe alguma diferença, ou 2 em caso de erro. Como em Bash o condicional if considera 0 como verdadeiro, basta checar a negação do retorno de diff.

Se o programa imprimir algo na saída-padrão diferente do que está no arquivo .lox.out, temos uma falha:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi
done

Também comparamos a saída de erro com o arquivo .lox.err:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi
done

Por fim, verifico se houve alguma falha e reporto o resultado:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Nem todos os meus programas em Lox podem ser checados, porém. Por exemplo, há um programa que cronometra a execução de loops, é impossível prever o valor que ele vai imprimir. Por isso, adicionei a possibilidade de pular alguns programas: basta criar um arquivo com a extensão .lox.skip:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Se, porém, tenho um exemplo em Lox e ele não possui os arquivos de saída esperada (nem o arquivo .lox.skip) então tenho um problema e o script inteiro falha:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  elif [ ! -e $l.out ] || [ ! -e $l.err ]
  then
    echo missing $l.out or $l.err
    exit 1
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Com isto, meu script testador está pronto. Vejamos como se comporta:

$ ./lcheck.sh
PASS examples/attr.lox
PASS examples/bacon.lox
PASS examples/badfun.lox
PASS examples/badret.lox
PASS examples/bagel.lox
PASS examples/bostoncream.lox
PASS examples/cake.lox
PASS examples/checkuse.lox
PASS examples/circle2.lox
PASS examples/circle.lox
1d0
< 3 1c1 < --- > [line 1] Error at ',': Expect ')' after expression.
FAIL examples/comma.lox
PASS examples/counter.lox
PASS examples/devonshinecream.lox
PASS examples/eclair.lox
PASS examples/fibonacci2.lox
PASS examples/fibonacci.lox
PASS examples/func.lox
PASS examples/funexprstmt.lox
PASS examples/hello2.lox
PASS examples/hello3.lox
PASS examples/hello.lox
PASS examples/math.lox
PASS examples/notaclass.lox
PASS examples/noteveninaclass.lox
PASS examples/point.lox
PASS examples/retthis.lox
PASS examples/scope1.lox
PASS examples/scope.lox
PASS examples/supersuper.lox
PASS examples/thisout.lox
PASS examples/thrice.lox
SKIP examples/timeit.lox
PASS examples/twovars.lox
PASS examples/usethis.lox
PASS examples/varparam.lox

Opa, aparentemente removi o suporte ao operador vírgula por acidente. Ainda bem que fiz este script, não é?

Espero que este post tenha sido minimamente interessante! Agora, vou consertar meu operador vírgula e seguir lendo este livro maravilhoso.

Post Revisions:

Post a Comment

Your email is never shared. Required fields are marked *

*
*