1 पॉइंट द्वारा GN⁺ 3 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Jam v1.0 से पहले के चरण की एक भाषा है, जिसका लक्ष्य C-परिवार की भाषाओं जैसी तुरंत उपयोग में आने वाली फीलिंग बनाए रखते हुए, GC के बिना सुरक्षा, कम learning curve और high performance को साथ लाना है
  • इसका केंद्र mutable value semantics और Rust-शैली का drop system है; compiler user code में reference या lifetime syntax दिखाए बिना ownership, borrow और automatic cleanup संभालता है
  • initialization model undefined और implicit zero initialization, दोनों से बचता है, और delayed initialization व out-parameter को Maybe(T) तथा unsafeAssumeInit() analysis से संभालता है
  • export Jam functions को C ABI के रूप में expose करता है और Jam struct को C-compatible layout रखने के लिए design किया गया है, ताकि अलग unsafe shim या repr annotation का बोझ कम हो
  • compiler अभी C++ में implemented bootstrap चरण में है और public release से पहले है; 108 distinct project Jam में बनाने के बाद इसे open source करने की योजना है

Jam जिस भाषा-स्थान को target करता है

  • Jam अभी भी v1.0 से पहले है, और फिलहाल बताए गए mechanisms compiler में काम करते हैं, लेकिन stabilization से पहले details बदल सकती हैं
  • लक्ष्य Go, Zig, modern C जैसी तुरंत समझ आने वाली C-family फीलिंग बनाए रखते हुए C की bug classes को कम करने वाली सुरक्षित भाषा बनाना है
  • design के दो केंद्रीय axis हैं
    • Racordon, Abrahams et al. 2022 की Mutable value semantics
    • Rust का drop system
  • वास्तविक टीमों में skill levels मिले-जुले होते हैं और कम अनुभवी members से गलती होने की संभावना अधिक होती है, इसलिए भाषा को review से पहले अधिक errors रोकने चाहिए—यही समस्या-बोध इसकी शुरुआत है

Rust, Zig, C++ से अंतर

  • Rust की safety philosophy मजबूत है, लेकिन “Rust कुछ हद तक इस्तेमाल कर पाना” और “Rust में productive होना” के बीच का gap बड़ा है, जिससे टीमों के लिए learning curve बोझ बन सकता है
  • Zig एक C-like भाषा के करीब छोटा surface area और तुरंत समझ आने वाला mental model देता है, लेकिन language level पर safe language नहीं है
    • uninitialized read, manual cleanup, use-after-free की रोकथाम language level पर enforced नहीं है
    • बड़े Zig या C++ production projects Valgrind, AddressSanitizer, fuzzing जैसे verification tools पर काफी निर्भर करते हैं
  • AI युग में production code का बड़ा हिस्सा इंसान के बजाय tools से लिखा या draft किया जाता है, और bottleneck code writing से code review की ओर shift होता माना जाता है
    • code volume बढ़ता है और review surface flat रहता है, इसलिए compiler को अधिक bugs पकड़ने चाहिए

Automatic drop system

  • Jam में binding value का मालिक होता है, और drop-bearing type की binding scope से बाहर जाती है तो compiler drop call synthesize करता है
  • उदाहरण में File type fn drop(self: mut File) declare करता है, और useFile() में केवल const f: File = { fd: 7 }; लिखा जाता है
    • explicit cleanup, defer, lifetime समाप्ति marker नहीं है
    • LLVM IR में ret से ठीक पहले call void @__drop_File(ptr %1) generate होता है
  • mangled name __drop_File कई types के drop functions को LLVM level पर collide होने से बचाता है
  • self: mut File pointer parameter में lower होता है, और call site binding address सीधे pass करता है
  • Zig में उसी cleanup के लिए defer f.deinit() explicitly लिखना पड़ता है
    • वह line हटाने पर IR का deinit call भी गायब हो जाता है
    • file descriptor leak तब होता है जब programmer cleanup याद नहीं रखता
  • C++ RAII भी scope exit पर destructor auto-run करता है, लेकिन Jam Rust का simpler drop model अपनाता है
    • C++ के rule of 0/3/5, virtual destructor, constructor exception, destructor exception, std::exit, std::abort, longjmp, signal जैसी complexities से बचने की दिशा है
    • Jam हर type के लिए एक drop function रखता है और उसे हर scope exit पर run करता है

Initialization और Maybe(T)

  • Jam में undefined value नहीं है, और binding को value के बिना declare नहीं किया जा सकता
    • हर var और const को actual initializer चाहिए
    • struct पहले field values calculate करता है, फिर struct literal से create करके binding करता है
  • Zig var f: File = undefined; return f.fd; allow करता है और runtime में stack garbage read हो सकता है
    • Debug mode में misuse दिखाने के लिए 0xaa fill डाला जाता है
    • Release mode में arbitrary bytes हो जाते हैं
  • Go garbage read रोकने के लिए हर var को zero-initialize करता है, लेकिन जल्द overwrite होने वाले fields में भी zero pattern लिखने की cost होती है
  • Jam undefined और implicit zero, दोनों से बचता है
  • delayed initialization और out-parameter के लिए Maybe(T) इस्तेमाल होता है
    • empty() अभी meaningless contents वाला slot बनाता है
    • write() slot को भरता है
    • unsafeAssumeInit() value extract करता है
  • lint pass track करता है कि slot write हुआ है या नहीं, और analyzer जिस unsafeAssumeInit() call के initialization को prove नहीं कर पाता, उसे compile error के रूप में reject करता है
    • unsafe prefix human और AI reviewer के लिए grep कर सकने वाला anchor बना रहता है

Scope exit, return, break, continue

  • compiler drop scope stack track करता है और हर lexical block boundary पर नया scope push करता है
  • block खत्म होने या branch से बाहर निकलने से ठीक पहले उस scope की binding drops emit करता है
    • if, else, match arm, while, for body के अंदर की bindings उसी block के अंत में drop होती हैं
    • nested block के अंदर return actual ret से पहले active scopes को innermost-first order में drop करता है
    • break और continue loop body में open scopes को drop करने के बाद loop exit या next iteration पर जाते हैं
  • nested break उदाहरण में outer iteration 0 के अंत में drop होता है, और iteration 1 के break path में पहले inner, फिर outer drop होता है

Parameter mode और first-class reference हटाना

  • function call में binding drop होगी या नहीं, यह parameter mode तय करता है
  • default mode read-only borrow है
    • callee value पढ़ता है और caller की binding initialized state में बनी रहती है
    • call return पर drop नहीं होता
  • mut exclusive read-write borrow है
    • caller की binding call के बाद भी initialized state में रहती है
  • केवल move value consume करता है
    • callee ownership लेता है और callee के अंत में drop होता है
    • caller की binding call के बाद Uninit हो जाती है, और उसे पढ़ना compile error है
  • call site marker नहीं है, f(x) form सभी modes में समान है
  • Jam में first-class reference type नहीं है
    • borrow को variable में store, return, या struct field में रखा नहीं जा सकता
    • parameter borrow केवल call-frame के दौरान मौजूद रहता है और call return पर expire हो जाता है
    • lifetime annotation की जरूरत इसलिए नहीं है क्योंकि attach करने के लिए lifetime ही नहीं है
  • collection API भी value-shaped बनी रहती है
    • v[i] = x v.setAt(i, x) में desugar होता है
    • let y = v[i] v.at(i) getter है जो element को value के रूप में return करता है
  • call site exclusivity check arguments से बने borrow set के path overlap की जांच करता है
    • swap(p.x, p.y) disjoint sub-paths हैं इसलिए OK
    • moveX(p, p.x) में p और p.x overlap करते हैं, इसलिए error है

C ABI और FFI

  • Rust का native ABI unstable है, इसलिए distribution boundary पार करने पर C form में फिर से encoding करनी पड़ती है
    • raw pointer dereference unsafe है
    • ownership Box::into_raw और Box::from_raw से manually transfer होती है
    • struct को by value pass करते समय #[repr(C)] जैसी अलग annotation चाहिए
    • cbindgen और abi_stable जैसे tools इस boundary पर manual work घटाने के लिए मौजूद हैं
  • Jam में first-class reference, lifetime, niche-packed layout नहीं हैं, इसलिए Jam value को पूरे रास्ते value-shaped माना जाता है
    • Jam struct पहले से ही C-compatible layout रखने के लिए design किया गया है
  • export Jam function को C calling convention के plain unmangled name में expose करता है
    • export fn counterAdd(c: mut Counter, n: i64) i64 को C से int64_t counterAdd(Counter *c, int64_t n); के रूप में call किया जा सकता है
    • mut Counter parameter caller-owned storage के लिए Counter * में lower होता है
  • Jam side function body ordinary Jam है, इसलिए drop, init analysis, call-site exclusivity rule लागू रहते हैं
  • C की ओर जाने के लिए extern से C signature declare किया जाता है
    • extern function C ABI को literally follow करता है
    • parameter-mode machinery boundary के बाहर apply नहीं होती
    • raw pointer से buffer C को pass किया जाता है, और C pointer के साथ क्या करता है, यह Jam verify नहीं करता
  • Jam जो scope देना चाहता है वह यह है कि Jam side default रूप से safe रहे, और Jam library को C ABI से expose करते समय अलग unsafe API mirror या shim layer न बनानी पड़े

Pattern matching

  • Jam का match Pattern Block form में है और => का इस्तेमाल नहीं करता
    • scrutinee match (opcode) की तरह parentheses इस्तेमाल करता है
    • _ catch-all arm है
    • arms top-to-bottom sequential first-match हैं और implicit fallthrough नहीं है
  • Game Boy emulator का opcode dispatcher इसका प्रमुख use case है
    • 256 base opcodes और 256 prefix opcodes dispatch करने वाला रूप
  • enum payload matching भी support है
    • variant pattern tag match करता है और payload field को arm के अंदर fresh local में bind करता है
    • compiler variant set पर exhaustiveness check करता है
    • नया variant जोड़ने पर उस variant को handle न करने वाली match site compile fail हो जाती है
  • match expression के रूप में भी काम करता है
    • हर arm block trailing expression की value produce करता है
    • सभी arms को same type produce करना चाहिए
    • match exhaustive होना चाहिए
  • अंदरूनी तौर पर सभी match Luc Maranget 2008 आधारित decision tree pipeline से compile होते हैं
    • integer literal cascade को LLVM simplifycfg profitable होने पर switch और jump table में fold करता है

Compile time design

  • Rust compile pipeline कई IR और analysis stages से गुजरती है
    • tokens → AST → HIR → THIR → MIR → monomorphization → LLVM IR → machine code
    • trait solving एक search problem है, और borrow checking whole-function region analysis है
    • monomorphization LLVM से पहले code volume बढ़ाता है
  • Jam pipeline को छोटा design किया गया है
    • tokens → AST → AstGen → JIR → codegen → LLVM IR → machine code
    • typed IR JIR केवल एक इस्तेमाल होता है
  • JIR AstGen बनाते समय से ही typed state में होता है
    • माना जाता है कि Jam में comptime-as-values नहीं है जो untyped lowering force करे
    • drop placement, init-before-use check, call-site exclusivity rule JIR पर local dataflow pass से किए जाते हैं
  • हर binding पर type annotation होने के कारण global type inference और open-ended trait search का बोझ कम माना जाता है
  • AST और JIR flat data structure हैं
    • small fixed-size nodes contiguous array में pack होते हैं
    • pointer के बजाय index इस्तेमाल होता है, और oversized payload side pool में store होता है
    • compiler heap-allocated tree track करने के बजाय cache-friendly array traverse करता है
  • backend में LLVM release build optimization time dominate करता है
    • debug build के लिए Cranelift और release build के लिए LLVM इस्तेमाल करने वाला split planned है
    • Cranelift roadmap में है और अभी complete नहीं है
  • वर्तमान compiler C++ implementation में language को bootstrap करने के चरण में है, और quote करने लायक build-time benchmark अभी नहीं है
    • compile-time से जुड़े claims measurement results नहीं, design claims हैं

Runtime performance और examples

  • लक्ष्य है कि Jam performance में Rust और Zig की बराबरी करे
  • Jam में GC, managed-memory runtime, per-allocation header नहीं है
    • codegen straightforward LLVM IR है
  • अभी यह नहीं माना जाता कि Jam Rust और Zig के level पर पहुंच गया है
    • Rust और Zig लंबे समय से standard library के target-specific intrinsic, auto-vectorization hint, allocator-aware container, hot path tuning, LLVM pass tuning जैसे काम करते आए हैं
    • Jam को भी आखिरी 10–30% gap कम करने के लिए उसी तरह का काम चाहिए
  • अभी measure किए गए workload में gap “अलग class” का नहीं बल्कि small constant factor के अंदर माना जाता है
  • terminal में चलने वाला Tetris demo Jam में लिखा गया है

Public release plan और बाकी काम

  • Jam अभी public नहीं है
    • compiler मौजूद है और काम करता है, लेकिन wider release से पहले है
  • day-to-day usability के लिए ये काम चल रहे हैं
    • stable surface
    • package manager
    • LSP
    • formatter
    • बाकी tooling
  • अलग posts में cover किए जाने वाले topics अभी बाकी हैं
    • parameter mode system
    • exclusivity rule
    • generics
    • Jam का comptime
    • standard library
    • allocator systems
    • panic model
    • GPU codegen pipeline के लिए MLIR exploration
    • FFI के लिए Rust ABI work
    • Cranelift
    • self-hosted compiler path
  • open source plan यह है कि Jam में 108 distinct project बनाने के बाद इसे public किया जाएगा
    • संख्या 108 Suikoden 2 के 108 Stars of Destiny से आया arbitrary milestone है
    • अभी यह small group of users को दिया गया है और tooling साथ आने पर scope बढ़ाने की योजना है
  • early access jamlang.org की beta list से मिल सकता है

1 टिप्पणियां

 
GN⁺ 3 시간 전
Lobste.rs की रायें
  • trait resolution एक search problem है। borrow checking पूरे function-scope का analysis है। monomorphization सबसे धीमे चरण LLVM के देखने से पहले ही code का आकार बढ़ा देता है…

    इस तरह की LLM-generated post वही कर रही है जिससे engineers, खासकर युवा engineers, को सावधान रहना चाहिए: quantitative data की जगह qualitative और plausible-sounding prose रख देना
    कहानी से मनाना, ठोस numbers इकट्ठा करने और उनका analysis करने से, writer और reader दोनों के लिए आसान है। इंसानी दिमाग कहानियां पसंद करता है, और कहानियां तब सबसे ज्यादा असर करती हैं जब वे simple और साफ-सुथरी हों। वास्तविक data अक्सर एक complex दुनिया को reflect करता है जिसमें जितना आप देखना चाहें उतनी nuance होती है
    इसकी तुलना rustc contributor द्वारा लिखी Rust compiler profiling पर quantitative blog post से कर सकते हैं

    • पहले तो यही सवाल उठता है कि इस project को गंभीरता से लेना चाहिए या नहीं
    • “quantitative data की जगह qualitative/evocative prose रख देना” वाली अभिव्यक्ति खास तौर पर अच्छी लगी
      अच्छी technical writing में, जहां उचित हो, दोनों हो सकते हैं और होने चाहिए, लेकिन जो सच में बताना जरूरी है उसे छोड़ना नहीं चाहिए। एक बड़ी assurance-function organization चलाकर पता चला कि technical writing कितनी कठिन है, और LLM accessibility आसान होने के साथ यह समस्या कितनी और खराब हो सकती है, इस पर सावधान रहना चाहिए
  • Zig से मुख्य फर्क यह है कि इसमें drop है, और आसानी से गलत इस्तेमाल होने वाला खास component undefined नहीं है?

    undefined नहीं है और सभी values initialize होनी चाहिए, लेकिन Maybe(T).empty() ऐसी value लौटाता है जिसकी content “अभी meaningful नहीं” है, और अगर तुरंत बाद unsafeAssumeInit() call करें तो शायद garbage value लौटाएगा। तो यह Rust की तरह safety नहीं है, जहां compiler unsafe को explicit unsafe { .. } की जरूरत वाले taint की तरह treat करता है

    safety और drop feature दिखाने वाला example यह code है:

    const File = struct {  
        fd: i32,  
        fn drop(self: mut File) {  
            close(self.fd);  
        }  
    };
    
    export fn useFile() i32 {  
        const f: File = { fd: 7 };  
        return f.fd;  
    }  
    

    अगर मैंने गलत नहीं देखा तो क्या यह unsafe नहीं है? manual file descriptor allocation को छोड़ भी दें, तो close(7) call करने के बाद 7 return करता है। lifetime tracking नहीं है, इसलिए user के पास यह express करने का तरीका नहीं है कि file descriptor की lifetime useFile() return से पहले खत्म हो चुकी है

    ABI example में जब export fn counterAdd(c: mut Counter, n: i64) i64 { .. } int64_t counterAdd(Counter *c, int64_t n); बनता है, तो यह कैसे express करेंगे कि c NULL हो सकता है या नहीं? Rust में इस हिस्से के लिए defined ABI है, और extern "C" fn counterAdd(c: &mut Counter, n: i64) -> i64 भी संभव है और extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64 भी

    Rust version को भी unsafe की जरूरत नहीं है। API को reference के साथ define किया जा सकता है। विडंबना यह है कि जहां unsafe की जरूरत हो सकती है वह सिर्फ modern Rust में #[unsafe(no_mangle)] यानी #[no_mangle] है, लेकिन example किसी वजह से Rust side पर raw pointer इस्तेमाल करने के लिए बनाया गया है

    आगे का यह example भी:

    extern fn snprintf(buf: *mut[] u8, size: u64, fmt: *const[] u8, ...) i32;
    
    fn render(value: i32) i32 {  
        var buf: [16]u8 = [0; 16];  
        return snprintf(&buf[0], 16, "n=%d", value);  
    }  
    

    यहां कहीं न कहीं unsafe होना चाहिए, नहीं? snprintf raw pointer लेता है, इसलिए पहले कही गई guideline के हिसाब से कि unsafe operations नाम से खोजे जा सकने चाहिए, शायद unsafeSnprintf और symbol redefinition जैसा कुछ होना चाहिए

    “एक ईमानदार संकेत: extern line में आप C से बात कर रहे हैं, और C के rules चलते हैं” — हम्म

    • मैंने भी इसे ऐसे ही पढ़ा। हालांकि यह Rust के .as_raw_fd() से अलग नहीं है, और वहां भी वही safety issue है
  • Rust के ABI को unstable बनाने वाली चीज Jam में मौजूद नहीं है। first-class references नहीं, lifetimes नहीं, मिटाने पड़ने वाले niche-packed layouts नहीं

    यह Rust standard library की FFI stability को गलत समझना है। shared references, mutable references, Box, और उनके Option — इन सभी के defined और stable ABI हैं। इसलिए example में Box::into_raw/from_raw की पूरी प्रक्रिया अनावश्यक है
    lifetimes binary level पर होती ही नहीं। अगर enum के लिए stable ABI define करने का चुनाव करें, तो niche optimization disable हो जाता है

    ज्यादातर types stable ABI define नहीं करते, इसका कारण यह है कि ऐसा करने पर type के internals बदल नहीं सकते, इसलिए अक्सर stable ABI चाहिए ही नहीं होता

  • Jam अभी public नहीं हुआ है। compiler मौजूद है और चलता है, लेकिन daily use के लिए अच्छा बनाने वाली चीजें—stable surface, package manager, LSP, formatter, और बाकी tools जिनकी कमी आपको तभी महसूस होती है जब वे नहीं होते—पर काम करते हुए व्यापक public release को टाल रहा है…

    यह choice समझ नहीं आती। किसी अधूरी चीज को “release” करने और बस source public करने के बीच बड़ा फर्क है। अगर बाद में वैसे भी करना है, तो project बनाते समय public करने से नुकसान क्या है?
    फायदा यह है कि जिन लोगों को direction पसंद आए, वे खुद try कर सकते हैं और शायद contribute भी कर सकते हैं। बेशक “AI के दौर” में यह स्पष्ट नहीं है कि ऐसे contributions net positive होंगे या नहीं। साथ ही लोग बेहतर समझ पाएंगे कि आप क्या बना रहे हैं, और यह evaluate कर पाएंगे कि इसके great होने के claims कितने सही हैं। ऐसा न कर पाने पर project काफी कम interesting हो जाता है

    ऊपर से कुछ लोग ऐसे tools बिल्कुल इस्तेमाल नहीं करते। अभी मेरी team auto formatter अपनाने पर भी सहमत नहीं हो पा रही, लेकिन बाकी सब बढ़िया है। इसलिए ऐसे tools बनाते समय public release टालना बहुत फर्क नहीं डालता

  • लोग लगातार “बिना झंझट वाली lifetimes वाला Rust” बनाने की कोशिश करते हैं और लगातार असफल होते हैं। दूसरे comment में असफलता के एक pattern की बात की गई थी; drop की गई value के किसी हिस्से को return करने वाली समस्या इसलिए आती है क्योंकि reference return नहीं किया जा सकता। एक और classic समस्या यह है:

    let mut arr = vec![1];  
    let x = &arr[0];  
    arr.push(2);  
    // `x` को इस्तेमाल करें तो क्या होगा?  
    

    जवाब तीन हैं:

    1. इसे reject करें। ऐसा करने के लिए किसी न किसी तरह की borrowing की अवधारणा चाहिए। आम तौर पर shared XOR mutable होता है; सिर्फ mutable हो तो असुविधाजनक है और सिर्फ shared हो तो unsafe
    2. इसे allow करें। क्योंकि किसी दूसरे variable के जरिए reference मौजूद नहीं है और सब कुछ GC या reference-counted pointer है
    3. इसे allow करें और runtime पर undefined behavior पैदा करें

    इन तीनों में से किसी को भी चुनने के अपने अच्छे कारण हो सकते हैं, लेकिन Jam Rust की तरह नंबर 1 होना चाहता है, जबकि असल में value semantics की वजह से नंबर 2 जैसा लगता है। अगर इसका मतलब है कि सब कुछ copy होता है, तो यह safe और efficient data structures लिखने से रोक सकता है

    • मेरे हिसाब से Inko काफी अच्छा कर रहा है। हालांकि मेरी स्पष्ट bias को अलग रखकर देखना चाहिए, लेकिन इसके अपने trade-offs भी साफ हैं
      खासकर borrow checker छोड़ देने पर, कई संकेत introduce किए बिना stack-allocated types को support करना कहीं ज्यादा मुश्किल हो जाता है। उदाहरण के लिए borrow करते समय copy करना—Inko और Swift दोनों ऐसा करते हैं
    • Jam के बारे में निश्चित नहीं, लेकिन Hylo-स्टाइल mutable value semantics में subscripts नाम का borrowing का एक रूप है। इसलिए यह कुछ ज्यादा middle ground के करीब है
    • उस हिस्से को पढ़ते समय मेरे मन में पहला सवाल आया: “अगर reference भी नहीं और lifetime annotations भी नहीं, तो struct के अंदर reference कैसे store करेंगे?”
      language reference देखने पर references तो नहीं हैं, लेकिन mut और const pointers हैं, और उनकी safety के बारे में कुछ नहीं मिला
  • Zig को Zig जैसा बनाने वाली बड़ी चीज RAII का न होना है, और Rust की पहचान borrow checker है। लेकिन इन design choices से जिस जगह पहुँचा गया है—“reference-less RAII”—उसकी वास्तव में जरूरत किसे है, यह मुझे समझ नहीं आता
    फिर भी मुझे लगता है कि इस niche में experiment करने की जगह है, और ऐसी कोशिशें अच्छी हैं। बस यह approach सही नहीं लगती

    इन दिनों मैं जिस दिशा के बारे में लगातार सोच रहा हूँ, वह है Zig के comptime, Pony जैसे reference capabilities, lifetimes को compile-time values की तरह treat करना, और lifetimes को allocators पर brand करने का combination
    उम्मीद यह है कि Zig की allocator strategy में reference safety जोड़कर, लगभग बिना annotations वाली lifetimes मिलें

  • नई languages अच्छी हैं, लेकिन यह पसंद नहीं कि सब कुछ LLVM का frontend बन जाए। backend कठिन है, यह समझता हूँ, लेकिन कभी-कभी दूसरे विकल्प भी हों तो अच्छा लगेगा

  • लगभग Swift जैसा सुनाई देता है