Intermediate

Um agente de programação pode oferecer um editor decente para uma linguagem totalmente nova?

On this page

Ao final do meu último post, eu listei o que o Slopersons não tem em comparação a uma linguagem real: um gerenciador de pacotes, um debugger, desempenho decente e suporte de editor. Escrever Slopersons no VS Code significava texto branco simples, e descobrir que você escreveu um nome errado apenas quando o programa travava nele.

Então eu quis saber: quão longe um agente de codificação consegue levar o suporte de editor para uma linguagem sem dados de treinamento e sem gramática no Marketplace para copiar? Eu queria uma boa DX, com realce de sintaxe, erros enquanto digita, ir para a definição, completação, hover com documentação, renomear entre arquivos. O resultado foi bom, mas as partes interessantes, novamente, foram as decisões e os momentos em que o agente estava confiantemente errado.

Se você não leu os posts anteriores (como eu construí a linguagem e como agentes a aprenderam do zero): Slopersons é uma pequena linguagem experimental que construí com um agente de codificação, onde os dados fluem da esquerda para a direita e os blocos são pipes na margem:

~ Load a module from another file, then flow values through procedures
"./lib/geometry" -> use => geometry

2.0 -> geometry/circle-area -> io/print-line
[1 2 3] -> array/each n
| n -> io/print-line

A Apresentação

Desta vez, não houve um prompt grande, eu apenas comecei com:

Eu quero planejar o desenvolvimento de uma extensão do VS Code para a linguagem. Vamos começar com o realce de sintaxe

Após cada etapa, o agente me perguntava o que fazer a seguir como uma questão de múltipla escolha, com uma recomendação e as vantagens e desvantagens de cada opção, da mesma forma que fez ao projetar a linguagem. Eu escolhia uma, ele planejava, construía e testava, e então eu testava no VS Code antes de dizer "commit. Próximo!". Tudo levou cerca de sete horas, da primeira mensagem ao último commit, e isso inclui o tempo que passei testando cada etapa eu mesmo.

Take 1: uma gramática e suas limitações

O primeiro passo foi uma gramática TextMate, o formato baseado em regex que o VS Code usa para realce de sintaxe, além de alguns comportamentos do editor: ~ alterna comentários, colchetes fecham automaticamente e pressionar Enter em uma linha | | x inicia a próxima com as mesmas barras. Essa parte é pequena, mas em uma linguagem onde blocos são feitos de barras de margem, é a diferença entre ser agradável e irritante.

Slopersons code highlighted in VS Code

O que eu mais gostei foi como ele testou a extensão. Como ela vive dentro do repositório do interpretador, os testes podem comparar a gramática com a coisa real:

  • a lista de palavras-chave da gramática deve coincidir com a do lexer, e sua lista de módulos integrados deve coincidir com o que o interpretador registra, então adicionar uma palavra-chave ou um módulo sem realçá-lo falha pnpm test;
  • cada token que o lexer real lê em todos os 36 arquivos .slop do repositório deve receber uma cor correspondente na gramática.

Então ele me disse o que uma gramática não consegue fazer:

[1 2 3] -> array/each n | n -> io/print-line

[1 2 3] -> array/each n
| n -> io/print-line

(x > 1 ? "many" : "one") => label
{"name": "Ada"  "born": 1815} => person

O n na primeira linha é colorido como um parâmetro, mas o da terceira linha não é. Uma gramática TextMate funciona uma linha por vez, e o que faz aquele n ser um parâmetro é o bloco na próxima linha. Apenas por aquela linha, ele poderia muito bem ser um argumento. E ambos os : parecem iguais, embora um seja o "else" de uma condição e o outro separe uma chave de seu valor em um hashmap, e distingui-los requer um parser.

Take 2: vamos construir um language server

Para resolver isso, o agente recomendou construir um language server como uma flag slopersons --lsp no executável que você já precisa instalar para rodar programas de qualquer maneira. Dessa forma, o editor usa o mesmíssimo parser que o interpretador, e eles não podem divergir.

O agente é ignorado mais uma vez

Antes de construir o servidor, ele fez três perguntas. Eu segui a recomendação dele em duas delas: começar apenas com erros de análise e recuperar-se de erros por instrução de nível superior, para que um procedimento quebrado não escondesse os erros nos outros.

Na terceira, eu fui contra. O agente recomendou escrever o protocolo à mão, cerca de 200 linhas, para que o interpretador mantivesse suas zero dependências de tempo de execução e ele pudesse escrevê-lo rapidamente. Eu escolhi vscode-languageserver porque não queria recriar o protocolo que o VS Code espera. O próprio cliente do VS Code é testado contra essa biblioteca, e cada novo recurso adicionaria mais protocolo para acertar à mão.

Essa escolha teve consequências:

  • A biblioteca chama process.exit a si mesma quando o editor fecha, e o projeto tem uma regra contra isso. Em vez de esconder, o agente documentou isso como a única exceção – em AGENTS.md e no ADR
  • O executável independente travou no --lsp com o código de saída 70. A biblioteca carrega módulos Node de uma forma que o bundle não suportava, e os testes de bundle existentes apenas executavam programas, então eles nunca notaram. O agente descobriu porque executou a mesma sessão de protocolo contra o checkout, o bundle e o executável

Um passo de cada vez

Depois disso, cada passo foi um commit, um ADR e uma rodada de eu testando:

  1. Tokens semânticos do parser, corrigindo as duas cores que a gramática errou
  2. Um resolvedor de nomes, para "ir para a definição", "encontrar referências" e destacar os outros usos de um nome
  3. Avisos para nomes desconhecidos, enquanto você digita
  4. Preenchimento automático
  5. Seguir use para arquivos de módulo, para que F12 em geometry/circle-area abra o módulo em proc circle-area
  6. Hover e ajuda de assinatura, com documentação integrada
  7. Um esboço e renomeação

A parse error and an unknown-name warning in VS Code

Vou dar mais detalhes sobre alguns deles:

O resolvedor seguiu a regra errada

Para saltar para uma definição, o servidor precisa saber a que cada nome se refere sem executar o programa. O primeiro resolver do agente permitia que cada nome visse cada declaração em seu escopo, em qualquer ordem. Então, ele quebrou o código de propósito para verificar seus testes (o hábito de mutation testing mencionado no post anterior) e descobriu que nenhum teste notou a diferença.

Investigando, ele descobriu que a regra em si estava errada: dentro de um procedimento, uma leitura que vem antes de um => x local apontaria para esse local, enquanto o programa na verdade lê o x externo naquele ponto. Então, ele mudou para a resolução de nomes na ordem das instruções, da forma como o programa as executa, e adicionou testes para ambos os comportamentos.

A ordem das instruções ainda é uma aproximação, porém: um loop pode ler um nome que seu próprio corpo vincula posteriormente, o que funciona a partir da segunda vez. É por isso que nomes desconhecidos tornaram-se avisos em vez de erros: o editor pode estar errado, e um erro em um código válido é pior do que um aviso ignorado. Eles também usam exatamente as mesmas mensagens que o interpretador imprime quando o programa é executado.

O que?!

Durante a etapa de avisos, o resumo do agente disse que todo programa no repositório recebe zero avisos, "exceto totl no programa golden que espera por isso", e eu tive que perguntar o que diabos aquilo significava.

Tenho certeza de que você já passou por esse tipo de redação confusa antes também; o agente estava tentando dizer algo que não fazia sentido e o contexto era super confuso. Após alguns segundos frustrantes, ele explicou que usar uma variável chamada "totl" dentro de um arquivo onde ela nunca é criada mostraria o erro. Eu era sequer esperado que entendesse isso?

Completar uma linha que não pode ser analisada

A conclusão tem o mesmo problema que tudo mais em um editor: a linha que você está digitando geralmente não é analisada, e o parser descarta a instrução inteira, muitas vezes todo o procedimento ao redor do seu cursor. O truque do agente foi substituir a palavra que estava sendo digitada por um nome de marcador e perguntar ao resolver o que está visível ali. Se isso ainda não for analisado, ele tenta novamente sem o restante da linha, depois apenas com as barras de margem, depois ao final do arquivo. Com isso, ao digitar io/, ele lista os membros do módulo diretamente do interpretador, então um novo builtin aparece no editor sem mexer no código do editor.

Renomear symbols

Na primeira versão do recurso de renomeação, ele se recusava a renomear nomes exportados porque outros arquivos os usavam, mas eu queria renomear as exportações também! A implementação agora escaneia os arquivos .slop no workspace e renomeia o uso em cada arquivo cujo use aponta para esse módulo.

Renaming an exported name across files from the imported file

Renaming an exported name across files from the importer

O momento em que precisei intervir

No final, pedi ao agente para verificar se todas as docs estavam atualizadas. Cada etapa individual tinha "atualizado as docs", e mesmo assim ele encontrou muitos problemas em múltiplas docs. docs/cli.md disse que os membros de nomes qualificados não têm definição, algumas linhas depois de dizer que F12 em geometry/area abre o módulo. AGENTS.md chamou o servidor de "a única dependência de runtime", quando havia duas, e não listou os oito módulos do editor adicionados depois disso. O README da extensão listava apenas as cinco primeiras funcionalidades.

Cada etapa atualizou o parágrafo sobre sua própria funcionalidade, e ninguém releu tudo, então adicionei uma regra ao AGENTS.md

: uma alteração não está pronta até que cada documento que descreve o que foi tocado diga o que o código agora faz, no mesmo commit, com a lista de documentos para verificar. And os ADRs fizeram seu trabalho novamente: em certo ponto, o agente editou um ADR antigo para dizer que uma limitação havia sumido, depois reverteu a alteração por conta própria, porque AGENTS.md diz que ADRs existentes apenas mudam seu status.

Então, um agente de codificação consegue dar um editor decente para uma linguagem novinha?

Eu diria que sim! Em algumas horas, Slopersons passou de texto simples para realce de sintaxe, erros e avisos enquanto você digita, ir para definição, encontrar referências, autocompletar, hover com documentação, ajuda de assinatura, um outline e renomear em vários arquivos. Foram 11 commits, cerca de 1.400 novas linhas no código-fonte do interpretador, 1.500 linhas de testes, 9 ADRs e uma suíte de testes que terminou o dia com 1.022 testes passando.

O que podemos aprender com isso?

  1. Verifique entre o que você está realmente escolhendo. Antes de construir o language server, tive que pensar em uma escolha: "tokens semânticos ou um language server?". Acontece que não era uma escolha real, o que só entendi depois que o agente explicou o porquê. As melhores respostas que recebi foram as que questionaram a pergunta
  2. Teste contra a fonte da verdade. A gramática é verificada contra o lexer, o servidor é o próprio parser do interpretador, os avisos usam as mensagens do interpretador e o hover lê a documentação real. Cada um desses pontos transformou divergências em um teste falho
  3. Passos pequenos, cada um testado de verdade. Uma funcionalidade, um commit, uma rodada de eu usando. O agente pode testar o servidor, mas alguém tem que abrir o editor
  4. Leia a documentação como um todo de vez em quando. Documentações atualizadas um parágrafo por vez são individualmente corretas e, juntas, se contradizem. Coloque a regra onde o agente irá lê-la

Nada disso é novo, certo? Testes contra a coisa real, pequenos passos e documentos que condizem com o código são o que sempre quisemos para humanos, e o agente se saiu melhor onde quer que o projeto permitisse que fossem verificáveis.

Mas note, ainda é um teste pequeno e limitado: testei a extensão manualmente no VS Code, na minha máquina, com programas pequenos. A extensão em si não possui teste automatizado dentro do VS Code, não está no Marketplace e, embora o servidor fale LSP, não a tentei em nenhum outro editor. O resolver funciona por ordem de instrução, que é uma aproximação de como o programa roda, então alguns avisos estarão errados.

Se quiser tentar, a extensão fica no repositório da linguagem, em editors/vscode disponível no GitHub.

Encerrando

Comecei a questionar como um agente de codificação se sairia com uma linguagem desconhecida e, conforme o agente me permitia ousar cada vez mais, consegui ir além do que poderia ter imaginado no início.

Obrigado por me acompanhar em toda essa aventura, e nos vemos em outra hora!

We want to work with you. Check out our Services page!

We want to work with you

The engineers who write here are the same ones who join client teams. Let’s build something that grows with your business.

Book a call