just foo processa "\n" no justfile e escreve no arquivo bar um único byte 0x0A; o texto rastreia passo a passo de onde veio esse valor
- O parser em Rust do
just foi implementado para, ao encontrar o escape \n, inserir na string o valor do escape de caractere em Rust '\n'
- Como o
rustc atual também é escrito em Rust, o rastreamento continua até o lexer do rustc, mas é possível encontrar uma pista mais direta na implementação em OCaml anterior ao self-hosting
- As versões iniciais do
rustc em OCaml tratavam o escape de caractere n como Char.code '\n', e o lexer de OCaml o define como '\010'
- Como
0x0A é 10, o \n do justfile é um valor transmitido através de gerações do compilador Rust, e seu ponto de partida leva ao byte colocado no binário inicial do rustc quando o compilador OCaml avaliou '\010'
Como o \n no justfile vira 0x0A
- Ao executar
just foo, o justfile abaixo escreve no arquivo bar um único byte 0x0A
x := "\n"
foo:
printf '{{x}}' > bar
- O
just é escrito em Rust, e a função cook_string do parser converte tokens de string do just com sequências de escape em strings UTF-8
- Quando há um
n depois da barra invertida, essa função executa cooked.push('\n')
State::Backslash => {
match c {
'n' => cooked.push('\n'),
…
}
}
- Nessa etapa, o
just deixa para o rustc a tarefa de inserir na string o resultado da avaliação do escape de caractere em Rust '\n'
O caminho de volta até rustc e OCaml
- O tratamento de escapes no
rustc fica na função scan_escape do lexer, e ao encontrar n ele novamente o trata como o escape de caractere em Rust '\n'
let res: char = match chars.next().ok_or(EscapeError::LoneSlash)? {
…
'n' => '\n',
…
};
- Como o
rustc atual é escrito em Rust e compila a si mesmo, o processo de descobrir o significado de '\n' continua de rustc para rustc novamente
- Porém, o
rustc nem sempre foi escrito em Rust; as versões iniciais anteriores ao self-hosting eram escritas em OCaml
- No
rustc em OCaml, o lexer tratava o escape de caractere n assim:
| 'n' { end_char (Char.code '\n') lexbuf }
- Aqui também aparece o escape de caractere em OCaml
'\n', mas o lexer de OCaml tem uma definição mais direta
let char_for_backslash = function
'n' -> '\010'
- Quando o compilador OCaml encontra
\n, ele insere o resultado da avaliação do escape decimal de caractere '\010'; como 0x0A é 10, isso coincide com o valor do byte procurado
- Portanto, o
\n do justfile leva a alguma forma do byte 0x0A dentro do binário do just; esse byte foi colocado pelo rustc, e os rustc anteriores transmitiram o mesmo valor ao longo das gerações
- O
rustc atual é a versão 1.81.0 e, considerando apenas a partir do rustc 1.0, esse processo ocorreu pelo menos 81 vezes; incluindo antes da 1.0, é possível que tenha acontecido ainda mais
- O ponto de partida do rastreamento é o momento em que o compilador OCaml avaliou o escape decimal de caractere
'\010' e colocou o byte 0x0A no binário inicial do rustc
1 comentários
Opiniões no Hacker News
O primeiro lugar onde li essa ideia não foi no contexto geral de trusting trust, mas sobre caracteres de nova linha: foi no dia 42 de https://www.sigbus.info/how-i-wrote-a-self-hosting-c-compile...
É interessante que, para interpretar
"\n"dentro de um literal de string como um caractere real de nova linha, o código-fonte não contém a informação daquele código ASCII; ela é transmitida pelo compilador anterior que compilou o compiladorNo fim, o caractere de nova linha desse compilador pode ser rastreado até o GCC que o compilou
'\n'a cargo de seu próprio compilador, mas na prática ele codificava diretamente os valores numéricos dos escapes[1], aparentemente deixando apenas opções para sistemas ASCII e EBCDIC[1] https://github.com/gcc-mirror/gcc/blob/8a4a967a77cb937a2df45...
Parece possível que o texto original em que o autor estava pensando seja a palestra do prêmio Turing de Ken Thompson, Reflections on Trusting Trust
Há bastante pesquisa, artigos e explicações sobre quines, então o autor pode ter lido algum texto nessa linha
https://en.wikipedia.org/wiki/Quine_(computing)
https://www.teamten.com/lawrence/writings/coding-machines/
Eu também me lembro de ter visto, alguns anos atrás, um texto exatamente com a mesma curiosidade sobre
'\n'em Rust, mas agora não consigo encontrar a fonteÉ interessante que, depois de 10 horas, ainda não haja nenhuma thread mencionando EBCDIC
Compiladores C iniciais também existiam em sistemas não ASCII que não mapeavam o “line feed” de
\npara o decimal 10, então todas as teorias discutidas aqui precisam explicar esse fatohttps://en.wikipedia.org/wiki/EBCDIC
Além disso, o EBCDIC tinha tanto um caractere explícito NextLine quanto um caractere LineFeed
Em ASCII,
for (c = 'A'; c <= 'Z'; ++c) putchar(c);imprime de A a Z, mas em EBCDIC há lacunas entre as letras, então ele acaba imprimindo 41 caracteres, incluindo caracteres não atribuídosA ordenação do EBCDIC colocava minúsculas antes de maiúsculas, e letras antes de números, o oposto do ASCII
A única garantia que o padrão C dava sobre codificação de caracteres era que os dígitos de
'0'a'9'seriam mapeados em ordem crescente e consecutivaEm teoria, um programa C simples deveria ser compilado a partir do mesmo código-fonte e produzir a mesma saída tanto em ASCII quanto em EBCDIC, mas na prática havia muitas armadilhas
Sistemas EBCDIC iniciais (MVS, VM/CMS, OS/400, DOS/VSE etc.) armazenavam texto como arquivos orientados a registros, em vez de arquivos de fluxo de bytes, e cada linha era um registro de comprimento fixo ou variável
Em registros de comprimento fixo, ao criar o arquivo se declarava um tamanho de registro, como 80 ou 132; linhas curtas normalmente eram preenchidas com o caractere de espaço EBCDIC 0x40, enquanto linhas longas eram truncadas ou usavam um caractere de continuação
Registros de comprimento variável traziam antes um Record Descriptor Word (RDW) contendo o tamanho, mas eram raros em arquivos de texto e código-fonte; registros de comprimento fixo eram o comum
Por isso, mesmo existindo, o NEL normalmente não era usado em arquivos em disco
Caracteres de nova linha como NEL são um sinal in-band que indica limites de linha/registro, enquanto sistemas de arquivos orientados a registros expressavam esses limites fora da banda
Não sei exatamente como o stdio era implementado na biblioteca de runtime dos compiladores C para EBCDIC, mas imagino que internamente
\nfosse mapeado para NEL e que a camada stdio o tratasse como separador de registros, escrevendo cada registro por uma chamada de sistema separada e aplicando padding quando necessárioMais tarde, a maioria desses sistemas operacionais ganhou subsistemas compatíveis com POSIX e passou a ter também arquivos de fluxo de bytes como os sistemas mainstream
Sistemas IBM em geral oferecem suporte a tags de página de código nos arquivos, de modo que um arquivo pode misturar EBCDIC e ASCII, com o sistema operacional fazendo a conversão na camada de entrada/saída
Graças a isso, mesmo aplicações que usam EBCDIC em runtime conseguem ler arquivos ASCII como se fossem EBCDIC, sem chamadas separadas a APIs de conversão nem especificação explícita
Aplicações novas usam cada vez mais sistemas de arquivos baseados em POSIX, mas aplicações antigas muitas vezes ainda armazenam dados, arquivos de texto e até código-fonte nos sistemas de arquivos clássicos orientados a registros
Pelo que entendo, o lugar onde era mais comum ver EBCDIC NEL em ambientes reais eram conexões de terminais em modo linha de terminais hardcopy como o IBM 2741 e o IBM 3767
É um texto realmente fascinante
Para mim, ele soa como uma mistura de programação literária e poesia
O texto tenta explicar a ideia de que aquele byte 0x0A que aparece ao executar
just footalvez tenha passado por centenas de ciclos de geração de códigoHá muito tempo, alguém codificou essa informação de alguma forma no compilador OCaml e, anos depois, a informação do 0x0A no meu computador está armazenada por causa dessa história
Mas esse fenômeno é explicado por código real
Claro que o código em si não é o ponto central, e provavelmente ninguém vai executar ou compilar esse código específico, mas ele está ali para que as pessoas consigam acompanhar a discussão
Fiquei curioso para saber se o clang também tem a mesma característica, e em
lib/Lex/LiteralSupport.cppisso está explicitamente hardcoded como 10ProcessCharEscapeanalisa as sequências de escape padrão de C e trata algo comocase 'n': ResultChar = 10; break;gcc/libcpp/charset.cc, onde isso fica hardcoded e escolhe entre ASCII ou EBCDICEle coloca os valores de
\a \b \e \f \n \r \t \vno arraycharconsts; se for ASCII, usa{ 7, 8, 27, 12, 10, 13, 9, 11 }, e se for EBCDIC, usa{ 47, 22, 39, 12, 21, 13, 5, 11 }, depois trata comcase 'n': c = charconsts[4]; break;Lembro de um texto parecido sobre algum compilador C
No fim, descobriu-se que o único lugar onde o valor 0x10 aparecia era no binário do compilador; no código-fonte, ele só aparecia em formas como
"\\n" -> "\n"Isso está acima do meu nível
Não entendo por que é preciso uma jornada tão longa para descobrir por que
\né codificado como um byte de valor 10Parece algo óbvio, e como nem o autor nem os comentários explicam, acabo me sentindo burro
Se, ao escrever um parser, você analisa uma quebra de linha como a sequência de escape
\n, de onde veio o valor 10?Se você analisa a quebra de linha como o literal inteiro
10, de onde veio o valor binário real1010?O objetivo final desse experimento mental é mudar a percepção sobre compiladores, como na famosa apresentação Reflections On Trusting Trust
Ou seja, um compilador não é simplesmente algo que gera programas; ele também é uma entrada do programa
Como o próprio compilador é um programa, o compilador que criou esse compilador foi uma entrada do compilador atual e, transitivamente, torna-se entrada do meu programa
E isso continua para o compilador do compilador do compilador, e para os compiladores acima dele
'\n'realmente é mapeadoÉ um exemplo real interessante do hack de Ken Thompson
Por que não 9 ou 11?
O código diz: “ao ver a string do caractere de nova linha, emita o caractere de nova linha”
Mas como o compilador sabe o que é o caractere de nova linha?
O código desse compilador, por sua vez, também só diz: “ao ver a string do caractere de nova linha, trate como o caractere de nova linha”
Uma pessoa pode pesquisar “códigos de escape de strings em C”, mas essa tabela não está em lugar nenhum dentro do compilador
Se o C 2025 definir Start of Heading como
\h, será que'h' => cooked.push('\h')vai começar a funcionar magicamente?Como, afinal, alguém poderia saber?
Em algum momento, certamente alguém programou manualmente o mapeamento
'n' => 10; a pergunta é onde fica esse lugarTalvez por causa do C, sempre pensei que
\0???fosse um escape octalEntão, na minha cabeça,
\012é\x0aou0x0a, e\010é0x08Por isso este texto me deixou bastante confuso
Talvez OCaml não tenha escapes octais, mas sim escapes decimais, e
\09talvez seja o caractere de tabulaçãoNão verifiquei
Escapes de barra invertida são simbólicos/mnemônicos:
\né “[Ne]wline”,\ré “carriage [R]eturn”,\té “[T]ab”, e assim por dianteEm vez disso, vale olhar para a convenção de caracteres de controle como
^C(interrupção),^G(bell),^M(carriage return)Eles ficam no conjunto de caracteres de controle C0, e
^Cé\0x3,^Gé\0x7,^Mé\0xDDe uma forma engenhosa que remonta a antes do Unix, os terminais representavam os caracteres C0 invisíveis do ASCII prefixando-os com o caractere
^e aplicando AND-0x40 ao caractere correspondente para movê-lo para uma faixa visível antes de imprimi-loPara acompanhar, é bom abrir uma tabela ASCII como https://www.asciitable.com
Cada caractere de controle é mapeado para o
^caractereduas casas ao lado nessa tabelaPor isso surgem equivalências difíceis de memorizar, como
\0sendo estranhamente representado por^@, e a tecla Esc virando^[Isso não foi uma escolha dos autores do Unix, mas um produto do sistema de numeração do ASCII
Entre as sintaxes de string que conheço, acho que só OCaml, Lua e DNS têm isso
Por causa da capitalização errada, achei que talvez existisse uma sequência de escape quase desconhecida
\N, diferente de\nPensei que fosse algo que combinasse com qualquer caractere exceto quebra de linha, mas não; era por causa das letras maiúsculas pequenas no texto original
\n, mas não aparece assim por causa desta regra CSS.title { font-variant: small-caps; }\NMuitos sistemas usam
\Ncomo NULL em CSV ou formatos parecidos, para diferenciá-lo de uma string vaziaPor isso achei que este texto fosse sobre isso
\NEla insere um caractere Unicode pelo nome
Por exemplo,
'\N{PILE OF POO}'é uma string Unicode com um único emoji de cocôÉ muito mais autoexplicativo do que usar uma sequência hexadecimal com
\uou\UAinda assim, foi uma leitura divertida
O “outro texto” que inspirou este artigo provavelmente é este
https://research.swtch.com/nih
Running the "Reflections on Trusting Trust" Compiler - https://news.ycombinator.com/item?id=38020792 - outubro de 2023, 67 comentários