just foo procesa el "\n" del justfile y escribe un único byte 0x0A en el archivo bar; el artículo rastrea paso a paso de dónde salió ese valor
- El parser de Rust de
just está implementado de modo que, cuando encuentra el escape \n, inserta en la cadena el valor del escape de carácter de Rust '\n'
- El
rustc actual también está escrito en Rust, así que el rastro vuelve al lexer de rustc, pero se puede encontrar una pista más directa en la implementación en OCaml anterior a la etapa self-hosted
- Las primeras versiones de
rustc en OCaml procesaban el escape de carácter n como Char.code '\n', y el lexer de OCaml lo define como '\010'
- Como
0x0A es 10, el \n del justfile es un valor transmitido a través de generaciones del compilador de Rust, y su punto de partida lleva al byte que el compilador de OCaml insertó en el binario inicial de rustc al evaluar '\010'
Cómo el \n de un justfile llega a convertirse en 0x0A
- Al ejecutar
just foo, el siguiente justfile escribe un único byte 0x0A en el archivo bar
x := "\n"
foo:
printf '{{x}}' > bar
just está escrito en Rust, y la función cook_string del parser convierte los tokens de cadena de just, incluidos los que tienen secuencias de escape, en cadenas UTF-8
- Si después de una barra invertida viene
n, esta función ejecuta cooked.push('\n')
State::Backslash => {
match c {
'n' => cooked.push('\n'),
…
}
}
- En esta etapa,
just le delega a rustc insertar en la cadena el resultado de evaluar el escape de carácter de Rust '\n'
El camino de regreso hasta rustc y OCaml
- El manejo de escapes en
rustc está en la función scan_escape del lexer y, cuando encuentra n, vuelve a procesarlo como el escape de carácter de Rust '\n'
let res: char = match chars.next().ok_or(EscapeError::LoneSlash)? {
…
'n' => '\n',
…
};
- El
rustc actual está escrito en Rust y se compila a sí mismo, por lo que la búsqueda del significado de '\n' lleva de rustc de vuelta a rustc
- Sin embargo,
rustc no estuvo escrito en Rust desde el principio; las primeras versiones, antes de ser self-hosted, estaban escritas en OCaml
- El lexer de la versión de
rustc en OCaml procesaba el escape de carácter n así
| 'n' { end_char (Char.code '\n') lexbuf }
- Aquí también se usa el escape de carácter de OCaml
'\n', pero el lexer de OCaml tiene una definición más directa
let char_for_backslash = function
'n' -> '\010'
- Cuando el compilador de OCaml ve
\n, inserta el resultado de evaluar el escape de carácter decimal '\010', y como 0x0A es 10, coincide con el valor de byte que se buscaba
- Por lo tanto, el
\n del justfile conduce a algún tipo de byte 0x0A dentro del binario de just; ese byte fue insertado por rustc, y puede verse como un flujo en el que las versiones anteriores de rustc transmitieron el mismo valor a lo largo de generaciones
- El
rustc actual es 1.81.0 y, si contamos solo desde rustc 1.0, este proceso ocurrió al menos 81 veces; si se incluyen las versiones anteriores a 1.0, probablemente ocurrió más veces
- El punto de inicio del rastro es el momento en que el compilador de OCaml evaluó el escape de carácter decimal
'\010' e insertó el byte 0x0A en el binario inicial de rustc
1 comentarios
Opiniones de Hacker News
La primera vez que leí esta idea no fue en el contexto general de trusting trust, sino sobre el carácter de nueva línea, en el día 42 de https://www.sigbus.info/how-i-wrote-a-self-hosting-c-compile...
Es interesante que, para interpretar
"\n"dentro de un literal de cadena como un carácter de nueva línea real, el código fuente no contiene esa información de código ASCII, sino que esta se transmite desde el compilador anterior que compiló al compiladorAl final, el carácter de nueva línea de ese compilador puede rastrearse hasta el GCC que lo compiló
'\n'en manos de su propio compilador, pero en realidad tenía hardcodeados los valores numéricos de los escapes[1], y al parecer solo ofrecía opciones para sistemas ASCII y EBCDIC[1] https://github.com/gcc-mirror/gcc/blob/8a4a967a77cb937a2df45...
Parece probable que el texto original que el autor tenía en mente fuera la conferencia del Premio Turing de Ken Thompson, Reflections on Trusting Trust
Hay bastantes estudios, artículos y explicaciones sobre quines, así que quizá el autor leyó algún texto de ese ámbito
https://en.wikipedia.org/wiki/Quine_(computing)
https://www.teamten.com/lawrence/writings/coding-machines/
Yo también recuerdo haber visto hace unos años un texto de curiosidades exactamente igual sobre
'\n'en Rust, pero ya no logro encontrar la fuenteEs interesante que hayan pasado 10 horas y todavía no haya ningún hilo que mencione EBCDIC
Los primeros compiladores de C también existían en sistemas no ASCII que no mapeaban el “line feed” de
\nal decimal 10, así que cualquier teoría que se maneje aquí tiene que explicar ese hechohttps://en.wikipedia.org/wiki/EBCDIC
Además, EBCDIC tenía tanto un carácter explícito NextLine como un carácter LineFeed
En ASCII,
for (c = 'A'; c <= 'Z'; ++c) putchar(c);imprime de la A a la Z, pero en EBCDIC hay huecos entre las letras, por lo que termina imprimiendo 41 caracteres, incluidos caracteres no asignadosEl ordenamiento de EBCDIC ponía las minúsculas antes que las mayúsculas, y las letras antes que los números, justo al revés que ASCII
Lo único que el estándar C garantizaba sobre la codificación de caracteres era que los dígitos
'0'a'9'se mapearan en orden ascendente y de forma consecutivaEn teoría, un programa simple en C debía compilarse desde el mismo fuente y producir la misma salida tanto en ASCII como en EBCDIC, pero en la práctica había muchas trampas
Los primeros sistemas EBCDIC (MVS, VM/CMS, OS/400, DOS/VSE, etc.) almacenaban texto como archivos orientados a registros, en vez de como archivos de flujo de bytes, y cada línea era un registro de longitud fija o variable
En los registros de longitud fija, al crear el archivo se declaraba una longitud de registro como 80 o 132; las líneas cortas normalmente se rellenaban con el carácter de espacio EBCDIC 0x40, y las líneas largas se truncaban o usaban un carácter de continuación
Los registros de longitud variable llevaban al principio una palabra descriptora de registro (RDW) con la longitud, pero eran raros en archivos de texto y código fuente; lo común eran los registros de longitud fija
Por eso, aunque NEL existiera, normalmente no se usaba en archivos en disco
Un carácter de nueva línea como NEL es una señal dentro de banda que indica límites de línea/registro, mientras que un sistema de archivos orientado a registros expresa esos límites fuera de banda
No sé exactamente cómo estaba implementado stdio en la biblioteca de runtime de un compilador C para EBCDIC, pero imagino que internamente mapeaba
\na NEL y luego la capa stdio lo trataba como separador de registros, escribiendo cada registro con una llamada al sistema aparte y aplicando padding si hacía faltaMás adelante, la mayoría de estos sistemas operativos obtuvo subsistemas compatibles con POSIX y, con ellos, archivos de flujo de bytes como en los sistemas más comunes
Los sistemas IBM suelen soportar la capacidad de etiquetar archivos con páginas de códigos, de modo que un archivo puede mezclar EBCDIC y ASCII y el sistema operativo realiza la conversión en la capa de entrada/salida
Gracias a eso, incluso una aplicación que usa EBCDIC en runtime puede leer archivos ASCII como si fueran EBCDIC, sin llamadas aparte a una API de conversión ni especificaciones explícitas
Las aplicaciones nuevas usan cada vez más sistemas de archivos basados en POSIX, pero muchas aplicaciones antiguas todavía guardan datos, archivos de texto e incluso código fuente en los sistemas de archivos clásicos orientados a registros
Según entiendo, en entornos reales donde más se veía EBCDIC NEL era en conexiones de terminal en modo línea de terminales de copia impresa como IBM 2741 o IBM 3767
Es un texto realmente interesante
A mí me suena como una mezcla de programación literaria y poesía
Intenta explicar la idea de que ese mismo byte 0x0A que aparece al ejecutar
just fooquizá haya pasado por cientos de ciclos de generación de códigoHace mucho tiempo alguien codificó esta información de alguna manera en el compilador de OCaml, y años después la información de 0x0A en mi computadora está almacenada por esa historia
Pero este fenómeno se explica con código real
Claro que el código en sí no es lo central, y tampoco parece que alguien vaya a ejecutar o compilar este código específico, sino que es código puesto ahí para que una persona pueda seguir la discusión
Tenía curiosidad por saber si clang tiene la misma propiedad, y en
lib/Lex/LiteralSupport.cppestá explícitamente hardcodeado como 10ProcessCharEscapeparsea las secuencias de escape estándar de C y las maneja comocase 'n': ResultChar = 10; break;gcc/libcpp/charset.cc, y elige entre ASCII o EBCDICColoca los valores de
\a \b \e \f \n \r \t \ven el arreglocharconsts; si es ASCII usa{ 7, 8, 27, 12, 10, 13, 9, 11 }, y si es EBCDIC usa{ 47, 22, 39, 12, 21, 13, 5, 11 }, y luego lo procesa concase 'n': c = charconsts[4]; break;Recuerdo un artículo similar sobre algún compilador de C
Al final, el único lugar donde aparecía el valor 0x10 era en el binario del compilador, y se descubrió que en el código fuente solo estaba en formas como
"\\n" -> "\n"Esto supera mi nivel
No entiendo por qué habría que hacer un viaje tan largo para averiguar por qué
\nse codifica como un byte con valor 10Me parece algo obvio, y como ni el autor ni los comentarios lo explican, me siento como un tonto
Si al escribir un parser parseas un salto de línea como la secuencia de escape
\n, ¿de dónde salió el valor 10?Si parseas el salto de línea como el literal entero
10, ¿de dónde salió el valor binario real1010?El objetivo último de este experimento mental es cambiar la percepción sobre los compiladores, como en la famosa presentación Reflections On Trusting Trust
Es decir, un compilador no es simplemente algo que produce programas; también es entrada de programas
Como el compilador en sí también es un programa, el compilador que creó a ese compilador fue entrada del compilador actual y, transitivamente, se vuelve entrada de mi programa
Y eso continúa con el compilador del compilador del compilador, y con los compiladores por encima de él
'\n'Es un caso real interesante del hack de Ken Thompson
¿Por qué no 9 u 11?
El código dice: “si ves la cadena de carácter de salto de línea, emite el carácter de salto de línea”
Pero ¿cómo sabe el compilador qué es un carácter de salto de línea?
El código de ese compilador, de nuevo, solo dice: “si ves la cadena de carácter de salto de línea, trátala como un carácter de salto de línea”
Una persona puede buscar “códigos de escape de cadenas de C”, pero esa tabla no está en ninguna parte dentro del compilador
Si C 2025 definiera Start of Heading como
\h, ¿'h' => cooked.push('\h')empezaría a funcionar mágicamente?¿Cómo podría saberlo?
Claramente, en algún momento alguien tuvo que programar manualmente el mapeo
'n' => 10; la pregunta es dónde está ese lugarPor culpa de C, siempre pensé que
\0???era un escape octalAsí que en mi cabeza
\012es\x0ao0x0a, y\010es0x08Por eso este artículo me resulta bastante confuso
Tal vez OCaml no tenga escapes octales sino escapes decimales, y quizá
\09sea el carácter de tabulaciónNo lo comprobé
Los escapes con barra invertida son simbólicos/mnemotécnicos, así que
\nes “[Ne]wline”,\res “carriage [R]eturn”,\tes “[T]ab”, y asíEn cambio, conviene mirar la convención de caracteres de control como
^C(interrupción),^G(campana),^M(retorno de carro)Estos están en el conjunto de caracteres de control C0, y
^Ces\0x3,^Ges\0x7,^Mes\0xDEs un método ingenioso que se remonta a antes de Unix: para representar los caracteres C0 invisibles de ASCII, las terminales anteponían el carácter
^y aplicaban AND-0x40 al carácter correspondiente para moverlo al rango visible antes de imprimirloPara seguirlo, conviene abrir una tabla ASCII como https://www.asciitable.com
Cada carácter de control se mapea al
^carácterubicado dos columnas a la derecha en esa tablaPor eso aparecen equivalencias difíciles de memorizar, como que
\0se represente extrañamente como^@, o que la tecla Esc sea^[Esto no fue una elección de los autores de Unix, sino producto de la numeración de ASCII
Entre las sintaxis de cadenas que conozco, solo se me ocurren OCaml, Lua y DNS
Por la capitalización incorrecta, pensé que quizá existía otra secuencia de escape casi desconocida,
\N, distinta de\nPensé que tal vez coincidía con cualquier carácter que no fuera un salto de línea, pero no: era por las versalitas del artículo original
\n, pero no se muestra así por esta regla de CSS.title { font-variant: small-caps; }\NMuchos sistemas usan
\Ncomo NULL en CSV o formatos similares para distinguirlo de una cadena vacíaPor eso pensé que este artículo trataba de eso
\NInserta un carácter Unicode por nombre
Por ejemplo,
'\N{PILE OF POO}'es una cadena Unicode con un solo emoji de popóEs mucho más autoexplicativo que usar una secuencia hexadecimal con
\uo\UAun así, me pareció una lectura entretenida
El “otro artículo” que inspiró este texto probablemente sea este
https://research.swtch.com/nih
Running the "Reflections on Trusting Trust" Compiler - https://news.ycombinator.com/item?id=38020792 - octubre de 2023, 67 comentarios