Jam प्रोग्रामिंग भाषा
(rapha.land)- 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 से संभालता है exportJam functions को C ABI के रूप में expose करता है और Jam struct को C-compatible layout रखने के लिए design किया गया है, ताकि अलग unsafe shim याreprannotation का बोझ कम हो- 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 करता है
- उदाहरण में
Filetypefn 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 होता है
- explicit cleanup,
- mangled name
__drop_Fileकई types के drop functions को LLVM level पर collide होने से बचाता है self: mut Filepointer 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 करता है
- C++ के rule of 0/3/5, virtual destructor, constructor exception, destructor exception,
Initialization और Maybe(T)
- Jam में
undefinedvalue नहीं है, और 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 दिखाने के लिए
0xaafill डाला जाता है - Release mode में arbitrary bytes हो जाते हैं
- Debug mode में misuse दिखाने के लिए
- 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 करता हैunsafeprefix 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,matcharm,while,forbody के अंदर की bindings उसी block के अंत में drop होती हैं- nested block के अंदर
returnactualretसे पहले active scopes को innermost-first order में drop करता है breakऔरcontinueloop body में open scopes को drop करने के बाद loop exit या next iteration पर जाते हैं
- nested break उदाहरण में
outeriteration 0 के अंत में drop होता है, और iteration 1 के break path में पहलेinner, फिरouterdrop होता है
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 नहीं होता
mutexclusive read-write borrow है- caller की binding call के बाद भी initialized state में रहती है
- केवल
movevalue 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] = xv.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 हैं इसलिए OKmoveX(p, p.x)मेंpऔरp.xoverlap करते हैं, इसलिए 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 घटाने के लिए मौजूद हैं
- raw pointer dereference
- Jam में first-class reference, lifetime, niche-packed layout नहीं हैं, इसलिए Jam value को पूरे रास्ते value-shaped माना जाता है
- Jam struct पहले से ही C-compatible layout रखने के लिए design किया गया है
exportJam 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 Counterparameter caller-owned storage के लिएCounter *में lower होता है
- Jam side function body ordinary Jam है, इसलिए drop, init analysis, call-site exclusivity rule लागू रहते हैं
- C की ओर जाने के लिए
externसे C signature declare किया जाता हैexternfunction 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 का
matchPattern Blockform में है और=>का इस्तेमाल नहीं करता- scrutinee
match (opcode)की तरह parentheses इस्तेमाल करता है _catch-all arm है- arms top-to-bottom sequential first-match हैं और implicit fallthrough नहीं है
- scrutinee
- 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 हो जाती है
matchexpression के रूप में भी काम करता है- हर arm block trailing expression की value produce करता है
- सभी arms को same type produce करना चाहिए
- match exhaustive होना चाहिए
- अंदरूनी तौर पर सभी match Luc Maranget 2008 आधारित decision tree pipeline से compile होते हैं
- integer literal cascade को LLVM
simplifycfgprofitable होने परswitchऔर jump table में fold करता है
- integer literal cascade को LLVM
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 टिप्पणियां
Lobste.rs की रायें
इस तरह की 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 से कर सकते हैं
अच्छी technical writing में, जहां उचित हो, दोनों हो सकते हैं और होने चाहिए, लेकिन जो सच में बताना जरूरी है उसे छोड़ना नहीं चाहिए। एक बड़ी assurance-function organization चलाकर पता चला कि technical writing कितनी कठिन है, और LLM accessibility आसान होने के साथ यह समस्या कितनी और खराब हो सकती है, इस पर सावधान रहना चाहिए
Zig से मुख्य फर्क यह है कि इसमें
dropहै, और आसानी से गलत इस्तेमाल होने वाला खास componentundefinedनहीं है?undefinedनहीं है और सभी values initialize होनी चाहिए, लेकिनMaybe(T).empty()ऐसी value लौटाता है जिसकी content “अभी meaningful नहीं” है, और अगर तुरंत बादunsafeAssumeInit()call करें तो शायद garbage value लौटाएगा। तो यह Rust की तरह safety नहीं है, जहां compilerunsafeको explicitunsafe { .. }की जरूरत वाले taint की तरह treat करता हैsafety और
dropfeature दिखाने वाला example यह code है:अगर मैंने गलत नहीं देखा तो क्या यह unsafe नहीं है? manual file descriptor allocation को छोड़ भी दें, तो
close(7)call करने के बाद7return करता है। lifetime tracking नहीं है, इसलिए user के पास यह express करने का तरीका नहीं है कि file descriptor की lifetimeuseFile()return से पहले खत्म हो चुकी हैABI example में जब
export fn counterAdd(c: mut Counter, n: i64) i64 { .. }int64_t counterAdd(Counter *c, int64_t n);बनता है, तो यह कैसे express करेंगे किcNULLहो सकता है या नहीं? 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 भी:
यहां कहीं न कहीं
unsafeहोना चाहिए, नहीं?snprintfraw pointer लेता है, इसलिए पहले कही गई guideline के हिसाब से किunsafeoperations नाम से खोजे जा सकने चाहिए, शायदunsafeSnprintfऔर symbol redefinition जैसा कुछ होना चाहिए“एक ईमानदार संकेत:
externline में आप C से बात कर रहे हैं, और C के rules चलते हैं” — हम्म.as_raw_fd()से अलग नहीं है, और वहां भी वही safety issue हैयह 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 चाहिए ही नहीं होता
यह 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 समस्या यह है:जवाब तीन हैं:
इन तीनों में से किसी को भी चुनने के अपने अच्छे कारण हो सकते हैं, लेकिन Jam Rust की तरह नंबर 1 होना चाहता है, जबकि असल में value semantics की वजह से नंबर 2 जैसा लगता है। अगर इसका मतलब है कि सब कुछ copy होता है, तो यह safe और efficient data structures लिखने से रोक सकता है
खासकर borrow checker छोड़ देने पर, कई संकेत introduce किए बिना stack-allocated types को support करना कहीं ज्यादा मुश्किल हो जाता है। उदाहरण के लिए borrow करते समय copy करना—Inko और Swift दोनों ऐसा करते हैं
language reference देखने पर references तो नहीं हैं, लेकिन
mutऔरconstpointers हैं, और उनकी 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 जैसा सुनाई देता है