Tutorial de Brainfuck
O Brainfuck (também conhecido por Brainf*ck ou por BF, devido ao seu nome polémico) é uma linguagem esotérica, cujo objetivo é “torrar cérebros” a ler o código. Mas, vão ver que depois de aprenderem toda a lógica de Brainfuck, ainda vão conseguir criar umas coisas engraçadas de forma fácil!
Um primeiro olhar
Antes de continuar a avançar “em seco”, aqui fica o típico “Hello World”:
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++
..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.
No final do tutorial, vamos ver em detalhe como funciona este código. Para avançar com as explicações, é preciso que estejam familiarizados com códigos da tabela ASCII e noções básicas de apontadores (ou vetores, também serve para o efeito). Recomendo a manter estes sites abertos enquanto seguem o tutorial:
- Referência para tabela ASCII: https://asciitable.com/ (ou outro site similar da vossa preferência)
- Para correr os exemplos que forem colocados e testarem código vosso podem usar o Brainfuck Interpreter do Project Nayuki.
Comecem por testar o código anterior e vão obter o famoso Hello World:
Hello World!
Como funciona o Brainfuck?
No Brainfuck, como noutras linguagens, temos variáveis. As variáveis são, na verdade, uma única memória composta por células ou posições. Não há, por isso, nomes ou etiquetas que correspondam a uma variável, mas sim uma posição. Cada posiçõa irá guardar um valor numérico inteiro, que faz correspondência com um caractere da tabela ASCII (daí a necessidade de ter uma tabela com os códigos correspondentes ou então conhecê-la muito bem :-) ).
Memória: [ 65 0 89 72 ]
Posição: ^
Neste exemplo é possível ver que temos uma memória com 4 posições conhecidas: a primeira com o valor 65, a segunda com o valor 0, etc. Apenas é possível ler um valor de cada vez, de acordo com a posição atual. Por omissão, o programa começa na primeira posição, onde temos o valor 65. Se for mais fácil de perceber, podemos fazer uma analogia com uma lista duplamente ligada, na qual é possível aceder a uma posição à esquerda ou à direita da posição atual.
Nas próximas secções vamos ver como ler e definir valores nesta memória e como alternar entre posições.
Operadores em Brainfuck
Em Brainfuck, temos apenas 8 operadores. Tipicamente, os interpretadores de Brainfuck ignoram tudo o que não sejam estes 8 operadores, por isso o resto pode ser considerado como comentários.
Operador | Equivalente em C | Explicação |
---|---|---|
> | ++p (incr. endereço) | Muda para a posição seguinte da memória |
< | −−p (decr. endereço) | Muda para a posição anterior da memória |
+ | ++(*p) | Incrementa o valor da memória na posição atual |
- | −−(*p) | Decrementa o valor da memória na posição atual |
. | putchar(p) | Imprime no ecrã o caractere ASCII correspondente ao valor da memória na posição atual |
, | *p = getchar(p) | Lê um caractere de teclado (ou de outra fonte de input) para a posição atual |
[ | while(*p) { | Inicio de um ciclo while, com o valor da posição atual como guarda/condição (!= 0) |
] | } | Fim de ciclo |
(Tabela adaptada de http://www.muppetlabs.com/~breadbox/bf/)
Manipulação básica
Em Brainfuck, a memória tem estado, ou seja, um valor alterado ou uma posição trocada irá manter-se na próxima iteração. A ideia base de um programa em Brainfuck é guardar valores correspondentes a carateres ASCII, de forma a poder usá-los mais tarde. Vamos ver um exemplo prático:
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
+++++
.
Usem o Brainfuck Interpreter para testar: copiem o código do exemplo para a caixa “Code”, carreguem no botão “Load” para iniciar a máquina de estados de depois no botão “Run”. Devem obter o seguinte output:
A
Incialmente a memória interna do programa tem este estado:
Memória: [ 0 ]
Posição: ^
Se consultarem a tabela ASCII, vão reparar que o caractere A maiúsculo corresponde ao inteiro 65. Segundo a tabela de operadores que já foi apresentada, o operador + (mais) serve para incrementar numa unidade (+1) o valor da posição atual da memória (inicializado internamente a zero). As 6 primeiras linhas têm 10 operadores + e a penúltima tem mais 5 operações: 65 operadores de incremento. Isto vai permitir colocar o valor 65 na primeira posição de memória, precisamente o que queremos para que apareça a letra A. No final da execução, a memória tem este estado:
Memória: [ 65 ]
Posição: ^
Mas é necessário mais um passo. A última linha contém o operador . (ponto), que nada mais faz que ir buscar o valor atual e imprimir o caractere correspondente. O resultado é o caractere pretendido impresso no ecrã.
Usar várias posições
Até aqui ainda só usámos a primeira posição da memória, na qual guardámos o valor. Mas como é natural, pode interessar-nos manter mais que um valor em memória. Vamos ver como usar várias posições e como alternar entre elas.
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++.
>
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
++++++++++
+++++++++.
<.
Corram agora este exemplo no Brainfuck Interpreter. O output deve ser a típica expressão LOL:
LOL
É semelhante ao exemplo anterior, com a diferença que desta vez introduzimos mais dois operadores: o operador > (maior) muda para a posição à direita/seguinte; de forma inversa, o operador < (menor) muda para a posição à esquerda/anterior. Neste exemplo, são usadas 2 posições de memória. Na primeira posição é colocado o valor 76, que corresponde ao L maiúsculo em ASCII.
Memória: [ 76 ]
Posição: ^
De seguida imprimimos para o ecrã, com o operador . (ponto). Na 9ª linha, encontramos o operador >, que vai trocar a posição atual. Então significa que temos agora duas posições conhecidas na memória: a primeira, com o valor 76, e agora esta 2ª posição para a qual acabamos de mudar, inicializada a 0.
Memória: [ 76 0 ]
Posição: ^
Vamos então criar a letra O, cujo código ASCII é 79. Tal como para o L, vamos colocar nesta nova posição o valor 79 correspondente ao caractere O e imprimimos. Tal como anteriormente, chamamos tantos operadores + quanto necessários para obter o valor. Até agora temos o output:
LO
A memória está assim:
Memória: [ 76 79 ]
Posição: ^
Resta-nos a última letra L. Podíamos colocar novamente 76 operadores de incremento e imprimir, mas na nossa primeira posição já temos o valor que corresponde ao L maiúsculo: tudo o que temos de fazer é ir lá buscá-lo; voltamos à posição anterior do vetor com o operador <, e imprimimos o L. É tudo o que faz a última linha de código. Em termos de memória, apenas se trocou a posição atual:
Memória: [ 76 79 ]
Posição: ^
Tirar partido dos ciclos
O Brainfuck também tem ciclos, semelhantes ao while: vamos tirar partido deles para escrever (um pouco) menos linhas de código. Como sabemos, no ciclo while, temos uma condição que enquanto foi verdadeira, o ciclo continua. No Brainfuck, é similar: como apenas dispomos de valores inteiros, a condição de paragem de um ciclo nesta linguagem é quando a posição atual, na altura da comparação, tem o valor 0.
Para ser mais específico, vamos ter de inicializar uma posição de memória, que irá servir de guarda do ciclo, com o número de iterações que queremos que o nosso ciclo faça, e ir decrementando. A nossa condição de paragem é quando este valor da guarda chegar a zero: aí, o ciclo deixa de executar, logo interessa-nos colocar exatamente o número de vezes que queremos que o ciclo repita, e ir subtraindo até chegar a 0. Vamos voltar ao exemplo da letra A, agora com ciclos:
++++++
[
>+++++++++++
<-
]
>-.
Output:
A
Comparando ao exemplo anterior, conseguimos poupar alguns caracteres ao usar ciclos. O objetivo é o mesmo: obter o valor 65 na primeira posição de memória.
Analisando: a primeira linha inicializa a primeira posição com o valor 6. Hã, mas não era 65 que queríamos? Sim. Mas não vai ser esta posição que vamos usar para mostrar o A. Na verdade, esta posição vai ser a condição de paragem (guarda) do nosso ciclo. Vamos então analisar o corpo do ciclo que fica entre os parênteses retos [ e ] (ou seja, o código que vai ser repetido à semelhança do ciclo while). No caso do Brainfuck, um ciclo continua até que a posição atual tenha o valor 0.
No interior do ciclo, tudo o que fazemos é trocar para a segunda posição, incrementar 11 unidades, voltar a trocar para a primeira posição e decrementar uma unidade com o operador - (menos). Esta última linha do ciclo é particularmente importante, porque é ela responsável por atualizar a condição de paragem. Na primeira iteração do ciclo, o valor da primeira posição vai passar de 6 para 5, e como 5 é diferente de 0, o código do ciclo vai ser novamente executado. E assim sucessivamente, até que o valor seja 0, e aí o ciclo termina.
Resumindo, a utilização do ciclo vai nos permitir colocar o valor 66 na segunda posição: o código do ciclo foi executado 6 vezes, e por cada vez, foram incrementadas 11 unidades na segunda posição, totalizando as 66 unidades (6 * 11 = 66). O nosso objetivo é imprimir o caractere correspondente ao valor 65, por isso, tendo o 66 na segunda posição, resta-nos trocar para a segunda posição novamente, decrementar uma unidade, e imprimir.
Um quadro resumo das iterações:
Nº Iterações ciclo | 1ª posição | 2ª posição |
---|---|---|
Antes do ciclo | 6 | 0 |
Após 1ª | 5 | 11 |
Após 2ª | 4 | 22 |
Após 3ª | 3 | 33 |
Após 4ª | 2 | 44 |
Após 5ª | 1 | 55 |
Após 6ª | 0 | 66 |
Há aqui um pormenor importante: com os ciclos obtemos sempre aproximações dos números ASCII que pretendemos, com vista a diminuir o código produzido. Neste caso, usamos a aproximação de 66, para escrevermos o 65 mas, por outro lado, podíamos incrementar 13 unidades 5 vezes e ter o valor 65, e tornava-se desnecessário fazer mais uma operação de decremento. No entanto, a técnica é pegar no ciclo e obter uma aproximação ou valor exato, se possível, numa posição de memória, trocar para essa posição, incrementar/decrementar para obter o valor pretendido (se for necessário) e imprimir.
Esta é uma das estratégias para otimizar programas em Brainfuck, guardando alguns caracteres em diversas posições de memória, para depois “jogar” com operações de incremento e decremento (aproximação a vários caracteres).
Análise ao Hello World!
Conhecer ciclos, operadores de incremento e decremento, e ainda os operadores de troca de posições, é o suficiente para compreender o programa Hello World! Vejamos novamente o programa:
++++++++++
[
>+++++++
>++++++++++
>+++
<<<-
]
>++. # Escrever H
>+. # Escrever e
+++++++.. # Escrever l (2 vezes)
+++. # Escrever o
>++. # Escrever um espaço
<<+++++++++++++++. # Escrever W
>. # Escrever novamente o
+++. # Escrever um r
------. # Escrever um l
--------. # Escrever um d
>+. # Escrever um !
Alternando as posições de memória, e aproveitando de forma inteligente os valores que já dispomos, conseguimos minimizar os programa em Brainfuck de modo a parecerem mais simples.
Receber dados de input
Já vimos até agora 7 dos 8 operadores de Brainfuck. Falta-nos apenas falar do operador responsável pela leitura de dados, o operador , (vírgula). Este operador lê um único caractere, obtém o seu valor inteiro equivalente, segundo a tabela ASCII, e armazena-o na posição atual. O seu funcionamento depende da implementação de cada compilador/interpretador: os caracteres podem ser lidos de ficheiros, teclado, por passagem de argumentos ao programa, etc. Não vou avançar muito mais que isto, pelo que me fico por um exemplo muito simples:
,-.+.+.
Na sua essência, este programa recebe um caractere como argumento, e mostra o caractere anterior, o próprio, e o caractere seguinte, segundo a tabela ASCII. Por exemplo, se introduzirem na caixa Input do JavaScript Interpreter (antes de correr o programa) a letra D, o programa irá produzir o seguinte output:
CDE
Desafios/Exercícios de revisão
Para tornar este tutorial mais rico, resolvi adicionar dois simples exercícios para tentarem resolver e aplicarem conhecimentos adquiridos aqui e nos links de secção de referências.
- Fazer um programa que imprima o alfabeto de letras minúsculas de A até Z, faça uma quebra de linha, e imprima os números de 9 até 0 (ou seja, ordem inversa de 0-9)
- Dados dois caracteres como argumento, o programa deve dizer quantos caracteres de distância faz de um a outro. Por exemplo, se tivermos as letras A e C, o resultado deve ser 2; se tivermos os caracteres D e M o resultado deve ser 9. (Não esquecer que o Brainfuck apenas imprime caracteres da tabela ASCII e não valores, dica útil para apresentar o resultado). Ajuda: a distância não deve ser maior que 9 caracteres; o primeiro caratere deve ser inferior (isto é, ter uma posição menor) que o segundo.
A ideia é que tentem resolver os exercícios através deste tutorial e de alguma pesquisa. No entanto, deixo aqui propostas de resolução dos exercícios se estiverem perdidos ou para comparar com as vossas soluções.
Extensões à implementação do Brainfuck
O modelo original do Brainfuck apenas podia expandir células para a direita e tinha um limite de 30 000 bytes (30 KBytes). Porém, existem propostas de interpretadores que libertam estas restrições.
Células virtualmente infinitas sem limite de expansão
Na implementação original do Brainfuck, é usado um vetor com limite para simular a memória de células. Isto traz os limites já referidos.
No entanto podemos suprimir estes limites, gastando mais alguma memória. Se forem usadas listas duplamente ligadas, ganhamos de imediato duas vantagens:
- A memória de células é expansível para a esquerda e para a direita em todas as situações
- Não existe um limite de células que podemos usar (virtualmente)
O inconveniente desta solução é o aumento de memória por célula. Se, por exemplo, usarmos a linguagem C e uma máquina de 32 bits, para criar um vetor necessitamos de alocar esta quantidade de memória:
1 byte por célula (usa tipo de dados byte)
Vetor de 30000 bytes
=> 30000 células, 30 KBytes
Na solução com listas ligadas, assumindo que apenas criamos nós e estes são geridos pelo interpretador (isto é, não existe nada a indicar inicio e fim de lista; temos sempre um apontador para a célula atual), e assumindo ainda que usamos uma estrutura para o efeito, teríamos algo como:
1 estrutura nó
-> Valor (byte) = 1 byte
-> Apontador para célula anterior = 4 bytes
-> Apontador para célula seguinte = 4 bytes
Total = 9 bytes por célula
=> 30000 células, 30000 * 9 = 270 KBytes
Ou seja, para replicar a implementação original, gastaríamos 9 vezes mais memória.
Eu fiz um pequeno interpretador que implementa esta ideia, podem experimentar e consultar o código no Github (está em linguagem C e funciona em ambientes GNU/Linux): https://github.com/leplastic/brainfuck-interpreter
Notas e referências
Para finalizar este tutorial de Brainfuck, sugiro-vos alguns endereços web onde podem explorar mais sobre a linguagem, das quais recolhi muita informação para criar o artigo: