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 टिप्पणियां
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 तक ट्रेस किया जा सकता है।
'\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 हो सकता है।
quine पर काफ़ी research, papers और explanations हैं, इसलिए संभव है कि लेखक ने उसी तरह की कोई चीज़ पढ़ी हो।
https://en.wikipedia.org/wiki/Quine_(computing)
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 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 पर हार्डकोडेड मिलाProcessCharEscapestandard C escape sequences को parse करता है, औरcase 'n': ResultChar = 10; break;की तरह handle करता हैgcc/libcpp/charset.ccमें यह हार्डकोडेड है, और ASCII या EBCDIC में से एक चुनता है\a \b \e \f \n \r \t \vके valuescharconstsarray में रखे जाते हैं; 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 क्यों किया जाता है, यह जानने के लिए इतनी लंबी यात्रा क्यों करनी पड़े, समझ नहीं आतायह तो स्वाभाविक बात लगती है, और न लेखक न ही टिप्पणियाँ इसे समझाती हैं, इसलिए मुझे खुद ही मूर्ख जैसा महसूस होता है
अगर parser लिखते समय newline को escape sequence
\nके रूप में parse किया जाता है, तो value 10 कहाँ से आईअगर newline को integer literal
10के रूप में parse किया जाता है, तो असली binary value1010कहाँ से आईइस 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 तक लगातार चलता जाता है
'\n'वास्तव में किससे map होता हैयह Ken Thompson hack का एक दिलचस्प वास्तविक उदाहरण है
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' => 10mapping को हाथ से program किया होगा; सवाल सिर्फ यह है कि वह जगह कहाँ हैशायद C की वजह से, मैं हमेशा
\0???को octal escape मानता रहा हूँइसलिए मेरे दिमाग में
\012,\x0aया0x0aहै, और\010,0x08हैइसी वजह से यह लेख मुझे काफ़ी उलझाने वाला लगा
शायद OCaml में octal नहीं बल्कि decimal escapes होते हों, और
\09tab character हो सकता हैमैंने जाँच नहीं की
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 का परिणाम है
जिन string syntaxes के बारे में मुझे पता है, उनमें OCaml, Lua, DNS ही ऐसे हैं
गलत capitalization की वजह से मुझे एक पल को लगा कि शायद
\nसे अलग कोई लगभग अनजान escape sequence\Nभी हैमैंने सोचा था कि शायद यह newline नहीं बल्कि किसी भी character से match करता होगा, लेकिन ऐसा नहीं था; यह मूल लेख की small caps styling की वजह से था
\nही है, लेकिन इस CSS rule की वजह से वह ऐसा नहीं दिखता.title { font-variant: small-caps; }\Nका उपयोग कुछ जगहों पर होता हैबहुत-से systems CSV या उससे मिलते-जुलते formats में empty string से अलग दिखाने के लिए
\Nको NULL के रूप में इस्तेमाल करते हैंइसलिए मुझे लगा था कि यह लेख उसी बारे में होगा
\Nescape 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 टिप्पणियाँ