8 पॉइंट द्वारा GN⁺ 2023-08-20 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • compiler-टाइप tools की implementation language पारंपरिक रूप से OCaml और C++ में बंटी रही है, लेकिन छोटे language experiments में TypeScript, ML-family languages की तरह हल्के-फुल्के तरीके से इस्तेमाल होने वाला विकल्प बन सकती है
  • Rust, ML और C++ की खूबियों को जोड़ता है और safe multithreading तक देता है, लेकिन data के physical layout को model करना पड़ता है, इसलिए छोटे prototypes के लिए बोझ बढ़ सकता है
  • Deno single binary, built-in linting और formatting, compile step न होना, task runner और watch mode की वजह से TypeScript language experiments जल्दी शुरू करने देता है
  • example typechecker generic AST, tagged union, visitor, bottom-up transform को मिलाकर Expr<void> को Expr<Type> में बदलता है, और TypeError को type value के रूप में डालकर cascading errors कम करता है
  • TypeScript autocomplete, flexible type system, और ज़रूरत पड़ने पर dynamic तरीके में उतर सकने वाली runtime प्रकृति की वजह से छोटे language hacking tools के लिए productive हो सकती है

implementation language चुनने की पुरानी तस्वीर

  • compiler जैसे tools बनाते समय implementation language चुनने में broadly दो धाराएँ रही हैं
    • formal specification या hobby toy language जैसे language-centric कामों के लिए OCaml अच्छी तरह फिट बैठती है
    • उदाहरण के तौर पर plzoo और WebAssembly reference interpreter हैं
  • implementation-centric और production-ready होने की ज़रूरत वाले कामों में अक्सर C++ चुनी जाती है
    • LLVM, clang, v8, HotSpot सभी C++ based हैं
  • Rust पर ML और C++ का सीधा प्रभाव रहा है, यह दोनों languages की खूबियों को जोड़ती है और safe multithreading जैसी अपनी ताकतें भी देती है
    • हालांकि spectrum पर यह production readiness की तरफ ज्यादा झुकी हुई है
    • “बस चल जाने वाला” build system prototyping में भी मदद करता है, लेकिन data के physical layout को model करने की अतिरिक्त complexity साथ आती है

Rust के index approach के फायदे और छोटे code का बोझ

  • Rust में compiler बनाते समय आम सलाह pointer से बचकर index इस्तेमाल करने की होती है
  • index बड़े codebase में कई फायदे देते हैं
    • side tables को संबंधित module के अंदर रखा जा सकता है, जिससे coupling घटती है
    • index u32 होता है और struct-of-arrays layout को बढ़ावा देता है, इसलिए performance के लिए अच्छा है
    • serialization या incremental compilation framework से जोड़ना आसान होता है, जिससे computation strategy flexible होती है
  • लेकिन छोटे स्तर की programming में index approach खुद ही झंझट बन जाती है, और hobby experiments में यह बोझ घातक हो सकता है
  • OCaml में पुराना-सा feel बचा हुआ है, और इसी context में देखा गया कि TypeScript को ML के equivalent alternative की तरह इस्तेमाल किया जा सकता है या नहीं

Deno और TypeScript से मिलने वाला तेज experiment environment

  • starting environment के रूप में deno इस्तेमाल किया गया
    • यह TypeScript को सीधे इस्तेमाल करने का out-of-the-box experience देता है
    • OCaml में यही हिस्सा pain point है, और Rust, OCaml या C++ से बेहतर है, लेकिन Deno Rust से भी ज्यादा simple experience देता है
  • Deno का development experience छोटे PLT hacking के लिए अच्छी तरह फिट बैठता है
    • यह single binary है
    • linting और formatting built-in हैं
    • अलग compile step नहीं है
    • built-in task runner और watch mode हैं
  • TypeScript खुद पर्याप्त flexible होते हुए भी syntax burden हल्का रखने वाला type system देता है

छोटे typechecker example से दिखने वाले TypeScript patterns

  • AST file location जानकारी वाली expression से शुरू होता है
    • Location में file, line, column होते हैं
    • TypeScript में string बस string और number बस number होता है, इसलिए usize और u32 जैसे फर्क की चिंता नहीं करनी पड़ती
  • expression में location और kind को अलग किया जाता है, और बाद में Expr<T> रूप में associated data को generic बनाया जाता है
    • parsing के तुरंत बाद expression में void data होता है
    • typechecker द्वारा process की गई expression में Type data होता है
    • type inference function ast.Expr<void> लेता है और ast.Expr<Type> लौटाता है
  • TypeScript runtime behavior अपने-आप add नहीं करता, इसलिए union type match करने के लिए runtime type information खुद डालनी पड़ती है
    • tag: "binary", tag: "if" जैसे fields यही भूमिका निभाते हैं
    • tag: "binary" का मतलब runtime पर उस value में सिर्फ string "binary" ही संभव है
  • boolean literal और int literal लगभग एक जैसे shape के हैं, इसलिए इन्हें ExprLiteral<T, V, Tag> से abstract किया जाता है
    • ExprBool<T> है ExprLiteral<T, boolean, "bool">
    • ExprInt<T> है ExprLiteral<T, number, "int">
  • type values TypeBool, TypeInt में बंटे हैं और singleton values भी उसी नाम से दी जाती हैं
    • TypeScript types को पूरी तरह erase कर देता है, इसलिए type-related names और value-related names अलग namespaces में मौजूद होते हैं
    • इस property का उपयोग करके type और value को एक ही नाम से define किया जा सकता है

Visitor, transform, error type, desugaring

  • TypeScript का switch expression नहीं बल्कि statement है, इसलिए expression kind को सुविधाजनक तरीके से handle करने के लिए visitor define किया जाता है
    • bool, int, binary, if methods अपने-अपने kind को handle करते हैं
    • editor autocomplete switch case और visitor implementation में मदद करता है
  • transform<U, V> एक generalized traversal function है जो Expr<U> को Expr<V> में बदलता है
    • transformation bottom-up तरीके से चलती है
    • internal node visit करते समय child expressions पहले ही transform हो चुकी होती हैं, इसलिए visitor type Visitor<U, V> नहीं बल्कि Visitor<V, V> बनता है
  • TypeScript dynamic typed language भी है, इस बात का उपयोग करके Object.keys से और general traversal बनाया जा सकता है
    • इस case में भी static function signature बनाए रखा जा सकता है
    • example में यह जरूरी नहीं है, लेकिन जरूरत पड़ने पर dynamic तरीके में उतरने की गुंजाइश है
  • type errors को side effect के रूप में array में accumulate नहीं किया जाता, बल्कि TypeError type से represent किया जाता है
    • Type बनता है TypeBool | TypeInt | TypeError
    • TypeError में tag: "Error", location, message होते हैं
    • type_equal किसी भी एक side के Error होने पर true लौटाता है, ताकि cascading failures रोके जा सकें
  • final typechecker binary और if को check करता है
    • binary expression में left-right operands के types अलग हों तो "binary expression operands have different types" error लौटाता है
    • if expression में condition boolean न हो तो "if condition is not a boolean" error लौटाता है
    • then और else branch के types अलग हों तो "if branches have different types" error लौटाता है
  • result में कुछ हद तक typing की जरूरत होती है, लेकिन autocomplete काफी भरपाई कर देता है, language से लड़ने जैसा कम लगता है और problem की shape से naturally मेल खाता है
  • TypeScript छोटे language hacking tool के रूप में productive क्यों है, इसे तीन बातों में समेटा जा सकता है
    • Deno छोटा, self-contained, powerful, और effective development workflow के लिए optimized scripting runtime है
    • TypeScript tools में IDE उपयोगी और productive है, और Deno की वजह से setup की जरूरत नहीं पड़ती
    • language runtime और compile time दोनों तरफ powerful है, types से काफी sophisticated expression कर सकती है, फिर भी जरूरत पड़ने पर dynamic तरीके में उतर सकती है
  • अतिरिक्त idea के रूप में बहुत सारे syntax sugar को type-safe तरीके से desugar करने की approach भी संभव है
    • Expr और ExprKind को associated data के बजाय पूरे ExprKind पर recursively parameterize किया जाता है
    • ExprKindCore basic expressions के set को represent करता है
    • ExprKindSugar basic expression या ऐसे expressions शामिल करता है जिन्हें basic expression में desugar किया जा सकता है
    • desugar(expr: ExprSugar): ExprCore syntax sugar expressions को core expressions में घटाता है
    • desugar_one(expr: ExprKindSugar<ExprCore>): ExprKindCore<ExprCore> child expressions के पहले ही desugar हो चुके होने की स्थिति में एक-step transformation करता है

1 टिप्पणियां

 
GN⁺ 2023-08-20
Hacker News की राय
  • TypeScript कुल मिलाकर एक शानदार भाषा है, और यह बात कम आंकी जाती है कि functions ऐसे objects होते हैं जिनके properties/methods हो सकते हैं
    functions की array को commands की तरह execute करते हुए बाद में help जैसी description जोड़ी जा सकती है, या closures/partial application से state जोड़ी जा सकती है, इसलिए Command जैसी class जल्दबाज़ी में define करने की ज़रूरत नहीं पड़ती
    object-oriented programming में चीज़ों को बहुत जल्दी नाम दे देने से कई टकराव पैदा होते हैं, और VideoCompressor#compress() जैसी structure की बजाय ज़रूरी values को function में pass करना ज़्यादा natural लगता है

    • ठीक-ठीक कहें तो यह TypeScript से ज़्यादा JavaScript की feature है
      functions जैसे behave करने वाले objects support करने वाली दूसरी languages https://en.wikipedia.org/wiki/Function_object पर सूचीबद्ध हैं
    • Go में functions पर भी methods लगाए जा सकते हैं, और असल में लगभग किसी भी type पर ऐसा किया जा सकता है
      net/http के handler की तरह कोई struct serve method implement कर सकता है, और कोई handler function खुद को call करके interface satisfy कर सकता है
      Clojure में function var पर metadata लगाकर इसी तरह handle किया जा सकता है, और Lisp होने की वजह से macros से लगभग कुछ भी संभव है
      साथ ही core/async के CSP channels execution और communication को अलग करते हैं, जिससे callbacks/promises/async/await जैसी function color problem से बचा जा सकता है, और commands ऐसे producers की तरह काम कर सकते हैं जो results channel में भेजते हैं
    • Java के single-method interface से भी वैसा ही effect हासिल किया जा सकता है
      method का नाम जो भी हो, function context में इस्तेमाल किया जा सकता है, और concrete method name को refer करने की ज़रूरत नहीं होती
      हालांकि मुझे यह पसंद नहीं कि function properties रखकर state अपने अंदर समेटे, और वही arguments देकर call करने पर भी behavior बदल सके। functional programming का बड़ा फायदा object-oriented style की state से बाहर निकलने में है
    • Python में सब कुछ object है, इसलिए यह दिशा अच्छी तरह support होती है
    • C# में भी return value वाले function को Func और return value न होने वाले function को Action से similar तरीके से express किया जा सकता है
      JavaScript के lambda expressions C# से काफी मिलते-जुलते हैं, और TypeScript तथा C# के function signatures भी काफी similar हैं
      JavaScript, TypeScript, C# की similarities दिखाने वाला एक छोटा repository भी है: https://github.com/CharlieDigital/js-ts-csharp
      वही logic JS/TS/C# में साथ-साथ दिखाने वाला screenshot: https://github.com/CharlieDigital/js-ts-csharp/blob/main/js-...
  • यह बहुत हैरान करने वाली बात नहीं है। TypeScript भी आखिरकार एक और ऐसी language है जिसने ML family की काफी features को मुश्किल से अपनाया है
    असली pattern matching न होने से यह OCaml से ज़्यादा असुविधाजनक है, लेकिन C#, Swift, Dart, Kotlin जैसी languages से तुलना करें तो ठीक-ठाक level पर है

    • यह तुलना बहुत high-level पर है। असली usage experience काफी अलग है
      TypeScript का type system powerful है, लेकिन underlying standard library और language खुद कुछ कमज़ोर लगते हैं, और pattern matching/switch expressions नहीं हैं
      Dart का object model closed है, इसलिए dynamic freedom कम है, type system और expressions भी ज़्यादा कमजोर हैं, और metaprogramming facilities लगभग नहीं हैं, इसलिए Java-style boilerplate और code generators पर निर्भर रहना पड़ता है
      C# बताई गई languages में ML features के सबसे करीब है, लेकिन TypeScript के उलट इसमें sum types नहीं हैं, जिससे कई काम ज़्यादा झंझट भरे हो जाते हैं
    • पहले Haskell में बनाए गए Pascal-C style learning compiler में, parser combinators ने grammar को BNF जैसा code में सीधे express करना संभव बनाया था
      उदाहरण के लिए { ... } पहचानने वाले parser को दूसरे parsers के साथ combine किया जा सकता है, और statement को control flow/declaration/assignment में से किसी एक के रूप में define किया जा सकता है
      ML-style list processing और pattern matching intermediate representation संभालते समय बेहद expressive थे
    • TypeScript में pattern matching की ज़रूरत होने पर मैं यह library पसंद से इस्तेमाल करता हूं: https://github.com/gvergnaud/ts-pattern
    • यह कहना मुश्किल है कि TypeScript ने ML features का ज़्यादातर हिस्सा अपना लिया है
      article में भी switch expression नहीं था, इसलिए visitor pattern से workaround करना पड़ा, और JavaScript का iterator support भी अजीब तरह से अधूरा है
      .map() है, लेकिन यह सिर्फ arrays पर काम करता है, सामान्य iterators पर सीधे apply नहीं किया जा सकता
    • TypeScript और kotlin-js इस्तेमाल करके देखें तो दोनों languages काफी करीब लगती हैं
      फर्क बहुत हैं, लेकिन switch करना मुश्किल नहीं है, और व्यक्तिगत तौर पर Kotlin को ज़्यादा पसंद करने के बावजूद दोनों इस्तेमाल किए जा सकते हैं
      सोचता हूं कि अगर TypeScript JavaScript compatibility से निकलकर WASM में compile होने लगे तो कैसा होगा। Kotlin WASM compiler जोड़ रहा है और उसके पास पहले से JS transpiler भी है, लेकिन वही code WASM में ज़्यादा छोटा और तेज़ी से load होता है
      browser JavaScript अच्छा compile target नहीं है, और जैसे-जैसे नए projects शुरू से TypeScript में शुरू हो रहे हैं, existing JavaScript से आसानी से transition करना चाहिए—यह वजह भी धीरे-धीरे कमज़ोर होती जा रही है
  • Rust और TypeScript के बीच आते-जाते यह बहुत साफ़ दिखता है कि tagged enums जैसी features की कितनी कमी है
    ADT enum proposal रुका हुआ लगता है, जानना चाहता हूं कि कोई और effort चल रहा है या नहीं: https://github.com/Jack-Works/proposal-enum/discussions/19

    • similar use cases के लिए discriminated unions काफी अच्छे से काम करते हैं
  • TypeScript का type system दिलचस्प है, लेकिन अगर compiler किसी compiled language में लिखा गया होता तो कितना ज़्यादा तेज़ होता, यह सोचने पर मजबूर करता है
    बेशक इसके लिए “अच्छी implementation” वाली बड़ी शर्त ज़रूरी है

    • swc और esbuild अच्छे comparison points नहीं हैं। speedup का काफ़ी हिस्सा इसलिए है क्योंकि वे TypeScript-specific syntax हटाकर JavaScript बनाते हैं
      tsc आम तौर पर सिर्फ़ पहली run में धीमा होता है, और incremental flag या --transpile-only watch mode इस्तेमाल करने पर compile time अक्सर 100ms से नीचे आ जाता है, जिससे SWC या ESBuild से महसूस होने वाला फर्क लगभग खत्म हो जाता है
    • TypeScript team का जवाब मौजूद है: https://twitter.com/drosenwasser/status/1260723846534979584
      उनका कहना है कि अगर medium-size program की type checking में 20 सेकंड लगते हैं, तो अक्सर वजह JS होना नहीं, बल्कि types में combinatorial explosion होना होता है
      दूसरे runtimes parallelism या startup time में फायदा दे सकते हैं, लेकिन उन्होंने ऐसा CPU-focused benchmark नहीं देखा जो कुल मिलाकर 20x speedup को support करे
    • type system तब तक मज़ेदार है जब तक आप generics में उसका पूरा फायदा उठाने नहीं लगते
      फिर type logic की 5 lines को पूरा दिन debug करने के बाद आप खुद से पूछने लगते हैं कि मैं यहाँ तक आ कैसे गया
    • अब सिर्फ़ कल्पना करने की ज़रूरत नहीं है: https://github.com/dudykr/stc
      यह SWC के मुख्य developer द्वारा Rust में लिखा गया project है; SWC TS को JS में compile करता है और STC TS types की checking करता है
    • हाल के समय में TypeScript compiler performance काफ़ी बेहतर हुई है
      कहा जा रहा है कि आने वाला isolated declaration mode compile time को 75% तक घटा सकता है: https://github.com/microsoft/TypeScript/pull/53463#issuecomm...
  • compiler सीखना शुरू कर रहे लोगों के लिए मैं यह किताब recommend करूंगा: https://keleshev.com/compiling-to-assembly-from-scratch/
    लेखक TypeScript के subset से 32-bit ARM assembly compiler बनाते हैं, और बताते हैं कि यह लगभग pseudocode जैसा दिखता है इसलिए approachable है

    • Crafting Interpreters की भी काफ़ी strongly recommendation है: https://craftinginterpreters.com/
      किताब दो हिस्सों में बंटी है; पहले में Java से language interpreter बनाया जाता है, और दूसरे में उसी language को bytecode में compile करने के बाद C में bytecode virtual machine implement की जाती है
      implementation की हर code line किताब में reference की गई है
  • visitor pattern से बचना हो तो run utility function के साथ IIFE-style switch इस्तेमाल किया जा सकता है
    यानी तुरंत execute होने वाले function के अंदर switch इस्तेमाल करें और return type infer होने दें

    • मैंने इस pattern पर कभी एक लेख लिखा था: https://maxgreenwald.me/blog/do-more-with-run
    • असल में उतना भी ज़रूरी नहीं है; बस function को तुरंत call कर दें तो return type infer हो जाता है
      अगर अंत का () पसंद नहीं है और IIFE से बचना चाहते हैं, तो function अलग से define करके फिर call कर सकते हैं
  • मैं TypeScript में compiler लिख रहा हूँ, और इस बात से सहमत हूँ कि यह उम्मीद से बुरा नहीं है
    शुरुआत में लेखक की तरह Deno से शुरू किया था, लेकिन आखिर में Bun पर आ गया; कुछ rough edges हैं, फिर भी Deno से ज़्यादा संतोषजनक और बहुत तेज़ है
    standard parser generator frontend के तौर पर Ohm-js काफ़ी pleasant है: https://ohmjs.org/
    official tsc compiler इतना बड़ा है कि उसे पढ़ने की सलाह नहीं दूंगा, और tsc कैसे काम करता है यह देखने के लिए mini-typescript बेहतर है: https://github.com/sandersn/mini-typescript/
    खास तौर पर centi-typescript branch मददगार है: https://github.com/sandersn/mini-typescript/tree/centi-types...
    उम्मीद है WASM में GC और DOM access संभव हो पाएगा

    • Bun पर जाने की वजह जानने की उत्सुकता है
  • मुझे लगा था TypeScript में interfaces की वजह से extra overhead ज़्यादा होगा, इसलिए यह हैरानी की बात है
    सोचता हूँ कि क्या इसे दूसरे क्षेत्रों में भी apply किया जा सकता है, जैसे language parsing के लिए भी ठीक रहेगा या नहीं

    • runtime पर interfaces का overhead 0 होता है
      compile process में वे पूरी तरह गायब हो जाते हैं
  • नतीजा बहुत चौंकाने वाला नहीं है। क्योंकि TypeScript compiler खुद TypeScript में लिखा गया है
    ML के रूप में TypeScript पहले से ही रोज़ heavy production environments में validated हो रहा है

  • मैंने C# में compiler लिखा है, और यहाँ जो खास दिखता है वह बस union types जैसा है
    निजी तौर पर मैंने visitor pattern की verbosity से बचने का फैसला किया, और compile-time completeness checking के लिए closed enum feature का इंतज़ार कर रहा हूँ

    • union types या Rust-style enums के बिना किसी language का इस्तेमाल करने पर उनकी बहुत कमी महसूस होती है
      आम तौर पर alternatives awkward होते हैं। किसी sum type को represent करने वाली class में N nullable properties रखकर “हमेशा सिर्फ़ एक ही non-null होगा” जैसी documented condition पर निर्भर रहना, या कई classes को common class से inherit कराना—दोनों ही भारी लगते हैं
      overlapping union types बनाने हों तो दोनों तरीकों में duplication या clever combinations की ज़रूरत पड़ती है