- pslang की शुरुआत बड़े गेम्स की modding क्षमता और C++ compiler द्वारा बनाए गए assembly में रुचि से हुई, और अब यह इतना काम करता है कि लगभग 1,000 LOC का Monte-Carlo path tracer लिखा जा सके
- modding language के लिए C interoperability, low-level array और pointer handling, आसान sandboxing, छोटा compiler size, और तेज compilation जरूरी हैं; जबकि Lua और C++ native mods में performance bridging, sandboxing, और distribution के मामले में अपनी-अपनी सीमाएँ हैं
- pslang एक imperative, eagerly evaluated, call-by-value low-level language है, जो static, strict, nominal type system, indentation-based scope, built-in arrays, function types, pointers, और guaranteed memory layout प्रदान करती है
- compiler को Bison-based parser, AST type checking, IR, interpreter, और JIT में बाँटा गया है; फिलहाल यह सिर्फ Aarch64 Mac को support करता है, और IR आने के बाद register allocator न होने की वजह से generated code की quality अभी कम है
- मौजूदा implementation लगभग 10,000 lines के C++ code की है, और आगे register allocator, IR optimization, IR interpreter, executable generation, debugging info, polymorphism, modules, और standard library जैसी सुविधाओं पर विचार हो रहा है
pslang बनाने की पृष्ठभूमि
- लगभग 17 साल programming करने के बाद, सिर्फ खिलौना नहीं बल्कि कुछ हद तक वास्तविक उपयोग को ध्यान में रखकर अपनी भाषा बनाने की इच्छा काफी बढ़ गई
- पहले FALSE जैसी esoteric language के interpreter और कई lambda calculus interpreter बनाए थे, लेकिन इससे “वास्तविक” language बनाने की इच्छा पूरी नहीं हुई
- विकसित किए जा रहे बड़े गेम की संरचना modding के लिए उपयुक्त थी, इसलिए modding के तरीके पर सोचते समय custom programming language एक सरल समाधान के रूप में सामने आई
- 2025 के दिसंबर में Matt Godbolt की Advent of Compiler Optimisations देखते हुए C++ compiler द्वारा generated assembly को ट्रैक करना शुरू किया, और फिर से assembly के साथ काम करने की इच्छा हुई
- फिलहाल यह भाषा production quality से काफी दूर है, लेकिन इसमें लगभग 1,000 LOC का काम करने वाला Monte-Carlo path tracer लिखा जा सकता है
modding requirements और मौजूदा विकल्पों की सीमाएँ
- गेम custom ECS engine पर सैकड़ों हज़ार entities simulate करता है, इसलिए इच्छा है कि modding language component pointers के bundles लेकर C के
for loop की तरह उन पर iterate कर सके
- mods को नियंत्रित करना कठिन होता है, इसलिए players की सुरक्षा के लिए sandboxing आसान होनी चाहिए, और आदर्श रूप से एक single switch से सभी IO और इसी तरह की सुविधाएँ disable की जा सकें
- modding इतनी आसान होनी चाहिए कि किसी खास folder में script रख देने भर से वह mod की तरह इस्तेमाल की जा सके
-
Lua और JIT scripting languages
- Lua एक standard choice है, लेकिन untrusted code के सामने standard library के IO functions हटाने वाला preprocessing code जोड़कर sandboxing करनी पड़ेगी, जो कोई स्थिर समाधान नहीं लगता
- Lua एक high-level dynamically typed language है, इसलिए यह C pointers को सीधे नहीं समझती; ECS entity iteration को जोड़ने के लिए या तो हर entity पर native ↔ Lua ↔ native transition होगा, या native entities को Lua array में बदलकर फिर वापस खोलना पड़ेगा
- standard Lua और LuaJIT कई versions पहले से अलग हो चुके हैं, जिससे modders और implementers दोनों के लिए भ्रम पैदा हो सकता है
-
C++ और native mods
- C++ में mod बनाने पर entity iteration की समस्या खत्म हो जाती है, लेकिन binary distribution के लिए हर platform का development environment और binary artifact storage चाहिए होता है
- source code के रूप में distribute करने के लिए गेम में C++ compiler शामिल करना होगा, और एक सामान्य LLVM installation भी मौजूदा गेम के आकार से 10~20 गुना अधिक disk space लेती है
- अगर native DLL
int open(); declare करके इस्तेमाल करे, तो filesystem या network access को रोकना लगभग असंभव है, इसलिए sandboxing व्यावहारिक रूप से संभव नहीं रहती
- Rust जैसी दूसरी native languages पर भी यही समस्या लागू होती है
- modding एक लक्ष्य जरूर है, लेकिन वास्तव में इस language का उपयोग game modding के लिए होगा या नहीं, यह अभी स्पष्ट नहीं है; और इसे किसी एक खास use case के लिए जरूरत से ज़्यादा specialized भी नहीं बनाना है
language design goals
- C interoperability को बिना रुकावट उपलब्ध कराकर native game code और modding code के बीच जुड़ाव को function call जितना सरल बनाना लक्ष्य है
- raw entity arrays को संभालना है, इसलिए low-level सुविधाएँ जरूरी हैं
- भाषा व्यावहारिक और उपयोग में आसान होनी चाहिए ताकि modders उचित सुविधा के साथ code लिख सकें
- sandboxing आसान होनी चाहिए, और compiler size भी छोटा होना चाहिए
- 50MB के गेम में 1GB का compiler नहीं रखना है, इसलिए compiler footprint कम रखना लक्ष्य है
- players को mod compilation के लिए लंबे समय तक इंतज़ार न करना पड़े, इसलिए compilation तेज होनी चाहिए; इसका कुछ हिस्सा व्यापक caching से कम किया जा सकता है
- असल cross-platform support चाहिए, लेकिन कुछ widely used desktop platforms, 64-bit, और IEEE754 support जैसी मान्यताएँ स्वीकार हैं
- अधिकांश dynamic languages की तुलना में उचित रूप से तेज होना पर्याप्त है
- लंबे समय तक C++ मुख्य भाषा रहने की वजह से इसने भाषा-सम्बंधी सोच को बहुत प्रभावित किया है, लेकिन जहाँ संभव हो C++ को ज्यों-का-त्यों फिर से नहीं बनाना है
pslang का मौजूदा language model
- working name pslang है, जो game engine psemek से लिया गया है; यह एक imperative, eagerly evaluated, call-by-value, low-level language है
- type system static, strict, nominal type system पर आधारित है
- मूल उदाहरण functions, struct, function type, और array return को साथ में दिखाता है
func min(x: i32, y: i32) -> i32:
return if x < y then x else y
struct vec3i:
x: i32
y: i32
z: i32
func apply(f: i32 -> i32, v: vec3i) -> vec3i:
return vec3i(f(v.x), f(v.y), f(v.z))
func as_array(v: vec3i) -> i32[3]:
return [v.x, v.y, v.z]
scope और basic types
- indentation-based scope का उपयोग किया गया है ताकि यह scripting language जैसा लगे और beginners के लिए अधिक सहज महसूस हो
- फिलहाल indentation में tab characters का उपयोग होता है, लेकिन आगे चलकर इसे spaces में बदला जा सकता है
- functions, loop bodies,
if bodies आदि नया scope बनाते हैं, और functions तथा structs किसी भी scope के भीतर define किए जा सकते हैं तथा सिर्फ उसी scope में दिखाई देते हैं
- local functions अपने define किए गए scope के variables तक पहुँच नहीं सकतीं, इसलिए वे closures नहीं हैं; scope सिर्फ name resolution को प्रभावित करता है
- top-level scope को भी दूसरे scopes की तरह माना जाता है, और इसमें वह entry point शामिल होता है जो file load या initialize होने पर execute होता है
- basic types कुल 13 हैं:
bool, signed integers के 4 प्रकार, unsigned integers के 4 प्रकार, floating-point के 3 प्रकार, और unit
i8 i16 i32 i64
u8 u16 u32 u64
f16 f32 f64
f8 को शामिल नहीं किया गया है, क्योंकि अधिकतर desktop CPU इसे support नहीं करते और 8-bit floating point के अर्थ पर भी सहमति नहीं है
f16 आम users के लिए कम उपयोगी हो सकता है, लेकिन graphics में HDR colors, vertex attributes आदि के लिए अक्सर इस्तेमाल होता है, और अधिकांश आधुनिक desktop CPU IEEE754 f16 implement करते हैं, इसलिए इसे built-in support दिया गया है
- सभी integer arithmetic overflow सहित two's complement तरीके से काम करते हैं, और कोई undefined behavior नहीं है
unit के पास सिर्फ एक value unit() होती है, और यही उन functions का औपचारिक return type है जिनका कोई return value नहीं होता
- जिन functions में return type छोड़ा गया है, वे अपने-आप
unit return करते हैं, और ऐसे functions के अंत में return न लिखने पर वह स्वतः जोड़ दिया जाता है
- अगर function
unit function नहीं है और फिर भी कोई value return नहीं करता, तो यह error है
लिटरल, array, function type, pointer
- संख्या
10 डिफ़ॉल्ट रूप से i32 होती है, और 10b, 10s, 10l जैसे suffix से उसका size तय किया जाता है
- unsigned literal के लिए
u suffix लगाया जाता है, जैसे 10ub, 10us, 10u, 10ul
- दशमलव बिंदु वाले floating-point literal डिफ़ॉल्ट रूप से
f32 होते हैं; 10.0h 16-bit और 10.0d 64-bit होता है
10. या .5 की तरह integer या fractional हिस्सा छोड़ नहीं सकते; इसे 10.0, 0.5 की तरह पूरा लिखना होता है
- सभी numeric literal का type बिना किसी ambiguity के तय होता है
- array एक built-in first-class type है, और C/C++ से अलग इसमें पूरे array को function में pass, return या आपस में assign किया जा सकता है
- array का size हमेशा compile time पर ज्ञात होता है, और यह एक ही type के कई fields वाले struct की तरह काम करता है
- array type को
i32[5] और array literal को [1, 2, 3, 4, 5] की तरह लिखा जाता है
- function type, C के function pointer के काफ़ी करीब है; इसे
(a, b, c) -> d फ़ॉर्म में लिखते हैं, और अगर argument एक हो तो a -> b की तरह parentheses छोड़े जा सकते हैं
- अंदरूनी रूप से function type एक सामान्य function pointer है जिसमें साथ में data pass नहीं होता; यह closure नहीं है
- pointer type को
i32* की तरह लिखा जाता है; डिफ़ॉल्ट रूप से pointer immutable होता है, और mutable pointer को i32 mut* के रूप में declare किया जाता है
- variable का address
&x, mutable pointer &mut x, dereference *p, और pointer arithmetic *(p + 10) की तरह इस्तेमाल होती है
struct, memory layout, empty type
- struct को
struct keyword और field सूची से declare किया जाता है
struct string_view:
size: u64
data: u8*
- struct को
string_view(10, data) जैसे built-in functional constructor से बनाया जाता है, और field को v.x की तरह dot से access किया जाता है
- struct pointer में भी field access के लिए वही dot syntax इस्तेमाल किया जा सकता है
- struct field के लिए अलग mutability specifier नहीं है; mutable object के field mutable होते हैं और immutable object के field immutable होते हैं
- access specifier नहीं हैं, और field हमेशा public होते हैं
- सभी object का memory layout guaranteed होता है; basic type की alignment उसके size के बराबर होती है, और
bool 1 byte का होता है
- pointer और function type हमेशा 64-bit के होते हैं और उनकी alignment भी एक जैसी होती है
- array की alignment उसके element जैसी होती है, और struct में alignment requirements पूरी करने के लिए padding होती है
- यह guarantee मुख्य रूप से C interoperability और GPU programming को आसान बनाने के लिए है
unit और बिना field वाले struct को एक ही valid value वाले empty type के रूप में माना जाता है, और उनका वास्तविक size 0 byte होता है
- empty type को function में pass करने, variable के रूप में declare करने या field में रखने से memory usage या struct size पर कोई असर नहीं पड़ता
- empty type का इस्तेमाल type-level compile-time tag जैसी चीज़ों के लिए किया जा सकता है
- empty type pointer के ज़रिए read/write अभी तय नहीं है, और फिलहाल ऐसे type पर pointer arithmetic illegal है
- C++ की तरह हर object का अपना अलग memory address होना ज़रूरी नहीं माना जाता
variable, function, control flow, external function
- immutable variable को
let x = 10 और mutable variable को mut x = 20 की तरह declare किया जाता है
- immutable variable के लिए mutable pointer नहीं बनाया जा सकता
let x: i32 = 10 की तरह type explicitly लिखा जा सकता है, लेकिन इसकी डिज़ाइन ऐसी है कि सभी expression type बिना ambiguity के infer किए जा सकें, इसलिए यह ज़रूरी नहीं है
- हर variable को initialize करना अनिवार्य है
- function को
func foo(x: A, y: B) -> C: के बाद body लिखकर define किया जाता है; return type छोड़ने पर unit माना जाता है
- सभी function execution platform के native C ABI का पालन करते हैं; यह C interoperability, callback, और ECS system जैसी स्थितियों में function pointer के रूप में pass करने के लिए किया गया फ़ैसला है
- एक ही scope के भीतर function और struct declaration का क्रम स्वतंत्र है, इसलिए बाद में declare किए गए function या struct को पहले इस्तेमाल किया जा सकता है
- चूंकि सभी function argument और return type पूरी तरह explicit होते हैं, इसलिए declaration order को स्वतंत्र करने से type inference जटिल नहीं होता
if/else if/else statement और while loop हैं, लेकिन for loop अभी नहीं है
- expression रूप वाला
if, if A then B else C की तरह इस्तेमाल होता है
- external function को
foreign func sin(x: f64) -> f64 की तरह declare किया जाता है, और उसका implementation कहीं और link होना चाहिए
- वर्तमान interpreter ऐसी function को interpreter executable के भीतर ही
dlsym से ढूँढता है
- external function, C library और third-party library interoperability का मुख्य mechanism हैं; raytracer उदाहरण में इसी सुविधा से square root निकालना, file लिखना, समय मापना, और thread बनाना किया जाता है
type casting और operator
- implicit type casting बिल्कुल नहीं है; manual casting के लिए
(x as f32) की तरह as operator इस्तेमाल होता है
- सभी numeric type एक-दूसरे में cast किए जा सकते हैं, और सभी pointer type भी एक-दूसरे में cast किए जा सकते हैं, लेकिन immutable pointer को mutable pointer में बदलना अपवाद है
- pointer type को
u64 में, और u64 को pointer type में cast किया जा सकता है
bool को किसी भी type में cast नहीं किया जा सकता
T mut* से T* में एक implicit cast जोड़ने पर विचार किया जा रहा है
- arithmetic, logical, comparison आदि standard operator अधिकांशतः उपलब्ध हैं
&, |, &&, || boolean और integer दोनों पर काम करते हैं; & और | दोनों operand को हमेशा evaluate करते हैं, जबकि && और || short-circuit evaluation करते हैं
- arithmetic और comparison केवल एक ही numeric type के जोड़ों पर काम करते हैं; numeric type promotion नहीं है
- अभी language feature बहुत ज़्यादा नहीं लगते, लेकिन इनके साथ भी व्यावहारिक प्रोग्राम काफ़ी आराम से लिखे जा सकते हैं
compiler architecture
- project कई library में बँटा हुआ है
types: type system की परिभाषा
ast: abstract syntax tree की परिभाषा और utility
parser: parser
ir: intermediate representation
interpreter: interpreter
jit: JIT compiler
- योजना यह है कि interpreter और compiler, इन library का इस्तेमाल करने वाले सरल CLI app हों; फिलहाल केवल JIT mode वाला interpreter है
- language को embed करने के लिए
parser और jit library का उपयोग किया जा सकता है
parser और indentation handling
- parser generator के रूप में Bison का उपयोग किया गया है
- token को lexer grammar में, और language grammar को parser grammar में परिभाषित किया गया है
- file, statements की सूची होती है, और statement function declaration, control flow operator, variable declaration, expression आदि हो सकते हैं; expression literal, variable, operator, function call आदि हो सकते हैं
- grammar में shift/reduce conflict को कुछ बार ठीक करना पड़ा, और Bison के
-Wcounterexamples flag से उन conflict को पैदा करने वाली सटीक स्थिति देखी गई
lalr1.cc Bison skeleton का उपयोग करके C++ parser class बनाई जाती है
- डिफ़ॉल्ट Bison parser state को global variable के रूप में रखने वाला C parser बनाता है, लेकिन interpreter या game mode जैसे मामलों में जहाँ कई file को parallel parse करना हो, यह उपयुक्त नहीं है
- Bison execution को CMake scripts के build step में शामिल किया गया है
- parser का output, parsed file के AST को दर्शाने वाला C++ object होता है
- indentation की वजह से grammar वास्तव में context-free नहीं है; कौन-सा statement
while body में आता है, यह उससे पहले आए indentation token की संख्या पर निर्भर करता है
- समाधान के रूप में हर line को स्वतंत्र statement और indentation level के रूप में parse किया जाता है, फिर एक सरल linear pass में indentation level देखकर scope तय किया जाता है
- यह तरीका थोड़ा hacky है, लेकिन काम करता है और बहुत तेज़ है, इसलिए इसे स्वीकार किया गया
- इसी pass में यह भी जाँचा जाता है कि
break और continue केवल loop के अंदर हों, return केवल function के अंदर हो, और field definition केवल struct के अंदर हो
टाइप जाँच और इंटरप्रेटर
- parsing के बाद पहला pass सभी identifiers को resolve करता है और identifier nodes को सीधे संबंधित variable, function, और struct definition nodes से जोड़ देता है
- अगला मुख्य pass सभी types की जाँच और inference करता है
- type inference ज़्यादातर सरल है और खास AST node types के अनुसार condition checks से बना है
- उदाहरण के लिए,
if या while के भीतर expression का type bool होना चाहिए, और addition के दो operands या तो एक ही numeric type के होने चाहिए, या एक integer और दूसरा pointer होना चाहिए
- शुरुआती इंटरप्रेटर AST nodes को सीधे visit करके C++ semantics चलाने वाला tree-walking interpreter है
- मुख्य functions
exec() और eval() हैं; exec() एक single statement चलाता है और eval() एक single expression का value निकालकर लौटाता है
- चूँकि C++ statically typed है,
eval() भाषा के सभी possible value types के लिए एक variant लौटाता है
- struct को हर field के लिए एक name-value pair array के रूप में दर्शाया जाता है, और variable values को store करने के लिए भी वही
variant इस्तेमाल होता है
- इंटरप्रेटर का उद्देश्य भाषा के code को cross-platform चलाना और implementation तथा program debugging में मदद करना है, न कि उसे तेज़ बनाना
- मौजूदा इंटरप्रेटर बहुत टूटी हुई स्थिति में है, इसलिए इसे IR-आधारित रूप में पूरी तरह फिर से लिखने की योजना है
- मौजूदा इंटरप्रेटर
foreign functions को execute नहीं कर सकता
foreign functions को C calling convention से call करना पड़ता है, और arguments की संख्या व types पहले से पता नहीं होते, इसलिए vararg तकनीक या libffi की ज़रूरत पड़ सकती है
- इंटरप्रेटर अपनी internal state, यानी variables के names, types, और values को stdout पर dump कर सकता है, और सही compiler बनने से पहले parser और इंटरप्रेटर debugging का यही मुख्य तरीका था
पहला Aarch64 JIT compiler
- जनवरी 2026 की शुरुआत में छुट्टियों के दौरान केवल M1 Mac उपलब्ध था, इसलिए पहला compiler target architecture Aarch64 Mac बना
- अभी भी यही एकमात्र supported architecture है
- compiler JIT तरीके से काम करता है, और उसका परिणाम executable bit के साथ mapped memory blob और हर function के start point pointers होता है
- high-level structure लगभग पारंपरिक stack-based compiler जैसा है, लेकिन expression results को AAPCS64, यानी Aarch64 Mac के standard C calling convention में उसी तरह रखा जाता है जैसे वही return type वाला function value रखता है
- integers और pointers
x0 general-purpose register में लौटते हैं, floating-point values v0 floating-point register में, और structs आकार के अनुसार registers या stack पर लौटते हैं
- इस तरीके से memory accesses कम होते हैं, generated code तेज़ होता है, और function calls भी सरल हो जाते हैं
- stack का इस्तेमाल मुख्य रूप से binary operations जैसे intermediate results के लिए होता है
(eval A) # the value of A is in x0
push x0 # the value of A is on stack top
(eval B) # the value of B is in x0
pop x1 # the value of A is in x1
add x0, x0, x1 # the value of A+B is in x0
- control-flow structures conditional jumps में बदल जाते हैं, लेकिन single-pass compilation में
if या while body अभी compile नहीं हुई होती, इसलिए jump target पता नहीं होता
- इसे हल करने के लिए पहले offset 0 वाला jump instruction emit किया जाता है, और target offset पता चलने के बाद असली jump offset inject किया जाता है
- function calls पर भी यही तरीका लागू होता है
- target CPU instructions generate करने के लिए third-party library का उपयोग नहीं किया गया; compiler को छोटा रखने के लिए इसे सीधे implement किया गया
- implementation का तरीका instruction manual खंगालकर ज़रूरी bits भरने वाला था
Aarch64 में मुश्किल हिस्से
- Aarch64 की सभी instructions 32-bit की हैं, इसलिए संभालना आसान लगता है, लेकिन 32-bit constant को register में डालने के लिए register select bits, instruction bits, और constant bits सभी चाहिए होते हैं, जो एक single 32-bit instruction में समा नहीं सकते
- 64-bit constants इससे भी बड़ी समस्या हैं
- constants को 16-bit chunks के रूप में offsets 0, 16, 32, 48 bit positions पर load करने वाली instructions से जोड़ा जाता है, या constant memory में रखकर वहाँ से load करना पड़ता है
- floating-point constants के लिए constant memory से load करने का तरीका इस्तेमाल किया जाता है
- x86 के विपरीत यहाँ push/pop instructions नहीं हैं; register और memory address के बीच read/write करने और address register को adjust करने वाली instructions को मिलाकर काम करना पड़ता है
- क्योंकि हर instruction ठीक 32-bit की है, इसलिए लगातार ध्यान रखना पड़ता है कि offset signed है या unsigned, क्या वह किसी खास constant से पहले से multiply होता है, और क्या वह address register को modify करता है
- SP register के आधार पर stack को पढ़ते-लिखते समय stack pointer हमेशा 16-byte aligned होना चाहिए
- possible offsets 12-bit तक सीमित हैं, इसलिए जब stack frame लगभग 16KB से बड़ा हो तो special code चाहिए, लेकिन वह अभी implement नहीं हुआ है
- calling convention में structs को अधिकतम 2 general-purpose registers, floating-point registers, या memory pointer के ज़रिए pass/return करने जैसे special cases हैं, इसलिए compiler code को इन्हें संभालना पड़ता है
IR की शुरुआत और दूसरा compiler
- बुनियादी इंटरप्रेटर और compiler बनाने के बाद, code reuse, दूसरी architectures के लिए compiler लिखना आसान बनाने, और optimizations के लिए intermediate representation (IR) जोड़ा गया
- IR की शुरुआत SSA जैसी थी, लेकिन क्योंकि एक ही node को value फिर से assign की जा सकती है और phi nodes भी नहीं हैं, इसलिए यह वास्तव में SSA नहीं है
- IR, nodes की sequence है, जहाँ हर node literal, input nodes वाले operation, conditional/unconditional jump, function call आदि को दर्शाता है
- value को दर्शाने वाले nodes उस value का type भी store करते हैं
- reassignment की अनुमति होने के कारण, किसी existing node value को फिर से assign करने के लिए
assign IR instruction है
- conditional jumps को
jump_if_zero और jump_if_nonzero में बाँटा गया है; यह आम तौर पर अलग CPU instructions से मेल खाते हैं और value को negate करके उलटी instruction इस्तेमाल करने से तेज़ है
- function pointers supported हैं, इसलिए known IR node को call करने वाली instruction और unknown pointer value को call करने वाली instruction अलग-अलग हैं
- optimization के दौरान मनचाही जगह nodes हटाना या डालना आसान हो, इसके लिए nodes को
std::list में store किया जाता है और references list iterators के रूप में रखे जाते हैं
- struct value literals बनाए नहीं जा सकते, इसलिए struct value को दर्शाने के लिए
alloc node रखा गया है, जो आम तौर पर stack पर uninitialized struct space allocate करने के रूप में compile होता है
- structs को individual fields में assign करके बनाया जाता है
- nested struct field
a.x.y को सरल तरीके से दर्शाने पर a.x को एक नए node के रूप में पढ़ा जाता है और फिर उस node का y पढ़ा जाता है, जिससे काफी बर्बादी होती है
a.x.y = b भी अगर t = a.x, t.y = b, a.x = t की तरह व्यक्त किया जाए तो अक्षम होता है, इसलिए IR में nested fields को विशेष रूप से handle किया जाता है
copy node struct से किसी भी nested field को निकाल सकता है, और assign node struct के किसी भी nested field में assign कर सकता है
- nested fields को index array के रूप में दर्शाया जाता है, जैसे “field 0 लो, उसके भीतर field 2 लो, और उसके भीतर field 5 लो”
- बाद में Aarch64 compiler को AST → IR compiler और IR → Aarch64 compiler में बाँटकर फिर से लिखा गया
- AST → IR अपेक्षाकृत सरल है, लेकिन IR → Aarch64 compiler की स्थिति अभी पिछले stack-based compiler से भी कहीं खराब है
- function शुरू होते ही उस function के सभी IR nodes के लिए जितनी stack space चाहिए उतनी allocate कर दी जाती है, इसलिए कम समय तक जीवित रहने वाले अधिकतर intermediate values भी stack frame घेर लेते हैं
- raytracer का एक function पहले बताई गई 12-bit सीमा के भीतर stack frame फिट कराने के लिए दो हिस्सों में बाँटना पड़ा
- यह compiler इस मान्यता पर बना है कि बाद में register allocator उपयोग होगा, इसलिए उम्मीद है कि आगे generated code कई गुना बेहतर हो जाएगा
Compiler और interpreter की योजना
- मौजूदा implementation लगभग 10,000 lines के C++ code से बनी है, और आधुनिक मानकों के हिसाब से compiler छोटा होने के बावजूद वास्तव में काम करता है, इससे संतुष्टि है
-
Register allocator
- मौजूदा IR → Aarch64 compiler को register allocator की सख्त ज़रूरत है
- compile speed और code quality के बीच संतुलन के लिए एक standard linear scan allocator इस्तेमाल करने की योजना है
-
IR optimization
- IR के आधार पर constant propagation, arithmetic simplification, dead code elimination, inlining, loop unrolling जोड़ना चाहता है
- लक्ष्य GCC या LLVM को हराना नहीं है, लेकिन 3D vector addition जैसी simple function को जितना संभव हो उतने कम CPU instructions में compile करना चाहता है
-
IR interpreter
- interpreter को IR direct evaluation तरीके से दोबारा लिखने की योजना है, और ऐसा करने से interpreter काफी सरल हो जाएगा, ऐसा मानता है
-
Executable file generation
- मौजूदा compiler अभी सिर्फ तुरंत चलाने के लिए JIT memory blob बनाता है
- platform-specific format में executable binary भी बनाना चाहता है, इसलिए ELF, Mach-O, PE जैसे binary format specs को गहराई से समझना होगा
- जितना संभव हो उतनी छोटी executable file बनाना भी लक्ष्यों में से एक है
-
Debugging
- JIT द्वारा बनाई गई assembly को lldb में काफी ट्रेस किया है, और भाषा खुद को ठीक से debug कर पाना चाहता है
- इसके लिए DWARF debug information format का support चाहिए होगा, इसकी पूरी संभावना है, लेकिन फिलहाल इसके बारे में लगभग कुछ नहीं जानता
भाषा में जोड़ना चाही जाने वाली सुविधाएँ
-
Struct constructor
- फिलहाल struct में या तो
vec3i(1, 2, 3) की तरह सभी fields सेट करनी होती हैं, या vec3i() की तरह उन्हें 0 से initialize करना होता है
- इस बात पर विचार है कि struct के नाम वाला function declare किया जाए तो वह arbitrary constructor की तरह काम करे
func vec3i(x: i32, y: i32) -> vec3i:
return vec3i(x, y, 0)
- हालांकि ऐसे function को अलग unique नाम देना बेहतर हो सकता है, इसलिए अभी तय नहीं है
-
Global variables
- फिलहाल global variables का support नहीं है
global keyword के साथ global variable बनाने की योजना है, और access फिर भी scope rules से बंधा रहेगा, इसलिए C के static variable की तरह function-local global variable बनाना संभव होगा
- top-level variables, जब तक
global न लिखा जाए, असली global नहीं बल्कि file entry point function के local variables हैं
- यह संरचना users के लिए confusing हो सकती है, इसलिए दूसरे विकल्पों पर भी विचार चल रहा है
- Mac एक साथ writable और executable memory mapping की अनुमति नहीं देता, इसलिए global variables को code से अलग allocate करके अलग flags के साथ map करना पड़ सकता है
- global access compile time पर ज्ञात offsets की जगह runtime में resolved address के जरिए करना पड़ सकता है
- लेकिन
mprotect() से mapping के कुछ हिस्सों के flags बदले जा सकते हैं, ऐसा लगता है, इसलिए पहले उसे आज़माने की योजना है
-
Method call syntax
- readability के लिए जहाँ संभव हो,
x.f(y) का मतलब f(&x, y) या f(&mut x, y) बनाना चाहता है
-
Polymorphism
- इसे सबसे महत्वपूर्ण संभावित feature मानता है
- प्रमुख विकल्प हैं: C++-style function overloading और unrestricted function templates·struct templates, या Haskell/Rust-style explicit trait और trait-constrained generic functions·structs
- C++-style ज्यादा powerful है, simple cases में पढ़ने में आसान है, और compiler implementation भी आसान है, लेकिन error messages बहुत कठिन हो सकती हैं
- explicit trait कुछ मामलों में पढ़ने में आसान होते हैं और error message की समस्या हल करते हैं, लेकिन trait और trait bound जैसी नई system चाहिए, इसलिए compiler implementation ज्यादा कठिन हो जाती है
- अभी फैसला नहीं हुआ है, लेकिन C++ को फिर से न बनाने की कोशिश के बावजूद पहला विकल्प काफी मजबूत तरीके से आकर्षित कर रहा है
struct vec2<t: type>:
x: t
y: t
func min<t: type>(x: t, y: t) -> t:
return if x < y then x else y
- जहाँ संभव हो, function argument inference भी चाहता है
-
Operator overloading
- इसके लिए किसी न किसी रूप में polymorphism चाहिए
a + b को add(a, b) जैसे overloaded function या Add::add जैसे trait method को call करने वाला बनाया जा सकता है
-
for loop
- चूँकि इसे
while से imitate किया जा सकता है, इसलिए for को C++ के range-based loop या Python loop की तरह collection-based loop के रूप में इस्तेमाल करने की योजना है
- इसके लिए range/iterator interface चाहिए, और फिर से polymorphism चाहिए
-
Automatic resource management
- मानता है कि practical और उपयोग में आसान भाषा में memory, file, socket, mutex जैसे resources को release करने में मदद करने का तरीका होना चाहिए
- उम्मीदवार हैं C++-style RAII और move, Zig-style
defer, और linear types
- RAII implicit होता है, इसलिए इसकी कमी यह है कि यह hidden commands और control flow जोड़ देता है
defer explicit है, लेकिन इसे हर बार खुद डालना पड़ता है, छूटने से नहीं रोकता, और file array जैसी nested collections को release करते समय असुविधाजनक है
defer free(array)
defer for file in array:
close(file)
- linear types आशाजनक लगते हैं, क्योंकि वे
free या close को manually call करने की स्पष्टता बनाए रखते हुए object को resource-release function द्वारा consume करवाना अनिवार्य बना सकते हैं
- लेकिन dynamic file array जैसी nested collections के साथ इन्हें मिलाना कठिन है, इसलिए अभी फैसला नहीं हुआ है
-
Polymorphic literals
- empty array
[] में size 0 तो पता है, लेकिन element type infer नहीं किया जा सकता
null किसी भी pointer type का हो सकता है, और जो inf literal जोड़ना चाहता है वह किसी भी floating-point type का हो सकता है
- समाधान के रूप में Haskell-style polymorphic literals, C++ के
nullptr_t जैसे special built-in·library types और implicit conversions, या AST में special literals और compiler की ad-hoc handling — इन तीन विकल्पों पर विचार है
- फिलहाल
null के लिए आख़िरी तरीके की ओर झुकाव है, जिसमें इसे सिर्फ उन जगहों पर अनुमति दी जाए जहाँ expected pointer type पता हो, जैसे explicit typed variable initialization या function argument passing
- यह तरीका सबसे सरल है, लेकिन extensible नहीं है, इसलिए custom types को
null से नहीं बनाया जा सकता
-
Compile-time evaluation
const keyword से compile-time variables declare करना चाहता है, ताकि उन्हें array size जैसे compile-time expressions में इस्तेमाल किया जा सके
const values को reassign नहीं किया जा सकता और उनका address नहीं लिया जा सकता
- उचित functions को compile-time expressions में call किया जा सकता है, अगर उनमें global variable access या side effects न हों
- function body सामान्य function की तरह काम करेगी, लेकिन compilation के दौरान execute होगी और उसका result compile-time expression बन जाएगा
- math functions या memory allocation जैसी
foreign functions को, जिन्हें compile time पर call करना सुरक्षित हो, mark करने की व्यवस्था चाहिए
-
Type computation
- metaprogramming के लिए types पर computation का support देना चाहता है
- static typed language में runtime type encoding बनाना नहीं चाहता, और runtime types की उपयोगिता भी सीमित है, इसलिए इसे सिर्फ compile-time के लिए plan कर रहा है
- मानता है कि C++ concepts जैसी सुविधा भी अलग syntax के बिना compile-time calls से implement की जा सकती है
func comparable(t: type) -> bool:
// Implemented somehow...
func min<t: comparable type>(x: t, y: t) -> t:
return if x < y then x else y
-
Coroutines
- Python या JS-style
async/await जोड़ना योजना से ज़्यादा एक इच्छा के करीब है
लाइब्रेरी और मॉड्यूल योजना
-
मॉड्यूल
- सारा कोड एक ही फ़ाइल में लिखना व्यावहारिक नहीं है, इसलिए मॉड्यूल की ज़रूरत है
import lib.sublib जैसे सरल statement की योजना है, जिसे कोड में कहीं भी रखा जा सकेगा और यह scope rules का भी पालन करेगा
- scope केवल visibility को प्रभावित करेगा, जबकि असली loading compile time पर होगी, और imported मॉड्यूल का entry point मौजूदा मॉड्यूल से पहले execute होगा
- लाइब्रेरी का नाम compiler या interpreter को दिए गए root path के आधार पर filesystem path से सीधे मेल खाएगा
- अगर यह एक single source file है तो केवल वही फ़ाइल import होगी, और अगर directory है तो उस directory की सभी फ़ाइलें किसी क्रम में import होंगी
- उसी directory की फ़ाइलों को संदर्भित करने के लिए syntax की ज़रूरत है, और
import .another जैसे रूप पर विचार किया जा रहा है
- imported functions और global variables को बिना prefix के इस्तेमाल किया जा सकेगा, और ambiguity होने पर
io.print(x) की तरह library name prefix लगाया जा सकेगा
- मॉड्यूल entry point import order और recursive import की topological sorting के आधार पर deterministic क्रम में execute होंगे, जिससे C या C++ की initialization order problem हल हो सकती है
- कई मॉड्यूल वाले program का memory layout अभी तय नहीं किया गया है
- हर मॉड्यूल के लिए अलग memory patch रखा जा सकता है और function call व global variable access को runtime पर resolve किया जा सकता है, या एक बड़ा memory mapping बनाकर relative offset का उपयोग किया जा सकता है
- एक बड़ा mapping runtime में तेज़ हो सकता है, लेकिन इससे कई मॉड्यूल की parallel compilation कठिन हो जाती है
-
Prelude
- मॉड्यूल आने पर बुनियादी utility को ऐसे prelude मॉड्यूल में रखा जा सकता है जो हर program में implicitly शामिल हो
- built-in array के लिए
length() function, iterator interface, string view type, और Python के range(n) जैसी numeric range इसके उम्मीदवार हैं
-
स्ट्रिंग लिटरल
- string literal अभी मौजूद नहीं हैं, और यह भी तय नहीं है कि उनका semantics क्या होना चाहिए
- योजना यह है कि prelude में immutable
string_view type हो, string content को executable memory में कहीं रखा जाए, और literal को उसी memory की ओर इशारा करने वाले string_view में बदल दिया जाए
-
स्टैंडर्ड लाइब्रेरी
- मॉड्यूल आने पर standard library की भी ज़रूरत होगी
- इसमें शामिल करने की इच्छा वाले दायरे में vector और matrix सहित math library,
libc से linked alloc/free शैली का memory management, dynamic array, dynamic string और formatting, hash table, console और file IO, filesystem helper, time·clock helper, और networking शामिल हैं
वर्तमान प्राथमिकताएँ
- योजनाबद्ध features को कब implement किया जाएगा, या इस भाषा को वास्तव में game modding या किसी और उपयोग के लिए इस्तेमाल किया जाएगा या नहीं, यह तय नहीं है
- लेखक का मानना है कि एक साथ कई ambitious projects पर गंभीरता से काम करना ठीक नहीं है, और अभी भी प्राथमिकता game development ही है
- चूँकि game बनने से पहले उसे mod नहीं किया जा सकता, इसलिए language पर काम अभी मन होने पर ही किया जा रहा है
1 टिप्पणियां
Lobste.rs की राय
यहाँ के कमेंट इस कम्युनिटी से मेरी उम्मीद से कहीं ज़्यादा कठोर लगते हैं
मुमकिन है कि Lua जैसी किसी दूसरी language से काम चल जाता। यह भी हो सकता है कि लेखक बहुत बड़े yak shaving में फँस गया हो
फिर भी यह साफ़ है कि उसकी स्किल बहुत अच्छी है और वह इसे खूब एंजॉय भी कर रहा है, और लेख में दिलचस्प तकनीकी बातें भी हैं
अगर यह किसी साथी nerd का गेम इंजन के लिए एक और scripting language डिज़ाइन करने पर लिखा गया लेख है, तो मैं उसे खुशी से पढ़ूँगा। अगर इससे AI-जनित उस तरह की बकवास से एक भी लेख कम पढ़ना पड़े, जिसमें vibecoding से बना SaaS कचरा दुनिया बचाता है और लेखक को अमीर बना देता है, तो मैं ऐसे दिन में हज़ार लेख भी पढ़ सकता हूँ
“Lua या कोई दूसरी JIT-compiled scripting language standard choice है, लेकिन sandboxing सच में मुश्किल है” — यह दावा समझना सच में कठिन है
Lua में sandboxing आसान है, और यही उसकी सबसे बड़ी खूबियों में से एक है; यह सिर्फ mod या plugin तक सीमित फायदा नहीं देता। मैंने जो भी language देखी है, उनमें कोई भी इसके करीब नहीं आती
Lua version issues वाली बात कुछ हद तक ठीक है, लेकिन मैंने लोगों को इस पर बहुत ज़्यादा नाराज़ होते कम ही देखा है। जब तक कोई “modern” Lua किसी खास काम के लिए इस्तेमाल करके फिर किसी और वजह से 5.1/5.2 पर वापस जाने को मजबूर न हो, ज़्यादातर लोग एक ही version पर टिके रहते हैं
यह ज़्यादा ऐसा लगता है जैसे “मैं अपनी language बनाना चाहता हूँ” को justify करने के लिए रिसर्च की गई हो। अपने आप में यह ठीक है, लेकिन existing options के बारे में पूरी तरह गलत दावे करने के बजाय ईमानदार होना बेहतर है
अगर virtual machine design या उससे भी low-level हिस्सों में दिलचस्पी है, तो लेख में बताया गया तरीका भी बिल्कुल ठीक है। लेकिन language design सीखने का यह सबसे अच्छा तरीका नहीं है
सबसे आसान example है bytecode escape। इसके बारे में पता हो तो इसे disable किया जा सकता है, लेकिन इसका बार-बार सामने आना एक बड़े मुद्दे की तरफ इशारा करता है। sandboxing rules को जोड़ने के लिए आपको Lua spec के अलग-अलग हिस्सों की आपसी interaction समझनी पड़ती है; यह ऐसा सिस्टम नहीं है जहाँ आप साफ़-साफ़ परिभाषित primitives से programs को सुरक्षित तरीके से compose कर सकें और यह स्पष्ट हो कि कौन-सी अतिरिक्त interactions संभव होंगी
थोड़ा ज़्यादा खिंचा हुआ example है एक ही Lua VM के अलग-अलग environments के बीच होने वाला prototype pollution। Redis में string के metatable को pollute किया जा सकता था, और फिर किसी दूसरे database user की authority के साथ code चलाया जा सकता था जो Lua functionality इस्तेमाल करता हो। Lua में JavaScript जैसी languages की तुलना में prototype pollution की surface बहुत ही कम है, लेकिन मज़ेदार बात यह है कि global prototypes लगभग सिर्फ 2 ही हैं और उनमें से एक के साथ भी वही काम हो जाता है
फिर भी Luau के पास इस समस्या का काफी सक्षम समाधान है, इसलिए समझ नहीं आता कि अगर लेखक नया sandbox बनाए तो वह क्यों मान रहा है कि वही सारी समस्याएँ अपने आप टल जाएँगी
“मेरा game simulation-heavy है। मैं custom ECS engine में सैकड़ों हज़ार entities simulate करता हूँ। आदर्श रूप से modding language कई component pointers लेकर C के for loop की तरह iterate कर सके” — इस आदर्श से बेहतर आदर्श हो सकता है
खास तौर पर यह देखने लायक है कि Unity, Unreal, Blender, Godot जैसे rendering engines इस समस्या को कैसे handle करते हैं। external iteration megapixels-per-second जैसी चीज़ों के लिए काफी तेज़ नहीं होती, और शायद प्रति सेकंड सैकड़ों हज़ार entities के लिए भी उपयुक्त न हो। यहाँ parallelism के बारे में सोचना चाहिए
बड़े engines आम तौर पर GPU-friendly होते हैं और ज़्यादातर embarrassingly parallel branchless algorithms के dataflow description का इस्तेमाल करते हैं। लेखक visual editors को नापसंद कर सकता है, और यह सोच आम भी है, लेकिन इसका मतलब यह नहीं कि for loop ही जवाब है
अगर लेखक यह कहता कि ECS मूल रूप से relational paradigm है, और तुलना के लिए सही ऐतिहासिक बोझ वाली language SQL है, तो शायद मैं इसे थोड़ा और सहानुभूति से देखता