1 पॉइंट द्वारा GN⁺ 2024-10-07 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • just foo justfile में मौजूद "\n" को प्रोसेस करके bar फ़ाइल में एक single byte 0x0A लिखता है, और लेख चरण-दर-चरण ट्रेस करता है कि यह value कहाँ से आई
  • just का Rust parser इस तरह implement किया गया है कि \n escape मिलने पर वह Rust character escape '\n' की value को string में डालता है
  • मौजूदा rustc भी Rust में लिखा गया है, इसलिए tracing फिर से rustc के lexer तक जाती है, लेकिन self-hosted होने से पहले के OCaml implementation में ज्यादा सीधा संकेत मिल सकता है
  • शुरुआती OCaml version rustc character escape n को Char.code '\n' के रूप में process करता था, और OCaml lexer इसे '\010' के रूप में define करता है
  • 0x0A 10 होता है, इसलिए justfile का \n Rust compiler की पीढ़ियों से होकर आई हुई value है, और इसका starting point उस byte तक जाता है जिसे OCaml compiler ने '\010' evaluate करके शुरुआती rustc binary में डाला था

justfile का \n 0x0A कैसे बनता है

  • just foo चलाने पर नीचे दिया गया justfile, bar फ़ाइल में एक single byte 0x0A लिखता है
x := "\n"
foo:
printf '{{x}}' > bar
  • just Rust में लिखा गया है, और parser का cook_string function escape sequences वाले just string token को UTF-8 string में बदलता है
  • backslash के बाद n आने पर यह function cooked.push('\n') execute करता है
State::Backslash => {
    match c {
        'n' => cooked.push('\n'),
        …
    }
}
  • इस चरण में just, Rust के character escape '\n' के evaluation result को string में डालने का काम rustc पर छोड़ देता है

rustc और OCaml तक पीछे जाती राह

  • rustc में escape handling lexer के scan_escape function में है, और n मिलने पर इसे फिर Rust character escape '\n' के रूप में process किया जाता है
let res: char = match chars.next().ok_or(EscapeError::LoneSlash)? {
    …
    'n' => '\n',
    …
};
  • मौजूदा rustc Rust में लिखा गया है और खुद को compile करता है, इसलिए '\n' का अर्थ खोजने की प्रक्रिया rustc से फिर rustc तक जारी रहती है
  • हालांकि rustc शुरुआत से Rust में नहीं लिखा गया था; self-hosted होने से पहले के शुरुआती versions OCaml में लिखे गए थे
  • OCaml version rustc का lexer character escape n को इस तरह process करता है
| 'n' { end_char (Char.code '\n') lexbuf }
  • यहाँ भी OCaml character escape '\n' इस्तेमाल होता है, लेकिन OCaml lexer में इससे ज्यादा सीधी definition है
let char_for_backslash = function
  'n' -> '\010'
  • OCaml compiler जब \n देखता है, तो decimal character escape '\010' का evaluation result डालता है, और 0x0A 10 होता है, इसलिए यह खोजी जा रही byte value से मेल खाता है
  • इसलिए justfile का \n, just binary के अंदर किसी रूप में मौजूद 0x0A byte तक जाता है; वह byte rustc ने डाला था, और इसे पिछले rustc versions ने पीढ़ी-दर-पीढ़ी वही value आगे बढ़ाई, ऐसा flow माना जा सकता है
  • मौजूदा rustc 1.81.0 है, और सिर्फ rustc 1.0 के बाद से देखें तो भी यह प्रक्रिया कम से कम 81 बार हुई है; 1.0 से पहले तक शामिल करें तो संभवतः और ज्यादा बार हुई होगी
  • tracing का starting point वह जगह है जहाँ OCaml compiler ने decimal character escape '\010' को evaluate करके शुरुआती rustc binary में 0x0A byte डाला था

1 टिप्पणियां

 
GN⁺ 2024-10-07
Hacker News की राय
  • मैंने यह विचार पहली बार सामान्य trusting trust के संदर्भ में नहीं, बल्कि newline character के बारे में पढ़ा था, https://www.sigbus.info/how-i-wrote-a-self-hosting-c-compile... के day 42 में।
    string literal के अंदर "\n" को असली newline character की तरह समझने के लिए source code में उसका ASCII code मौजूद नहीं होता; वह उस पुराने compiler से आता है जिसने इस compiler को compile किया था — यह बात दिलचस्प है।
    आखिरकार उस compiler का newline character पीछे जाकर उसे compile करने वाले GCC तक ट्रेस किया जा सकता है।

    • मुझे उम्मीद थी कि GCC भी '\n' की value अपने compiler पर छोड़ता होगा, लेकिन वास्तव में उसने escape की numeric value hardcode कर रखी थी[1], और लगता है कि सिर्फ ASCII और EBCDIC systems के लिए विकल्प रखे गए थे।
      [1] https://github.com/gcc-mirror/gcc/blob/8a4a967a77cb937a2df45...
  • लगता है कि लेखक को याद आया मूल लेख Ken Thompson का Turing Award lecture Reflections on Trusting Trust हो सकता है।

    • उस talk में भी यह बताया गया है कि यह technique अधिक सामान्य रूप से quine में इस्तेमाल होती है।
      quine पर काफ़ी research, papers और explanations हैं, इसलिए संभव है कि लेखक ने उसी तरह की कोई चीज़ पढ़ी हो।
      https://en.wikipedia.org/wiki/Quine_(computing)
    • 2009 का यह छोटा और शानदार web article भी पढ़ने लायक है।
      https://www.teamten.com/lawrence/writings/coding-machines/
    • शायद वह नहीं था।
      मुझे भी याद है कि कुछ साल पहले मैंने Rust के '\n' पर बिल्कुल ऐसा ही एक trivia article देखा था, लेकिन अब उसका source नहीं मिल रहा।
  • दिलचस्प है कि 10 घंटे बीत जाने के बाद भी thread में EBCDIC का ज़िक्र नहीं हुआ।
    शुरुआती C compilers ऐसे non-ASCII systems पर भी थे जहाँ \n का “line feed” decimal 10 पर map नहीं होता था, इसलिए यहाँ चल रही हर theory को उस तथ्य की व्याख्या करनी चाहिए।
    https://en.wikipedia.org/wiki/EBCDIC
    ऊपर से EBCDIC में explicit NextLine character और LineFeed character दोनों थे।
    ASCII में for (c = 'A'; c <= 'Z'; ++c) putchar(c); A से Z तक print करता है, लेकिन EBCDIC में letters के बीच gaps हैं, इसलिए यह unassigned characters सहित 41 characters print कर देगा।
    EBCDIC ordering में lowercase, uppercase से पहले आता था, और letters, digits से पहले आते थे, यानी ASCII का बिल्कुल उल्टा।
    character encoding के बारे में C standard सिर्फ इतना guarantee करता था कि digits '0'~'9' लगातार बढ़ते क्रम में mapped हों।
    theory में एक simple C program को ASCII और EBCDIC दोनों में एक ही source से compile होकर एक जैसा output देना चाहिए था, लेकिन व्यवहार में बहुत traps थे।

    • EBCDIC में newline/next-line character NEL तो है, लेकिन कई EBCDIC systems में वह आम तौर पर दिखाई नहीं देता।
      शुरुआती EBCDIC systems (MVS, VM/CMS, OS/400, DOS/VSE आदि) text को byte-stream files की तरह store करने के बजाय record-oriented files में store करते थे, और हर line fixed-length या variable-length record होती थी।
      fixed-length records में file बनाते समय 80 या 132 जैसी record length declare की जाती थी; छोटी lines को आम तौर पर EBCDIC space character 0x40 से pad किया जाता था, और लंबी lines को truncate किया जाता था या continuation character इस्तेमाल होता था।
      variable-length records के आगे length वाला record descriptor word (RDW) लगाया जाता था, लेकिन text files और source code में यह कम मिलता था; fixed-length records ज़्यादा आम थे।
      इसलिए NEL मौजूद होने के बावजूद disk files में वह आम तौर पर इस्तेमाल नहीं होता था।
      NEL जैसा newline character line/record boundary का in-band signal है, जबकि record-oriented file systems उस boundary को out-of-band तरीके से व्यक्त करते थे।
      मुझे नहीं पता EBCDIC C compiler की runtime library में stdio ठीक-ठीक कैसे implement था, लेकिन लगता है कि internally \n को NEL पर map किया जाता होगा, फिर stdio layer उसे record separator की तरह handle करती होगी और हर record को अलग system call से लिखते समय ज़रूरत पड़ने पर padding करती होगी।
      बाद में इन ज़्यादातर operating systems को POSIX-compatible subsystems मिले, जिनसे mainstream systems की तरह byte-stream files भी उपलब्ध हुईं।
      IBM systems आम तौर पर files पर code page tags लगाने का समर्थन करते हैं, इसलिए किसी file में EBCDIC और ASCII दोनों हो सकते हैं और OS I/O layer पर conversion कर देता है।
      इसकी वजह से runtime में EBCDIC इस्तेमाल करने वाले applications भी ASCII files को बिना अलग conversion API calls या explicit settings के EBCDIC की तरह पढ़ सकते हैं।
      नए applications अब POSIX-based file systems का बढ़ता उपयोग करते हैं, लेकिन पुराने applications अब भी data, text files, यहाँ तक कि source code को भी पारंपरिक record-oriented file systems में store करते हैं।
      मेरी समझ से वास्तविक दुनिया में EBCDIC NEL सबसे ज़्यादा IBM 2741 और IBM 3767 जैसे hardcopy terminals के line-mode terminal connections में दिखाई देता था।
  • यह सच में बहुत दिलचस्प लेख है।
    मुझे यह literate programming और कविता का मिश्रण जैसा लगता है।
    just foo चलाने पर जो 0x0A byte निकलता है, यह लेख शायद यह समझाने की कोशिश करता है कि वह सैकड़ों code-generation cycles से होकर आया हो सकता है।
    बहुत पहले किसी ने किसी न किसी तरह इस जानकारी को OCaml compiler में encode किया था, और कुछ साल बाद मेरे computer में 0x0A की जानकारी उसी इतिहास की वजह से मौजूद है।
    और यह घटना असली code के ज़रिए समझाई गई है।
    बेशक वह code खुद मुख्य बिंदु नहीं है, और शायद कोई इस खास code को चलाने या compile करने वाला भी नहीं है, लेकिन वह code इसलिए रखा गया है ताकि इंसान इस चर्चा का पीछा कर सके।

  • मुझे जिज्ञासा थी कि क्या clang में भी यही गुण है, और lib/Lex/LiteralSupport.cpp में यह 10 पर हार्डकोडेड मिला
    ProcessCharEscape standard C escape sequences को parse करता है, और case 'n': ResultChar = 10; break; की तरह handle करता है

    • GCC में भी ऐसा ही है; gcc/libcpp/charset.cc में यह हार्डकोडेड है, और ASCII या EBCDIC में से एक चुनता है
      \a \b \e \f \n \r \t \v के values charconsts array में रखे जाते हैं; ASCII होने पर { 7, 8, 27, 12, 10, 13, 9, 11 }, और EBCDIC होने पर { 47, 22, 39, 12, 21, 13, 5, 11 } इस्तेमाल होते हैं, फिर case 'n': c = charconsts[4]; break; से handle किया जाता है
  • मुझे किसी C compiler के बारे में ऐसा ही एक लेख याद है
    आखिर में पता चला कि 0x10 value जिस एकमात्र जगह दिखती थी, वह compiler binary थी, और source code में वह सिर्फ "\\n" -> "\n" जैसे रूप में ही मौजूद थी

  • यह बात मेरी समझ से बाहर है
    \n को value 10 वाले byte के रूप में encode क्यों किया जाता है, यह जानने के लिए इतनी लंबी यात्रा क्यों करनी पड़े, समझ नहीं आता
    यह तो स्वाभाविक बात लगती है, और न लेखक न ही टिप्पणियाँ इसे समझाती हैं, इसलिए मुझे खुद ही मूर्ख जैसा महसूस होता है

    • असली मुद्दा यह पूछना है कि उस byte को value 10 के रूप में encode करने वाला “कौन” है
      अगर parser लिखते समय newline को escape sequence \n के रूप में parse किया जाता है, तो value 10 कहाँ से आई
      अगर newline को integer literal 10 के रूप में parse किया जाता है, तो असली binary value 1010 कहाँ से आई
      इस thought experiment का अंतिम उद्देश्य मशहूर Reflections On Trusting Trust व्याख्यान की तरह compiler के बारे में हमारी समझ बदलना है
      यानी compiler सिर्फ program output करने वाली कोई चीज़ नहीं है, बल्कि program का input भी है
      compiler खुद भी एक program है, इसलिए उस compiler को बनाने वाला compiler मौजूदा compiler का input था, और transitive sense में मेरे program का भी input बनता है
      और यह compiler के compiler के compiler, और उसके ऊपर के compiler तक लगातार चलता जाता है
    • दिलचस्प बात यह है कि value 10 Rust source code के अंदर define नहीं है, बल्कि compiler से compiler तक मौखिक परंपरा की तरह चली आती है
    • अगर आपको Rust compiler को बिल्कुल शून्य से फिर से build करना हो और आपके पास सिर्फ rustc source code हो, तो source code में कहीं भी यह जानकारी नहीं है कि '\n' वास्तव में किससे map होता है
      यह Ken Thompson hack का एक दिलचस्प वास्तविक उदाहरण है
    • मूल सवाल यह है कि आख़िर 10 ही क्यों
      9 या 11 क्यों नहीं
      code कहता है, “newline character string दिखे तो newline character output करो”
      लेकिन compiler को यह कैसे पता चलता है कि newline character है क्या
      उस compiler का code भी फिर सिर्फ इतना ही कहता है, “newline character string दिखे तो उसे newline character की तरह treat करो”
      इंसान “C string escape code” खोज लेता है, लेकिन वह table compiler के अंदर कहीं भी नहीं है
      अगर C 2025 में Start of Heading को \h के रूप में define कर दिया जाए, तो क्या 'h' => cooked.push('\h') जादुई तौर पर काम करने लगेगा
      आख़िर यह कैसे पता चलेगा
      स्पष्ट है कि किसी बिंदु पर किसी ने 'n' => 10 mapping को हाथ से program किया होगा; सवाल सिर्फ यह है कि वह जगह कहाँ है
  • शायद C की वजह से, मैं हमेशा \0??? को octal escape मानता रहा हूँ
    इसलिए मेरे दिमाग में \012, \x0a या 0x0a है, और \010, 0x08 है
    इसी वजह से यह लेख मुझे काफ़ी उलझाने वाला लगा
    शायद OCaml में octal नहीं बल्कि decimal escapes होते हों, और \09 tab character हो सकता है
    मैंने जाँच नहीं की

    • उस दिशा में कुछ हद तक बात सही है, लेकिन इसका backslash escape से संबंध नहीं है
      backslash escapes प्रतीकात्मक/स्मरण-सहायक होते हैं, इसलिए \n का मतलब “[Ne]wline”, \r का “carriage [R]eturn”, और \t का “[T]ab” जैसा है
      इसकी बजाय ^C (interrupt), ^G (bell), ^M (carriage return) जैसी control character परंपराएँ देखनी चाहिए
      ये C0 control character set में होते हैं, और ^C, \0x3 है, ^G, \0x7 है, ^M, \0xD है
      Unix से भी पहले से चली आ रही एक चतुर पद्धति में, terminal अदृश्य ASCII C0 characters को दिखाने के लिए आगे ^ जोड़ते थे और संबंधित character पर AND-0x40 लागू करके उसे दिखने वाली range में खिसकाकर output करते थे
      इसे समझने के लिए https://www.asciitable.com जैसी ASCII table खोलकर देखना उपयोगी होगा
      हर control character उस table में दो खाने दूर वाले ^character से map होता है
      इसलिए \0 को अजीब तरह से ^@ लिखा जाता है, Esc key को ^[ कहा जाता है, वगैरह—ऐसे equivalences जिन्हें याद रखना मुश्किल है
      यह Unix लेखकों की पसंद नहीं, बल्कि ASCII numbering scheme का परिणाम है
    • वास्तव में OCaml decimal escapes का उपयोग करता है: https://ocaml.org/manual/5.2/lex.html#char-literal
    • backslash-decimal character escapes सचमुच दुर्लभ हैं
      जिन string syntaxes के बारे में मुझे पता है, उनमें OCaml, Lua, DNS ही ऐसे हैं
  • गलत capitalization की वजह से मुझे एक पल को लगा कि शायद \n से अलग कोई लगभग अनजान escape sequence \N भी है
    मैंने सोचा था कि शायद यह newline नहीं बल्कि किसी भी character से match करता होगा, लेकिन ऐसा नहीं था; यह मूल लेख की small caps styling की वजह से था

    • source view में देखें तो वह वास्तव में \n ही है, लेकिन इस CSS rule की वजह से वह ऐसा नहीं दिखता
      .title { font-variant: small-caps; }
    • वास्तव में \N का उपयोग कुछ जगहों पर होता है
      बहुत-से systems CSV या उससे मिलते-जुलते formats में empty string से अलग दिखाने के लिए \N को NULL के रूप में इस्तेमाल करते हैं
      इसलिए मुझे लगा था कि यह लेख उसी बारे में होगा
    • Python में \N escape sequence है
      यह नाम के आधार पर Unicode character insert करता है
      उदाहरण के लिए, '\N{PILE OF POO}' एक Unicode string है जिसमें poo emoji का एक character होता है
      \u या \U के साथ hexadecimal sequence लिखने की तुलना में यह कहीं ज़्यादा self-descriptive है
    • मैंने भी इसी वजह से यह लेख खोला था
      फिर भी, पढ़ने में मज़ा आया
  • इस लेख को प्रेरित करने वाला “दूसरा लेख” शायद यही होगा
    https://research.swtch.com/nih

    • यहाँ इस पर चर्चा हुई थी
      Running the "Reflections on Trusting Trust" Compiler - https://news.ycombinator.com/item?id=38020792 - अक्टूबर 2023, 67 टिप्पणियाँ