2 pontos por GN⁺ 2024-11-03 | 1 comentários | Compartilhar no WhatsApp
  • Ao lidar com 42 linguagens para criar um novo realce de sintaxe para o llamafile, ficou claro que até o lexing simples esconde exceções por linguagem e gramáticas antigas em toda parte
  • A implementação usa C++ e GNU gperf para acelerar a busca de palavras-chave, e o tratamento centrado em strings, comentários e palavras-chave é resolvido com um laço for e uma máquina de estados finitos baseada em switch
  • O trigraph de C, os terminadores de linha u2028 e u2029 do JavaScript, os heredocs de Shell, Perl e Ruby, e a interpolação de strings em Kotlin, Scala, TypeScript e Swift reaparecem como casos difíceis de tratar apenas com lexing
  • Em linhas de código de implementação, FORTH fica em 125 linhas, enquanto Ruby chega a 1042; em Ruby, o operador <<, heredoc e backquote entram em conflito, o que torna especialmente difícil realçar sem parsing
  • O novo realçador do llamafile foi demonstrado no Windows 10 com o Meta LLaMA 3.2 3B Instruct e também pode ser executado em macOS, Linux, FreeBSD e NetBSD, diferenciando-se do ollama por oferecer realce de sintaxe

Motivo para criar o realçador do llamafile

  • Para criar um novo realce de sintaxe para o llamafile, foram estudadas 42 linguagens de programação ao longo de um mês
  • O suporte inclui Ada, Assembly, BASIC, C, C#, C++, COBOL, CSS, D, FORTH, FORTRAN, Go, Haskell, HTML, Java, JavaScript, Julia, JSON, Kotlin, ld, LISP, Lua, m4, Make, Markdown, MATLAB, Pascal, Perl, PHP, Python, R, Ruby, Rust, Scala, Shell, SQL, Swift, Tcl, TeX, TXT, TypeScript e Zig
  • Essa lista cobre a maior parte do TIOBE Index, mas Scratch foi excluída do escopo porque usa blocos em vez de texto

Método de implementação: gperf e máquina de estados finitos

  • O maior gargalo em um realçador de sintaxe básico costuma aparecer ao determinar se um token é uma palavra-chave, quando aumentam as comparações repetidas de strings
  • Foi usado C++ com GNU gperf para gerar uma tabela hash perfeita
    • Um exemplo de entrada do gperf define constantes de Java como true, false e null como palavras-chave
    • O arquivo C gerado pelo gperf pode criar uma função de hash que considera apenas um único caractere para consultas sem colisão
  • O realce de C consegue processar cerca de 4.000 palavras-chave a 35 MB/s graças ao gperf
  • O restante do processamento, em sua maior parte, pode ser resolvido com uma máquina de estados finitos
    • Dá para criar um realçador básico apenas com laços for e switch, sem flex, bison nem ragel
    • Se o foco estiver em strings, comentários e palavras-chave, em geral o tratamento fica no nível de lexing
    • Para destacar elementos como nomes de função em C, pode ser necessário parsing de verdade
  • highlight_ada.cpp é usado como implementação de exemplo

Demo do llamafile e ambiente de uso

  • O novo realçador e a interface de chatbot melhoraram a usabilidade do llamafile, e o realce de sintaxe vira um diferencial em relação ao ollama
  • A demo foi executada no Windows 10 com o modelo Meta LLaMA 3.2 3B Instruct
  • Esse llamafile também pode ser executado em macOS, Linux, FreeBSD e NetBSD
  • Com a melhora da qualidade de modelos de pesos abertos como gemma 27b it, diminui o incentivo para usar Claude

C: exceções de lexing diferentes da imagem de linguagem simples

  • Apesar da impressão de simplicidade, C tem elementos lexicais bastante peculiares
  • O trigraph permite substituir caracteres como #, [, \\, ^, {, |, }, ~ por ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??-
    • Ele foi removido no padrão C23, mas compiladores provavelmente continuarão oferecendo suporte por causa de software legado
    • Um bom realçador de sintaxe também precisa tratar essa gramática
  • O universal character de C permite representar identificadores Unicode em código-fonte ASCII, como em int \\uFEB2 = 1;
    • O GCC gera erro se o caractere não estiver em um dos planes Unicode específicos permitidos pelo comitê do padrão
  • O comentário de uma linha em C pode se estender por várias linhas se houver uma barra invertida no fim da linha
    • Linguagens que aceitam escape com barra invertida no código-fonte, como Perl, Ruby e Shell, não suportam esse comportamento ao estilo C
    • Tcl e GNU Make parecem suportar esse comportamento
    • Emacs e Pygments às vezes tratam isso errado, enquanto o Vim aparentemente sempre acerta o tratamento da barra invertida
  • C também tem a diretiva de pré-processador nula
    • Arquivos .c do código-fonte v6 antigo frequentemente começam com uma linha contendo apenas #
    • Isso ainda é código válido hoje e pode ser usado para fazer o pré-processador remover certos comentários, mesmo quando cc -C -E pede preservação deles

Sintaxe de comentários: Haskell e D

  • Em C, não é possível aninhar comentários de múltiplas linhas dentro de outros comentários de múltiplas linhas
  • Haskell oferece suporte a comentários aninhados na forma {- ... {- ... -} ... -}
  • D adota os comentários // e /* ... */ de C e ainda introduz uma sintaxe separada de comentário recursivo no formato /+ ... +/
  • A documentação da gramática lexical de D é formal e detalhada, oferecendo bem as informações necessárias para implementação
    • O documento de sintaxe lexical de D trata de detalhes como strings hexadecimais e strings heredoc
    • D tem várias formas de string, como \"...\", strings com crase, r\"...\", q\"...\", x\"...\" etc.

Tcl e JavaScript: aspas e terminadores de linha invisíveis

  • Em Tcl, aspas podem aparecer dentro de identificadores
    • puts a\"b imprime a\"b
    • Também é possível colocar aspas em nomes de variáveis, mas ao referenciá-las deve-se usar ${a\"b} em vez de $a\"b
  • JavaScript tem uma gramática lexical embutida para expressões regulares
    • Em algo como /[/]/g, o / dentro dos colchetes do conjunto de caracteres não precisa ser escapado
    • Um método que só procura a próxima barra de fechamento pode falhar em código minificado
  • ECMAScript define u2028 LINE SEPARATOR e u2029 PARAGRAPH SEPARATOR como terminadores de linha
    • Na prática, esses caracteres se comportam como \\n
    • Como são caracteres do tipo Trojan Source, no Emacs eles são configurados para aparecer como e
    • Muitos softwares não reconhecem esses caracteres e acabam renderizando-os como pontos de interrogação
    • Fora D, o autor diz não conhecer outra linguagem com esse tipo de tratamento
  • Essa característica permitiu criar um polyglot em C e JavaScript no SectorLISP
    • lisp.js roda no navegador e também pode ser compilado com GCC para rodar localmente
    • O llamafile consegue realçar corretamente esse tipo de código, e o autor diz não ter encontrado o mesmo comportamento em outros realçadores

Shell e os casos de borda do heredoc

  • O heredoc de Shell permite escrever strings multilinha no formato cat <<EOF ... EOF
  • A sintaxe de heredoc com aspas, como cat <<'END', desativa a substituição de variáveis
  • Se o marcador do heredoc for uma string vazia, ele termina na próxima linha em branco
    • O programa de exemplo imprime hello e world em duas linhas
  • Em linguagens com suporte a heredoc, como Shell, Ruby e Perl, é possível ter vários heredocs na mesma linha
  • Em Shell, assim como em Tcl, # nem sempre inicia um comentário
    • Em ${x#hi-}, # é usado para remover um prefixo, e o exemplo imprime there

Interpolação de strings: linguagens que exigem pilha de estados

  • Em Kotlin, mesmo que a string comece com \", a interpolação faz com que ao encontrar { seja necessário entrar em outro estado
  • TypeScript, Swift, Kotlin e Scala oferecem forte suporte a interpolação de strings no sentido de permitir código real dentro da string
  • Para realçar Kotlin, Scala e TypeScript, é preciso contar chaves e manter uma pilha de estados do parser
    • TypeScript é relativamente simples, então basta adicionar alguns estados à máquina de estados finitos
    • Kotlin e Scala oferecem interpolação tanto com aspas duplas quanto com aspas triplas, por isso só o lexing de strings exigiu cerca de 13 estados independentes
    • Swift suporta a sintaxe de interpolação \"\\(var)\" e aspas triplas, mas a implementação ainda precisou de 10 estados

Swift, C# e FORTH: diferenças na forma de delimitar strings

  • Swift aceita a sintaxe de strings \"...\", \"\"\"...\"\"\" e /regex/ delimitadas por uma quantidade arbitrária de #
    • O número de # dos dois lados precisa coincidir
    • Isso resolve o problema de colocar aspas ou delimitadores de regex dentro da string
  • C# é parecido com as strings multilinha de aspas triplas do Python, mas permite usar no início e no fim uma quantidade ainda maior de aspas, desde que coincidam
    • A quantidade de aspas à esquerda define a condição de término à direita
    • O autor considera que permitir uma quantidade arbitrária de aspas reduz as regras de validade em comparação com a string clássica de aspas triplas do Python, tornando a decodificação mais simples com uma máquina de estados finitos
  • FORTH tokeniza tudo com base em separação por espaços
    • A sintaxe de início de string c\" também é um único token
    • c\" hello world\" tem o mesmo significado que \"hello world\" em outras linguagens

FORTRAN e COBOL: regras de colunas fixas

  • O llamafile é apresentado como um caso de uso útil para ajudar na manutenção de sistemas bancários mesmo depois da aposentadoria de programadores de FORTRAN e COBOL
  • Segundo o texto, é possível pedir código COBOL e FORTRAN a uma IA controlável em ambiente air-gapped, como Gemma 27b
  • As regras de colunas fixas de FORTRAN são as seguintes
    • Se houver *, c ou C na coluna 1, a linha vira comentário
    • Se houver um caractere não branco na coluna 6, é possível continuar uma linha que ultrapassa 80 caracteres
    • Se houver números nas colunas 1 a 5, eles formam um rótulo
  • As regras de COBOL são as seguintes
    • Se houver * na coluna 7, a linha vira comentário
    • Se houver - na coluna 7, é possível continuar uma linha que ultrapassa 80 caracteres
    • As colunas 1 a 6 contêm o número da linha

Zig e Lua: soluções diferentes para strings multilinha

  • Zig tem uma sintaxe de string multilinha iniciada por duas barras invertidas
    • Isso elimina a necessidade de chamar textwrap.dedent() em strings com aspas triplas ao estilo Python
    • O autor considera que a desvantagem é o ponto e vírgula ficar pouco elegante
    • A sintaxe é sugerida como algo que linguagens sem ponto e vírgula obrigatório, como Go, Scala e Python, poderiam considerar
  • A string multilinha de Lua se baseia em [[...]] e permite inserir uma quantidade arbitrária de = entre os colchetes
    • Em algo como [==[ ... ]==], a quantidade de = no início e no fim precisa coincidir
    • O mesmo esquema também pode ser usado em comentários
    • São possíveis tanto --[[ ... ]] quanto --[==[ ... ]==]

Assembly: combinação de dialetos e pré-processadores

  • Assembly é uma das linguagens mais difíceis de realçar porque existem muitos dialetos diferentes
  • O llamafile tenta lidar de forma razoável com várias sintaxes, como AT&T e nasm
  • As palavras-chave podem ser tratadas como o primeiro identificador da linha que não venha depois de dois-pontos, o que já faz a maior parte dos códigos assembly parecer aceitável
  • A sintaxe de comentários também não é simples
    • O comentário original do UNIX exigia apenas uma única /
    • O GNU as ainda oferece suporte a isso, mas só no início da linha
    • Segundo o texto, o Clang não oferece suporte ao fixed comment, o que dificulta seu uso prático em código open source
  • O assembler original do UNIX não usava aspas de fechamento em literais de caractere
    • 'x representa o valor 0x78 do caractere x
    • O GNU as ainda suporta isso, mas o LLVM não
    • Como esse formato aparece em código legado, um bom realçador deve suportá-lo
  • O GNU assembler permite identificadores entre aspas, de modo que quase qualquer caractere pode entrar em um símbolo
  • Assembly costuma ser usado junto com o pré-processador de C ou com m4
    • Linhas iniciadas com dnl, m4_dnl ou C também devem ser tratadas como comentário

Ada e BASIC: casos em que uma gramática pequena bagunça o lexing

  • Ada é relativamente simples de fazer lexing, mas o uso de aspas simples é peculiar
    • Assim como em C, pode haver literais de caractere como 'x'
    • Aspa simples também é usada para referenciar atributos, como em Foo'Size
    • Character'(')')'Image declara um caractere e o converte em representação textual por meio da função Image
  • O exemplo de Commodore BASIC quebra várias suposições comuns de realce de sintaxe
    • Strings podem omitir a aspa de fechamento no fim da linha
    • Nomes de variáveis podem ter sigils como $
    • Palavras-chave como goto são analisadas agressivamente até mesmo dentro de identificadores
  • Visual BASIC tem sintaxe de literal de data como #1/1/2024#
  • Visual BASIC também tem diretivas de pré-processador como #If DEBUG Then, #Else e #End If, o que dificulta o lexing

Perl: a complexidade entre Shell e linguagem de programação

  • Perl fica entre Shell e linguagem de programação, herdando a complexidade dos dois lados
  • Tornou expressões regulares um elemento de primeira classe da linguagem, e sua influência chegou a outras linguagens como Python
  • A sintaxe de substituição do Perl lembra a do sed, como em s/hello/Perl/i
    • Em vez de /, pode-se usar qualquer caractere de pontuação como delimitador
    • Formas como s!hello!Perl!i são úteis quando há barras dentro da regex
    • Com caracteres pareados, como em s{hello}{Perl}i, é necessário um caractere adicional
  • Perl tem muitos prefixos mágicos que também precisam ser realçados como strings
    • m, s, y, qr, qw, qq, qx etc. aparecem com vários delimitadores diferentes
  • Para não interpretar y/x/y/ como divisão, é preciso considerar o contexto
    • Como variáveis de Perl usam sigils como escalar $, array @ e hash %, isso ajuda na distinção mesmo sem parsing completo da gramática
  • Em Perl, é comum embutir documentação POD de páginas man no código-fonte
    • Um =word no início da linha inicia a documentação POD e =cut a encerra

Ruby: o alvo de lexing mais difícil

  • Ruby parece uma união de várias das linguagens anteriores, e sua gramática é avaliada como insuficientemente documentada de forma formal
  • O documento de syntax do manual Ruby é visto como leve nos detalhes
  • Ruby suporta a sintaxe com backquote e ao mesmo tempo permite usar backquote como se fosse nome de método, o que dificulta para o realçador decidir se aquilo é string ou não
  • Ruby tem tanto o operador << quanto heredoc
    • Em código real, existem formas como options[:includes] <<arg; true
    • Isso pode parecer heredoc, e até o Emacs interpreta errado
  • Também é válido código como puts \"This is #{<<HERE.strip} evil\", em que há um heredoc dentro da interpolação de string
  • Entre as 42 linguagens, Ruby foi considerada a maior surpresa; o autor sugere que Ruby talvez não possa ser analisada lexicalmente sem parsing, e que mesmo com parsing pode ser difícil ter certeza de como interpretá-la

Complexidade medida em linhas de código de implementação

  • Considerando a quantidade de linhas de código de cada implementação de realçador, FORTH foi a mais simples e Ruby a mais complexa
  • A implementação mais curta é highlight_forth.cpp, com 125 linhas
  • Entre as implementações relativamente curtas estão m4 com 132 linhas, Ada com 149, LISP com 160, MATLAB com 166, COBOL com 186, BASIC com 199 e FORTRAN com 200
  • Entre as implementações de tamanho intermediário estão JavaScript com 337 linhas, TypeScript com 371, Kotlin com 387, Scala com 387, Assembly com 447, C com 449, Swift com 455 e D com 521
  • As implementações mais longas são Shell com 570 linhas, Perl com 583 e highlight_ruby.cpp com 1042 linhas

1 comentários

 
GN⁺ 2024-11-03
Opiniões do Hacker News
  • Meu trigraph favorito de C é algo como do_action() ??!??! handle_error()
    Parece uma sintaxe especial de tratamento de erros, mas, quando você entende, ??!??! vira o OR lógico || e, pelas regras de avaliação de curto-circuito, handle_error() é executado quando do_action() retorna um valor diferente de 0, o que é bem satisfatório

  • Foi uma leitura divertida, mas acabei me identificando mais com a perspectiva do Lisp
    Pelo que entendi, a posição é que a sintaxe não é uma parte tão importante de uma linguagem e tende a virar mais um obstáculo do que uma ajuda; por isso ela deveria ser o mais simples e uniforme possível, para que se possa focar em outras coisas
    Dito isso, aprender edição estrutural em Lisp até agora tem sido mais um obstáculo do que uma ajuda, e imagino que um dia a recompensa venha

    • Uma sintaxe simples pode ser boa para o computador, mas sintaxe é projetada principalmente para ser lida e escrita por pessoas
      Torná-la simples como em Lisp só empurra a discussão sintática para questões semânticas; é apenas uma mudança de camada
      Acho uma sintaxe complexa muito mais fácil de ler e escrever do que uma sintaxe simples com semântica complexa. Erros de sintaxe dão feedback rápido, mas erros semânticos podem ficar escondidos até o momento da execução
    • Lisp tem reader macros, que permitem reprogramar o lexer, e o processo de transformar a estrutura visível em uma árvore sintática também pode ser tratado com macros
      Por exemplo, há algo como https://pyret.org/. Na prática, é difícil dizer que Lisp seja simples ou necessariamente uniforme
    • Fico um pouco surpreso que edição estrutural tenha sido um obstáculo; se eu pudesse dar um conselho, seria usar parinfer e saber apenas os atalhos dos três comandos slurp, barf e raise
      Só com esses quatro itens já dá para aproveitar cerca de 95% das vantagens do paredit sem a complexidade, e o resto dos truques pode ser aprendido depois que você se acostumar
  • Havia uma parte dizendo “não sei quem quer fazer syntax highlighting de C a 35 MB por segundo, mas agora é possível”; é rápido, mas o tcc compila 29 MB por segundo de C para código binário mesmo em computadores muito antigos: https://bellard.org/tcc/#speed
    Provavelmente dá para fazer ainda mais rápido, mas talvez nem seja necessário

  • Há algumas coisas que o autor deixou passar
    Não foram só TypeScript, Swift, Kotlin e Scala que chegaram ao extremo de colocar código real em interpolação de strings; C#, Python, JavaScript, Ruby, Shell e Make também oferecem algo parecido
    Em Tcl, a distinção entre código e dados é difícil, então { } é, na prática, um delimitador de string peculiar, e em xyzzy {#hello world} não dá para saber antes da avaliação se #hello world é um comentário ou uma string
    PostgreSQL tem as convenientes strings com aspas de dólar, de modo que, como em https://www.postgresql.org/docs/current/sql-syntax-lexical.h..., 'Dianne''s horse', $$Dianne's horse$$ e $SomeTag$Dianne's horse$SomeTag$ têm o mesmo significado

    • Perl também consegue
      Uma string como "I have $foo $bar's: @{[$bar x $foo]}" pode imprimir I have 5 x's: xxxxx
      A sintaxe @{[...]} explora uma característica do Perl que permite interpolação de arrays, não só de escalares; o [...] interno cria uma referência de array, e o @{...} externo a desreferencia. O interpretador Perl permite código arbitrário nessa expressão interna
    • Acho mais correto ver isso não tanto como “código dentro de strings”, mas como uma sintaxe especial de concatenação de strings
      Assim, seria possível aninhar como em "foo { toUpper("bar { x + y } bar") } foo", e, se + for concatenação de strings, isso seria essencialmente igual a "foo " + toUpper("bar " + (x + y) + " bar") + " foo"
      Não sei se existe alguma linguagem que realmente funcione assim
    • Ruby leva isso ao limite
      Eu gosto de Ruby, mas é difícil defender Ruby válido como puts "This is #{<, que mistura interpolação de strings e heredoc
      Além disso, em Ruby espaço em branco também pode ser caractere de aspas. Em um contexto sem operando à esquerda, % inicia uma string entre aspas, e o próximo caractere indica o tipo de aspas; portanto % hello vira uma string contendo hello. %(this is a string) ou %{this is a string} são bons, mas nunca vi uso de espaço em branco na prática e o irb também não lida direito com isso, então eu não me importaria se fosse removido
    • A interpolação de strings do Scala também pode ser usada como alvo de pattern matching
      Se você escrever val s"${a} + ${b}" = "1 + 2", a vira 1 e b vira 2
    • O ponto interessante das strings interpoladas de C# é que elas são avaliadas de forma preguiçosa
      Antigamente, em loops quentes, código como log.trace($"Entering iteration {i} for customer {c.ID} [{c.ShortName}]"); chamava string.Concat toda vez, antes mesmo de o logger sair, então muitos loggers implementavam sua própria interpolação
      Em C#, é possível declarar uma sobrecarga que receba DefaultInterpolatedStringHandler ou um padrão de handler customizado; essa sobrecarga tem prioridade e pode verificar se o log realmente é necessário antes de adiar a criação da string
  • Há outra peculiaridade sintática não mencionada aqui que quebra a maioria dos realçadores de sintaxe
    Em Java, escapes Unicode podem aparecer não só dentro de strings, mas em qualquer lugar
    Por exemplo, class Foo\u007b} é uma classe válida, e um \u000A dentro de um comentário // pode ser tratado como uma quebra de linha real, então o exemplo com assert também se comporta de forma diferente do esperado

    • Também vejo como um problema de segurança o fato de isso não ser realçado corretamente
      Como é possível encerrar um comentário de bloco com um escape Unicode, para esconder código malicioso em um comentário dentro de um arquivo-fonte Java basta criar uma desculpa para haver um bloco de escapes Unicode dentro do comentário
      Um desenvolvedor que não conheça esse recurso provavelmente vai achar que está comentado e passar batido
    • Nunca vi isso em Java; será que existe algum uso realmente útil?
  • Há um trecho dizendo que “Ruby é uma linguagem que escapa de todas as tentativas de entender sua sintaxe”, mas, do ponto de vista do TeX, que tem um lexer reprogramável arbitrariamente, isso é até bonitinho

    • Macros de leitura do Lisp também permitem programar o lexer
  • Dizem que “todo programador C sabe que não dá para colocar um comentário de várias linhas dentro de outro comentário de várias linhas”, mas, para programadores de Standard ML, isso pode ser uma limitação surpreendente
    Comentários aninhados como (* (* Nested (**) *) comment *) são válidos, e um val _ = print "hello, world\n" em seguida também executa normalmente
    Considerando que C era visto como uma linguagem expressiva quando surgiu, é curioso que comentários aninhados não tenham entrado na linguagem

    • Há três coisas interessantes nessa afirmação
      ML não tinha comentários de uma linha, então isso também é uma limitação igualmente surpreendente; chamar C de “expressiva” é algo que nunca ouvi, mas talvez fosse verdade em 1972 se comparada a assembly
      E não sei que relação a sintaxe de comentários tem com a expressividade da linguagem; por definição, diria que nenhuma
    • Para fazer o lexing de comentários aninhados, é preciso uma pilha ou pelo menos um contador de nível de aninhamento
      Tradicionalmente, a análise léxica era vista como uma área que usa apenas autômatos de estado finito, como expressões regulares, então esse tipo de recurso parecia estar fora do escopo do lexing
    • Em C também existe uma forma de aninhar comentários usando #if 0
      Dá para envolver algo como #if 0 ... #if 0 ... #endif ... #endif
    • Pascal também sempre ofereceu suporte à sintaxe de comentários aninhados como no exemplo
    • Isso vale não só para Standard ML, mas também para ML em geral
  • Gostaria de ver a implementação de realce de sintaxe do joe em um formato reutilizável
    O formato de https://joe-editor.sf.net/ é poderoso o suficiente para realçar corretamente f-strings do Python
    A documentação e exemplos relacionados estão em https://github.com/cmur2/joe-syntax/blob/joe-4.4/misc/HowItW..., https://gist.github.com/irdc/6188f11b1e699d615ce2520f03f1d0d...

    • Criei vários lexers e parsers com base no método de parsing por DFA do joe
      A gramática de estados e transições era muito mais fácil de entender do que a das ferramentas padrão
      A desvantagem é que o conjunto de regras fica verboso e uma estruturação ideal se torna um pouco difícil, mas vejo isso até como uma vantagem no sentido de que fica mais fácil inferir todas as regras de geração a partir do código
    • Curiosamente, como a sintaxe das f-strings do Python mudou na versão 3.12, o realce também precisa variar conforme a versão
  • O autor parece não ter tentado realçar sintaxe de TeX
    Talvez isso seja bom para a saúde mental, porque, em TeX, normalmente é impossível fazer realce de sintaxe completo sem interpretar
    Nem só fazer parsing basta, pois é possível redefinir o papel de cada caractere, permitindo coisas como “agora K é { e C é }
    De fato, há artigos no arXiv que usam esse recurso amaldiçoado

    • Criei https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil..., e ele realça de forma bastante decente, sem quebrar, os arquivos .tex que encontrei no meu disco rígido
      O objetivo é cobrir 99,9% do uso no mundo real. Assim, é provável que também cubra em geral o que um LLM produziria
      Sintaxes obscuras normalmente não são um grande problema, desde que não façam uma string ou comentário continuar até o fim do arquivo e engolir o restante do código-fonte
    • Foi difícil acreditar quando descobri que \makeatletter não significa “transformar algo em um caractere at”, mas sim tratar o caractere @ como letra durante o parsing
    • Common Lisp também permite redefinir readtables de forma parecida. Só que é menos provável que isso seja abusado no arXiv
  • Acho que não é fácil escrever um bom mecanismo de coloração de sintaxe como o do Vim
    A coloração de sintaxe exige processamento de contexto, aplicando regras diferentes a conteúdos aninhados de determinadas formas
    O realçador de sintaxe do Vim permite declarar dois tipos de itens, match e region: match é uma regra léxica simples, enquanto region tem expressões para casar início, fim e partes intermediárias separadamente
    Um item pode ser declarado para ficar ativo apenas quando estiver dentro de uma determinada região, e as relações de inclusão também podem ser declaradas mutuamente
    Sobre essa estrutura semântica básica, há vários recursos acrescentados para casos especiais. Mesmo para a Justine, seria difícil criar isso na hora em uma entrevista; acho que seria mais algo para uma tarefa de uma noite

    • Um exemplo difícil de lidar é TXR Lisp embutido dentro da linguagem TXR
      Há aqui um script genman que recebe o resultado da conversão de manpage para HTML e o refina para o HTML do manual do TXR: https://www.kylheku.com/cgit/txr/tree/genman.txr
      As partes que aparecem em branco são templates literais, e o código Lisp fica dentro de diretivas como @(do ...). Palavras-chave de TXR aparecem em roxo, palavras-chave de TXR Lisp em verde, e até a mesma palavra muda conforme o contexto
      Quasi-strings também podem conter sintaxe aninhada, e dentro delas pode haver novamente código embutido ou outras quasi-strings. Os arquivos de definição de sintaxe txr.vim e tl.vim são ambos gerados a partir de https://www.kylheku.com/cgit/txr/tree/genvim.txr
    • Ingenuamente, eu pensava que, para criar um realçador de sintaxe corretamente, bastaria parsear para uma AST e depois percorrer os tokens, atualizando as cores conforme o tipo do nó
      Se não fazem assim, imagino que seja por questões de eficiência. Por exemplo, em vez de realçar só a parte visível na tela, seria preciso parsear o arquivo inteiro, o que poderia ficar caro
    • Eu não apostaria dinheiro que a Justine não conseguiria fazer isso numa entrevista ;)