- Lone Lisp é um Lisp que roda diretamente sobre chamadas de sistema do Linux, sem
libc; Matheus Moreira o iniciou para verificar se seria possível criar uma linguagem completa e programas de espaço de usuário usando apenas C freestanding - Ao usar diretamente a estável ABI de espaço de usuário do Linux, ele removeu estados globais e APIs legadas da biblioteca C, como
errno, locale e buffering implícito de arquivos, e implementou por conta própria o runtime, o alocador de memória e as ferramentas de teste - Oferece suporte a FEXPR e otimização adequada de chamadas de cauda, além de geradores, continuações delimitadas e tratamento de erros retomável; vetores, tabelas e continuações também são tratados pelo mesmo mecanismo de chamada de função
- A inicialização é rápida, mas em benchmarks provisórios é um interpretador em nível de listas 10 a 100 vezes mais lento que Python; ele pretende manter o interpretador em C como referência de bootstrap e, no longo prazo, implementar um compilador JIT no próprio Lone
- Moreira usa Claude para revisão de código e gerenciamento de projeto, mas escreve e revisa pessoalmente o código do Lone; seu objetivo é criar um gerador de site estático, um shell e utilitários para reconstruir o espaço de usuário tradicional do Linux
Dos games a C++, Ruby e Lisp
- O que despertou seu interesse por programação foi Mega Man Battle Network; para interagir em fóruns relacionados, aprendeu inglês e também sua primeira linguagem, C++
- Começou um curso de informática por volta dos 13 ou 14 anos em uma escola técnica no Brasil
- Usou Dev-C++ e os tutoriais do cplusplus.com
- Em 6 meses, aprendeu todo o currículo da escola e foi contratado como monitor para ajudar outros alunos
- Também tentou criar jogos, mas não teve sucesso por falta do conhecimento de física necessário
- Depois de C++, passou por Java, Ruby e Python até chegar a Lisp e Scheme, sendo atraído pela elegância linguística de Scheme
- Aprendeu programação orientada a objetos lendo quase todo o tutorial de Java da Sun
- Gostava de Java Swing e do design Nimbus, e chegou a criar um app para desenhar círculos e arcos para um professor de matemática
- Em Ruby, escreveu gems, incluindo Acclaim, um parser de argumentos baseado em comandos no estilo Git
- A motivação para explorar várias linguagens começou em projetos escolares, mas depois passou a ser movida pela curiosidade e pelo desejo de encontrar a linguagem certa para si
A expressividade de Ruby e a transparência de C
- Ruby combinava bem com seu modo de pensar: muitas vezes dava para adivinhar o nome de um método e acertar, e a linguagem suportava formas no singular, no plural e sinônimos
- Com composições próximas de pipelines de shell, como
items.each.with_index.map, é possível fazer muito com pouco código - Ele ainda mantém Ruby sempre instalado e recentemente também iniciou um projeto Rails
- Com composições próximas de pipelines de shell, como
- Em C, as funções correspondem diretamente a símbolos ELF e código assembly, tornando o resultado da compilação fácil de entender
- Ele considera que o C++ moderno se tornou, na prática, uma linguagem diferente da antiga, e já não diz mais que conhece C++
- Tentou migrar para Rust, mas não se fixou; também se interessa por Zig e por seu criador, que justificam reescritas em C com velocidade e ABI
- Na adolescência, lendo o código-fonte da VM do CRuby, encontrava na implementação respostas para perguntas do Stack Overflow como “por que Ruby se comporta assim?”
- Depois explorou os códigos de CRuby, CPython, JVM, Guile e V8, avaliando especialmente o código-fonte do kernel Linux como referência de programação C profissional
Como lê código de outras pessoas
- Ele não se limita a usar bibliotecas: lê código-fonte continuamente para entender implementações internas, a comunicação entre drivers e hardware e camadas ocultas
- Mesmo ao criar aplicações reais, frequentemente acabava desviando para investigar a tecnologia de base, e desenvolveu uma tendência a refazer melhor problemas já resolvidos
- Por concluir que essa tendência não combinava com uma carreira comum em desenvolvimento de software, decidiu não escolher programação como profissão e mantê-la como hobby
- Ele não entendia código de drivers desde o começo; continuou lendo apesar da frustração até conseguir compreender parte do funcionamento
Engenharia reversa de um teclado RGB
- O aplicativo Windows para controlar o teclado RGB de um notebook barebone Clevo era tão lento que levava mais de 1 minuto para abrir a janela, então ele criou um substituto
- Com Wireshark, capturou os pacotes USB que o app enviava ao teclado e analisou sua estrutura para escrever um driver em C no espaço de usuário do Linux
- Depois de colocar no GitHub, esqueceu o projeto, mas usuários reais apareceram; um deles até criou uma GUI
- Em seguida, fez engenharia reversa de outros recursos do notebook, como gerenciamento de energia via ACPI, embora não tenha descoberto todas as funções
liblinux contornando a libc
- Chamadas de sistema do Linux têm código simples o bastante para se parecerem com chamadas de função comuns, permitindo mirar diretamente no Linux sem passar pela
libc - Linux é conhecido pela estabilidade da ABI de espaço de usuário, mas ele considera que os componentes de espaço de usuário não têm a mesma estabilidade
- Encontrou no manual a observação de que algumas chamadas de sistema não eram suportadas pela glibc
- Também foi influenciado por um caso da LWN segundo o qual
getrandomlevou cerca de 2 anos para entrar na glibc
- A partir da ideia de criar uma
liblinux.acontendo apenas chamadas de sistema e vinculá-la com-llinux, desenvolveu a liblinux- Era uma coleção de wrappers de chamadas de sistema do Linux para C freestanding
- O runtime C (CRT) se refere a bibliotecas C padrão como glibc, musl e uclibc
Problemas que desaparecem em C freestanding
- Linux retorna erros como valores de retorno comuns, mas a biblioteca C os armazena na variável global
errno - Ao remover a
libc, é possível evitar vários estados globais e APIs antigaserrno,rand/srand, estado de localestrtok, que não é reentrante- buffering implícito de arquivos, que pode causar problemas inesperados
gets,atexit,signal,mallocmath.h, que se torna complexo ao se combinar com NaN eerrno
- Por outro lado, as regras de promoção de inteiros e a ausência de verificação de overflow são problemas do próprio C e permanecem
- Para overflow, ele usa recursos embutidos do compilador
- Também se mantém atento à possibilidade de criar novos defeitos por conta própria
- Sem biblioteca padrão, foi necessário escrever tudo, de cópia de bytes a conversão de inteiro para string, mas a tarefa não foi tão impossível quanto parecia
- O repositório da glibc era difícil de interpretar, mas o código-fonte da
musl libcera limpo e fácil de explorar, ajudando bastante - No Lone, também implementou uma suíte de testes automatizados que usa apenas memória estática, sem biblioteca C nem alocação dinâmica de memória
- O repositório da glibc era difícil de interpretar, mas o código-fonte da
A descoberta do nolibc e o início do Lone Lisp
- Ele perguntou a Greg Kroah-Hartman por que o Linux não tinha uma biblioteca C oficial, e recebeu como resposta que no passado existiu o projeto klibc
- Depois descobriu que desenvolvedores do kernel Linux haviam criado nolibc.h para ferramentas internas
- Considerou que era mais completo e usável que sua liblinux, então arquivou o projeto
- Hoje,
nolibc.hcresceu dentro da árvore do kernel e virou um diretório próximo de umalibctemporária, mas não há garantia clara de estabilidade externa
- Como projeto seguinte, iniciou Lone Lisp
- Ele pensou que, se fosse possível criar um Lisp freestanding que roda diretamente no Linux, qualquer programa poderia ser escrito do mesmo modo
- É um experimento para remover o complexo espaço de usuário moderno do Linux e recomeçar sobre a estável interface binária de chamadas de sistema
- Também enviou patches ao Cosmopolitan, incluindo uma implementação de parsing de ponto flutuante
- Ele valoriza o formato APE, interpretado simultaneamente como formatos executáveis de várias plataformas, e o recurso de arquivos embutidos dentro do executável
Tratamento de erros e continuações delimitadas
- No início do Lone, todos os caminhos de erro encerravam o interpretador com
exit(255), mas ele implementou um sistema de condições retomáveis ao estilo Common Lisp - A base do tratamento de erros são continuações delimitadas
- Ele entende continuações como a cópia de frames da pilha e sua reprodução em outro lugar
- Também vê exceções como uma forma de continuação que não captura valores
- Tipos de erro são mapeados de símbolos como
'not-foundpara lambdas(lambda (error) ...), com um argumento, funciona como um tratador de exceções comum(lambda (error continuation) ...), com dois argumentos, pode chamar a continuação e retomar o código que errou com um valor
- Ele optou deliberadamente por não adotar uma API de restart em que o código que sinaliza o erro enumera opções de reinício, como
use-valueem Common Lispsignal,throwetransfersão estruturados como mecanismos básicos internos do avaliador, de modo que se comportem como se simplesmente tivessem retornado um valor
FEXPR e elementos da linguagem chamáveis
- Ele também evita o modelo em que um avaliador Lisp tradicional trata diretamente formas especiais como
if - Lone suporta FEXPR, equivalente a plugins do avaliador, separando formas especiais para fora do avaliador
- Com esse design, a otimização de chamadas de cauda para funções Lisp puras foi possível primeiro, mas primitives tiveram bugs por algum tempo
- O problema foi resolvido adicionando um mecanismo separado para que primitives pudessem retornar em posição de cauda
- Vetores e tabelas também são chamáveis
([1 2 3] 0)retorna1([1 2 3] 0 4)altera o vetor para[4 2 3]
- Continuações e geradores seguem a mesma convenção de chamada
controlé parecido combegin, mas com suporte a continuaçõestransfersai do blococontrolmais próximo e retorna uma continuação chamável que representa o ponto de suspensão- Ao chamar essa continuação com um valor, a computação suspensa pode ser retomada com outro valor de retorno
Chamadas de sistema e estrutura de execução
- Antes do Lone iniciar, o processo pai chama
execve, e o Lone faz cerca de 12 chamadas ammappara seu gerenciamento interno de memória - Ele lê um script da entrada padrão, executa e encerra; durante a execução, podem ocorrer chamadas adicionais a
mmap,munmapemremap - O sistema de módulos usa
openateclose - Das mais de 300 chamadas de sistema do Linux, o próprio Lone usa apenas algumas, mas oferece uma primitive de chamada de sistema no nível Lisp para que programas possam chamar as demais
- Para chamadas como
ioctl, que exigem structs C do kernel, será preciso adicionar recursos de criação e manipulação de structs- Consultar o tamanho do terminal é um exemplo que exige esse suporte
O espaço de usuário que pretende criar com Lone
- No nível da linguagem, ele implementou tratamento de erros, otimização adequada de chamadas de cauda, geradores e continuações delimitadas, e no longo prazo quer também incorporar ideias de Erlang/BEAM
- A estrutura está sendo preparada para que vários interpretadores possam executar código Lisp em paralelo
- O primeiro programa que ele pretende criar com Lone é um substituto para seu atual gerador de site estático baseado em um fork do PugJS
- Depois, planeja escrever seu próprio shell e utilitários para recriar o espaço de usuário tradicional do Linux à sua maneira
- Ele também criou uma implementação que exporta
envcomo uma tabela Lone Lisp; na época não havia recurso de iteração, mas hoje há suporte a geradores - O interpretador também tem um recurso para ler e executar código Lisp de um segmento ELF especial
- Ao copiar o binário do interpretador e inserir código Lisp, é possível criar um programa independente
Revisão e gerenciamento de projeto com Claude
- Embora não haja limite para o volume total de trabalho, ele considera que o conjunto de recursos da linguagem em si está se aproximando da conclusão
- Usa Claude como uma espécie de gerente, e a maior ajuda vem em revisão de código e gerenciamento de projeto
- Pede que ele examine toda a base de código de forma rigorosa em busca de problemas, áreas não desenvolvidas e direções futuras
- Abordagens rejeitadas e recursos a tratar depois são gerenciados em documentos e na memória do projeto
- O coletor de lixo e o alocador de memória melhoraram bastante porque Claude sugeriu algoritmos melhores
- Ele revisa pessoalmente cada linha que entra no Lone e não confia em IA para escrever código do Lone em seu lugar, pois também vê a escrita do código como educativa
- Por outro lado, em um gerador de site estático em JavaScript, já deixou Claude escrever um recurso inteiro
- Entre os recursos planejados estão suporte às APIs dos compiladores GCC/Clang e funções de proteção de pilha
Sintaxe trazida de Ruby e Scheme
- Ele tentou combinar a expressividade de Ruby com a concisão de Scheme; como o parser de Lisp é simples, pôde escrevê-lo por conta própria e manipular listas aninhadas diretamente
- O momento em que realmente entendeu Lisp foi ao implementar FEXPR, em que a função recebe a própria lista, não valores avaliados
- Arrays usam a sintaxe
[1 2 3], e dicionários usam{k v}- Isso reduz barreiras de entrada desnecessárias por ser familiar não só para usuários de Clojure e newLisp, mas também de Ruby, Python e JavaScript
- Seu estilo de codificação em C adapta o estilo do kernel Linux, mas com maior preferência por alinhamento e ornamentos
- Em Ruby, prefere reduzir pontuação e escrever mais próximo de frases em inglês, como
method 'arg'em vez demethod('arg') - Em Lisp, também reduz o aninhamento sintático
- O
letdo Lone tem a forma(let (a b c d ...) body...) - Ele considera que nomes e valores aparecendo em sequência é mais natural para a aplicação de funções do que o formato tradicional
(let ((a b) (c d) ...) body...)
- O
Desempenho e plano de bootstrap
- O benchmark em que o Lone se destaca é a velocidade de inicialização: ele consegue iniciar e encerrar um programa mais rápido do que Ruby leva para ler Rubygems
- Em medições provisórias, é cerca de 10 a 100 vezes mais lento que Python
- Lone é um interpretador em nível de listas parecido com a forma do capítulo 5.4 de SICP
- Python e Ruby são interpretadores de bytecode, portanto pertencem a outra categoria de desempenho
- Ele evita compilar para bytecode porque sente que transformar listas em bytecode faz o Lisp perder sua essência
- No longo prazo, pretende manter o interpretador em C como referência final de bootstrap e escrever um compilador JIT dentro do Lone
- A abordagem seria obter páginas executáveis com
mmape então gerar instruções
- A abordagem seria obter páginas executáveis com
- Em uma comparação provisória de Fibonacci recursivo com Bash, Lone foi muito mais rápido, mas ele reconhece distorções de medição, como a possibilidade de Bash criar um processo a cada iteração
- Em scripts, o tempo de inicialização é importante, então o bootstrap rápido do Lone pode ser útil
- Se tabelas hash forem pré-computadas na etapa de build e transformadas em dados de inicialização em C, o custo de inicialização em tempo de execução pode cair ainda mais
Memória e ambiente mínimo
mmapsolicita páginas de memória ao kernel, e o tamanho comum de página é 4.096 bytes- É possível especificar permissões de leitura, escrita e execução, e JIT usa páginas executáveis
- Mesmo que sejam necessários apenas 16 bytes, pedir 4 KiB gera muito desperdício; por isso, alocadores comuns obtêm uma região grande, como 128 MB, e a dividem em pedaços pequenos
- O ambiente de nível superior do Lone tem apenas os dois símbolos
importeexport- Além de listas, literais e mecanismo de chamada de função, até
ifequoteprecisam ser importados - É um design para que o usuário controle diretamente todos os nomes do ambiente
- Além de listas, literais e mecanismo de chamada de função, até
- Atualmente há cerca de 50 funções, com foco em
if,let,begin,control/transfer,intercept/signal,flattene outras
Biblioteca padrão e documentação
- A documentação da linguagem ainda está incompleta, mas o trabalho em biblioteca padrão está se aproximando mais que novos recursos da linguagem
- Ele planeja tomar como referência a biblioteca padrão de Ruby e a convenção de nomes de Scheme
- A próxima tarefa é um protocolo de iteração
- Considera a forma
(for (each collection) (lambda (item) ...)) - As funções
eachexistentes serão redesenhadas para todas retornarem geradores
- Considera a forma
- Depois, pretende implementar manipulação de strings e um string builder para concatenação eficiente
- Quando iteração e recursos de string estiverem prontos, ele acredita que também poderá escrever um framework web no estilo Hiccup
- Ele se preocupa com a possibilidade de perder foco quando as funções de baixo nível terminarem e o trabalho passar para tarefas repetitivas de biblioteca padrão, e pretende usar a ajuda de gerenciamento de Claude
O objetivo de possuir diretamente a stack inteira
- Mesmo recursos que pareciam misteriosos, como continuações, viram problemas comuns de implementação quando entendidos como cópia de frames da pilha; o objetivo do Lone é estimular outras pessoas a criarem e explorarem seus próprios sistemas
- Ele quer deixar de ser apenas consumidor de bibliotecas e se tornar criador de bibliotecas, usando diretamente até chamadas de sistema que parecem tratadas como proibidas para desenvolvedores comuns
- Ter um sistema próprio implica consertá-lo pessoalmente quando quebrar, mas ele espera que mais pessoas escolham essa responsabilidade e suas recompensas
- Ele acredita que não é preciso temer o código antigo acumulado na computação e que camadas desnecessárias podem ser removidas
- Talvez quem refaça o espaço de usuário do Linux não seja ele, mas desenvolvedores Rust ou Zig
- Mesmo que Lone não tenha sucesso, ele ficará satisfeito se inspirar tentativas desse tipo
Software livre e Android
- Ele valoriza o fortalecimento dos direitos do usuário buscado pelo software livre e se preocupa com a redução dos computadores de uso geral a eletrodomésticos gerenciados por empresas ou governos
- Apesar de ter adotado uma abordagem pesada em alguns aspectos, systemd é visto por ele como um caso bem-sucedido de reimaginar e reconstruir o espaço de usuário tradicional do Linux
- Ele acredita que é preciso escolher as batalhas, em vez de construir pessoalmente todas as camadas
- Se Lone tivesse sido feito como um Lisp bare-metal, talvez nem chegasse à etapa de boot
- Cita como exemplo o Sector Lisp de jart, um Lisp de setor de boot com menos de 512 bytes
- Lone começou dentro do Termux, e uma parte considerável foi escrita no celular
- Embora Android bloqueie algumas chamadas de sistema, incluindo
statx, Lone roda no Android e a suíte de testes passa 100%
Médico e programador não profissional
- Moreira é médico, formado em medicina em 2019
- No ensino médio, interessou-se por biologia ao ler sobre programação de redes neurais
- Sua rotina permite reservar tempo para programar: em alguns dias atende por 10 a 14 horas, em outros folga
- Em hospitais que usam prontuário eletrônico, reduziu muito o trabalho com templates e habilidade de digitação
- Consegue registrar informações mantendo contato visual com o paciente
- O projeto Rails que iniciou recentemente é um sistema de prontuário eletrônico para resolver suas próprias necessidades
- Ele aponta a falta de interesse pelo próprio computador como uma das razões pelas quais desenvolvedores profissionais não resolvem diretamente problemas pessoais
- Para investigar implementações por dentro, é preciso ter curiosidade, não apenas a atitude de querer obter um resultado; ele ainda não sabe como despertar essa curiosidade nas pessoas
1 comentários
Comentários no Lobste.rs
Gosto tanto dessa série de entrevistas que leio todas sem falta, e é muito interessante conhecer a relação pessoal que cada um construiu com a computação
Shonumi, autor do emulador GBE+ GBA, também foi influenciado por Mega Man Battle Network como eu, e espero poder ler uma entrevista com ele algum dia
Exceções reiniciáveis não exigem necessariamente continuations de primeira classe (first-class continuations)
Common Lisp oferece exceções reiniciáveis mesmo sem continuations, e o ponto principal é não desenrolar a pilha antes de chamar o handler
Assim, a pilha e o estado em que a exceção ocorreu são preservados, e o handler pode retornar normalmente ou saltar para algum reinício em algum ponto da pilha, como no Common Lisp
Ainda assim, no Lone Lisp isso de fato é necessário, porque a continuation retornada pode ser chamada várias vezes e pode escapar para fora do manipulador de sinal Como geradores são a base do processamento iterativo, o desempenho é mais importante, então usei uma pilha separada em vez de continuations delimitadas para eliminar cópias
Como tratamento de erros é um caminho relativamente raro, decidi preservar toda a funcionalidade
Para muita gente, o trabalho ou projeto mais importante pode ser justamente aquilo que faz no tempo livre, sem remuneração e por diversão
Isso me faz imaginar que até o meu médico poderia estar levando uma vida dupla secreta, criando sua própria linguagem Lisp
O Dr. Cameron Kaiser também tocou projetos de peso, compartilha uma filosofia sobre propriedade das máquinas e usa no dia a dia uma workstation POWER9 totalmente aberta
Foi a primeira vez que ouvi falar de Lone Lisp, e parece um projeto muito legal
Eu também comecei a aprender C++ por volta dos 13 ou 14 anos, usando o velho Dev-C++ nas aulas de informática, e isso me trouxe uma nostalgia enorme
Eu jamais esperaria que ele fosse médico, e isso me lembrou Con Kolivas
Pelo visto, há bem mais médicos-programadores do que eu imaginava
https://en.wikipedia.org/wiki/Con_Kolivas