Ensinando um programa em C a mostrar o raciocínio
O projeto de rotas começou como um trabalho de 2023 que fiz com dois colegas: ler um mapa, achar a rota mais rápida e replanejar quando o trânsito muda no meio do caminho. É C puro. Você dá um arquivo e ele devolve três linhas: o caminho, os quilômetros e o tempo.
Para o professor corrigir, essa resposta basta. Para uma página web, não serve para nada. Eu queria que o leitor visse o Dijkstra abrindo e fechando nós, o carro saindo, uma via travando e o carro mudando de ideia. O C sabia tudo isso enquanto rodava. Só nunca falava.
Reescrever o algoritmo em JavaScript transformaria o C num link para o repositório, e animações gravadas tirariam do leitor a chance de criar o próprio engarrafamento. Então o C ia ter que aprender a narrar.
Ganchos que não custam nada quando ninguém escuta
O C ganhou uma struct de ponteiros de função, um por evento, todos NULL por
padrão:
typedef struct {
void (*pop)(int node, double time);
void (*relax)(int from, int to, double new_time);
void (*plan)(const int *path, int length, double eta);
void (*move)(int from, int to, double t0, double t1);
/* ...update, replan, unreachable, done */
} TraceHooks;
O dijkstra e o laço de replanejamento só chamam cada um se ele estiver
preenchido. O programa de linha de comando original nunca preenche, então para
ele a mudança custa umas poucas comparações com NULL.
Antes de mexer em qualquer linha, salvei a saída do programa para alguns mapas de teste como resultado esperado. Qualquer mudança dali em diante tinha que reproduzir esses arquivos exatamente. Na primeira execução dessa base, um deles saiu assim:
1;2;4;6
3.000000
00:02:60.000000
Dois minutos e sessenta segundos. A rota está certa, são exatamente 180
segundos. Em ponto flutuante a soma dá algo como 179,99999, o código trunca com
(int) para tirar os minutos, chega em 2, e imprime o resto, que o printf
arredonda de volta para 60. Isso vinha sendo impresso desde 2023. A correção
arredonda para milissegundos antes de quebrar o tempo em partes. Foi a única
linha da base que o projeto teve permissão de mudar.
Uma linha por evento
Com os ganchos no lugar, uma função nova, run_trace, imprime uma linha para
cada coisa que acontece. Esta é a saída real para o mapa da página do projeto,
onde a via 2→4 cai para 5 km/h aos 30 segundos:
pop 1 0.000
relax 1 2 60.000
relax 1 3 90.000
pop 2 60.000
relax 2 4 120.000
relax 2 5 210.000
...
plan 1 2 4 6 180.000
move 1 2 0.000 60.000
update 2 4 5.000 60.000
replan 2 60.000
pop 2 0.000
...
plan 2 5 6 180.000
move 2 5 60.000 210.000
move 5 6 210.000 240.000
done 240.000 4.000
Repare nos tempos depois de replan 2 60.000. O pop 2 seguinte diz 0.000,
o plano novo diz que a viagem leva 180.000, mas o carro chega em 240. Os dois
estão certos. Quando o carro replaneja, o Dijkstra recomeça do nó 2 com o
relógio zerado, então tudo que ele imprime é relativo àquele momento. Já
move, update e done usam o relógio de verdade. Minha própria spec errou
isso: ela tinha o segundo plano em 240.000, e só corrigi depois que o C de
verdade imprimiu 180. O JavaScript guarda o relógio do último replan e soma
em todo evento do Dijkstra que vem depois.
O trace também mostra coisas que o código não diz em lugar nenhum. O nó 5 é
relaxado duas vezes, com 210 e depois com 156, porque o nosso heap não tem
decrease-key: ele só insere o nó de novo e deixa a cópia velha na fila. E o
pop 4 720.000 aparece depois de o destino já estar fechado, porque o laço não
para antes. Os dois comportamentos sempre estiveram no código. Eu só vi quando
eles apareceram impressos, um por linha.
Lendo um arquivo que não é arquivo
O programa original lê o mapa com fscanf de um FILE *. Uma página web não
tem arquivo nenhum para entregar, e eu não queria um segundo parser só para o
navegador. O fmemopen resolveu: ele abre uma string como FILE *, então o
run_trace recebe o mapa como texto e passa para as mesmas três funções de
leitura que o programa de linha de comando usa. O único custo foi compilar com
-std=gnu99.
Para o navegador, o Emscripten compila todos os arquivos menos o main.c. Sem
main, o módulo carrega e fica esperando. A página chama run_trace com o
mapa em forma de string, junta tudo que ele imprime pelo callback print do
módulo e recebe a execução inteira como texto. São 49.687 bytes de
WebAssembly, carregados uma vez por página, e uma execução nesses mapas leva
poucos milissegundos. Quando você cria um engarrafamento no editor de trânsito,
a página monta uma entrada nova e chama o C de novo.
Entrada de desconhecidos
Antes do editor de trânsito, a única entrada que esse programa já tinha lido eram arquivos que nós três escrevemos. Agora ele lê o que qualquer leitor monta clicando. Três coisas quebraram na hora:
- o
read_updateslia cada linha numchar aux[50], então uma linha comprida escrevia além do fim do buffer; - uma atualização para uma via que não existe andava a lista de adjacência até passar do fim e derrubava o programa;
- um destino inalcançável também derrubava, em vez de avisar.
Teve uma quarta, mais quieta. O dijkstra dimensiona os vetores pelo número de
nós da primeira linha do arquivo, então um cabeçalho dizendo que o mapa tem um
bilhão de nós estouraria a pilha. O run_trace recusa qualquer coisa acima de
um milhão.
Nenhum desses era bug para o trabalho da faculdade, porque a gente nunca deu arquivo ruim para ele. Hoje cada um tem um teste, e a saída do programa para os mapas originais continua batendo byte a byte com a base, fora aquela linha.
Reproduzindo
No lado do JavaScript, o trace passa por um reducer que transforma cada evento num retrato completo: quais nós estão abertos ou fechados, o plano atual, onde está o carro. Todos são calculados de uma vez, então tocar, pausar, avançar um passo e mudar a velocidade são só uma posição num array. O canvas lê as cores das variáveis CSS do site a cada quadro, e é por isso que o grafo acompanha a troca de tema claro/escuro no meio da animação sem código extra.
O C em si ainda imprime três minutos como 00:03:0.000000, com os segundos
sem o zero à esquerda. O README documenta isso agora, e vou deixar assim.