आधुनिक संकलक में जेनरिक को कैसे लागू किया जाता है?


15

यहाँ मेरा क्या मतलब है कि हम किसी टेम्पलेट T add(T a, T b) ...से उत्पन्न कोड में कैसे जाते हैं? मैंने इसे प्राप्त करने के लिए कुछ तरीकों के बारे में सोचा है, हम एक एएसटी में जेनेरिक फ़ंक्शन को स्टोर करते हैं Function_Nodeऔर फिर हर बार जब हम इसका उपयोग करते हैं तो हम मूल फ़ंक्शन में स्टोर करते हैं, सभी प्रकार Tके साथ प्रतिस्थापित किए गए सभी प्रकारों के साथ खुद की एक प्रतिलिपि नोड करते हैं। उपयोग किया जा रहा है। उदाहरण के लिए add<int>(5, 6)जेनेरिक फ़ंक्शन की एक प्रति संग्रहीत करेगा addऔर T कॉपी में सभी प्रकारों को प्रतिस्थापित कर सकता है int

तो यह कुछ इस तरह दिखेगा:

struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};

तब आप इन के लिए कोड जनरेट कर सकते हैं और जब आप Function_Nodeकॉपियों की सूची पर जाते हैं copies.size() > 0, तो आप visitFunctionसभी कॉपियों को इनवोक कर देते हैं।

visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}

क्या यह अच्छा काम करेगा? आधुनिक कंपाइलर इस समस्या को कैसे देखते हैं? मुझे लगता है कि ऐसा करने का एक और तरीका यह होगा कि आप प्रतियों को एएसटी में इंजेक्ट कर सकते हैं ताकि यह सभी अर्थ चरणों के माध्यम से चले। मैंने यह भी सोचा कि शायद आप उन्हें एक तत्काल रूप में उत्पन्न कर सकते हैं जैसे उदाहरण के लिए रस्ट के एमआईआर या स्विफ्ट एसआईएल।

मेरा कोड जावा में लिखा गया है, यहां के उदाहरण C ++ हैं क्योंकि यह उदाहरणों के लिए थोड़ा कम क्रिया है - लेकिन सिद्धांत मूल रूप से एक ही बात है। हालाँकि इसमें कुछ त्रुटियां हो सकती हैं क्योंकि यह सिर्फ प्रश्न बॉक्स में हाथ से लिखा गया है।

ध्यान दें कि मेरा मतलब है कि आधुनिक कंपाइलर इस समस्या से निपटने का सबसे अच्छा तरीका है। और जब मैं कहता हूं कि जेनेरिक का मतलब मैं जावा जेनेरिक की तरह नहीं है, जहां वे टाइप इरेज़र का उपयोग करते हैं।


C ++ में (अन्य प्रोग्रामिंग भाषाओं में जेनेरिक हैं, लेकिन वे प्रत्येक इसे अलग तरीके से लागू करते हैं), यह मूल रूप से एक विशाल, संकलन-समय मैक्रो सिस्टम है। वास्तविक कोड प्रतिस्थापित प्रकार का उपयोग करके उत्पन्न होता है।
रॉबर्ट हार्वे

क्यों नहीं मिटाते टाइप? ध्यान रखें कि यह सिर्फ जावा नहीं है जो यह करता है, और यह एक बुरी तकनीक नहीं है (आपकी आवश्यकताओं के आधार पर)।
एंड्रेस एफ।

@AndresF। मुझे लगता है कि जिस तरह से मेरी भाषा काम करती है, वह अच्छी तरह से काम नहीं करेगी।
जॉन फ्लो

2
मुझे लगता है कि आपको स्पष्ट करना चाहिए कि आप किस तरह की जेनरिक की बात कर रहे हैं। उदाहरण के लिए, C ++ टेम्प्लेट, C # जेनरिक और जावा जेनरिक सभी एक दूसरे से बहुत अलग हैं। आप कहते हैं कि आपको जावा जेनरिक से मतलब नहीं है, लेकिन आप यह नहीं कहते कि आप क्या करते हैं।
svick

2
यह वास्तव में अत्यधिक व्यापक होने से बचने के लिए एक भाषा की प्रणाली पर ध्यान केंद्रित करने की आवश्यकता है
डेनिथ

जवाबों:


14

आधुनिक संकलक में जेनरिक को कैसे लागू किया जाता है?

मैं आपको एक आधुनिक संकलक के स्रोत कोड को पढ़ने के लिए आमंत्रित करता हूं यदि आप जानना चाहते हैं कि आधुनिक संकलक कैसे काम करता है। मैं रोज़लिन प्रोजेक्ट के साथ शुरू करूँगा, जो C # और Visual Basic कंपाइलरों को लागू करता है।

विशेष रूप से मैं सी # कंपाइलर में कोड पर आपका ध्यान आकर्षित करता हूं जो प्रतीकों को लागू करता है:

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

और तुम भी परिवर्तनीय नियमों के लिए कोड को देखना चाहते हो सकता है। वहाँ बहुत कुछ है जो सामान्य प्रकार के बीजीय हेरफेर से संबंधित है।

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

मैंने बाद में पढ़ने में आसान बनाने के लिए कड़ी मेहनत की।

मैंने इसे प्राप्त करने के कुछ तरीकों के बारे में सोचा है, हम एक फ़ंक्शन में एएसटी को जेनेरिक फ़ंक्शन को स्टोर करते हैं जैसे कि फंक्शन_Node और फिर हर बार जब हम इसका उपयोग करते हैं तो हम मूल फ़ंक्शन नोड में स्टोर करते हैं जो कि सभी प्रकारों के साथ खुद की एक प्रतिलिपि के साथ टाइप किया जाता है। इसका उपयोग किया जा रहा है।

आप टेम्प्लेट का वर्णन कर रहे हैं , जेनरिक का नहीं । C # और Visual Basic के पास अपने प्रकार के सिस्टम में वास्तविक जेनरिक हैं।

संक्षेप में, वे इस तरह से काम करते हैं।

  • हम औपचारिक रूप से संकलित समय पर एक प्रकार का गठन करने के लिए नियम स्थापित करके शुरू करते हैं। उदाहरण के लिए: intएक प्रकार है, एक प्रकार का पैरामीटर Tएक प्रकार है, किसी भी प्रकार के लिए X, सरणी प्रकार X[]भी एक प्रकार है, और इसी तरह।

  • जेनरिक के नियमों में प्रतिस्थापन शामिल है। उदाहरण के लिए, class C with one type parameterएक प्रकार नहीं है। यह प्रकार बनाने के लिए एक पैटर्न है। class C with one type parameter called T, under substitution with int for T है एक प्रकार है।

  • प्रकारों के बीच संबंधों का वर्णन करने वाले नियम - असाइनमेंट पर संगतता, अभिव्यक्ति के प्रकार का निर्धारण कैसे करें, और इसी तरह - संकलक में डिज़ाइन और कार्यान्वित किए जाते हैं।

  • एक बायोटेक भाषा जो अपने मेटाडेटा सिस्टम में सामान्य प्रकारों का समर्थन करती है, डिज़ाइन और कार्यान्वित की जाती है।

  • रनटाइम में JIT कंपाइलर bytecode को मशीन कोड में बदल देता है; यह एक सामान्य विशेषज्ञता को देखते हुए उपयुक्त मशीन कोड के निर्माण के लिए जिम्मेदार है।

उदाहरण के लिए, C # में जब आप कहते हैं

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

तब संकलक पुष्टि करता है कि C<int>, तर्क intएक वैध प्रतिस्थापन है T, और इसके अनुसार मेटाडेटा और बायटेकोड उत्पन्न करता है। रनटाइम के दौरान, घबराना पता चलता है कि C<int>पहली बार बनाया जा रहा है और गतिशील रूप से उपयुक्त मशीन कोड उत्पन्न करता है।


9

जेनेरिक (या बल्कि: पैरामीट्रिक बहुरूपता) के अधिकांश कार्यान्वयन प्रकार के क्षरण का उपयोग करते हैं। यह सामान्य कोड को संकलित करने की समस्या को बहुत सरल करता है, लेकिन केवल बॉक्स प्रकारों के लिए काम करता है: चूंकि प्रत्येक तर्क प्रभावी रूप से एक अपारदर्शी सूचक है, हमें तर्कों पर संचालन करने के लिए एक VTable या समान प्रेषण तंत्र की आवश्यकता होती है। जावा में:

<T extends Addable> T add(T a, T b) { … }

संकलित किया जा सकता है, टाइप-चेक किया गया है, और उसी तरह से बुलाया जा सकता है

Addable add(Addable a, Addable b) { … }

सिवाय इसके कि जेनेरिक कॉल साइट पर अधिक जानकारी के साथ टाइप चेकर प्रदान करते हैं। यह अतिरिक्त जानकारी प्रकार चर के साथ संभाला जा सकता है , खासकर जब सामान्य प्रकार का अनुमान लगाया जाता है। जाँच के दौरान, प्रत्येक सामान्य प्रकार को एक चर के साथ बदला जा सकता है, आइए इसे कॉल करें $T1:

$T1 add($T1 a, $T1 b)

प्रकार चर को तब अधिक तथ्यों के साथ अद्यतन किया जाता है जब तक कि वे ज्ञात नहीं हो जाते हैं, जब तक कि इसे एक ठोस प्रकार से प्रतिस्थापित नहीं किया जा सकता है। प्रकार की जाँच एल्गोरिथ्म को इस तरह से लिखा जाना चाहिए जो इन प्रकार के चर को समायोजित करता है, भले ही वे अभी तक पूर्ण प्रकार से हल न हों। जावा में यह आमतौर पर आसानी से किया जा सकता है क्योंकि प्रकार के तर्कों को अक्सर फ़ंक्शन कॉल के प्रकार से पहले जाना जाता है। एक उल्लेखनीय अपवाद फ़ंक्शन तर्क के रूप में एक लंबोदर अभिव्यक्ति है, जिसे इस प्रकार के चर के उपयोग की आवश्यकता होती है।

बहुत बाद में, एक ऑप्टिमाइज़र निश्चित संख्या के तर्कों के लिए विशेष कोड उत्पन्न कर सकता है, फिर यह प्रभावी रूप से एक प्रकार की इनलाइनिंग होगी।

जेनेरिक-टाइप किए गए तर्कों के लिए एक VTable से बचा जा सकता है यदि जेनेरिक फ़ंक्शन प्रकार पर कोई कार्रवाई नहीं करता है, लेकिन केवल उन्हें किसी अन्य फ़ंक्शन में भेजता है। उदाहरण के लिए हास्केल फ़ंक्शन call :: (a -> b) -> a -> b; call f x = f xको xतर्क को बॉक्स में नहीं रखना होगा । हालांकि, इसके लिए एक कॉलिंग कन्वेंशन की आवश्यकता होती है, जो उनके आकार को जाने बिना मानों से गुजर सकता है, जो अनिवार्य रूप से वैसे भी संकेत करने के लिए प्रतिबंधित करता है।


इस संदर्भ में C ++ अधिकांश भाषाओं से बहुत अलग है। एक टेम्प्लेटेड क्लास या फंक्शन (मैं यहाँ केवल टेम्पल किए गए फंक्शन की चर्चा करूँगा) अपने आप में कॉल करने योग्य नहीं है। इसके बजाय, टेम्पलेट्स को एक संकलन-समय मेटा-फ़ंक्शन के रूप में समझा जाना चाहिए जो एक वास्तविक फ़ंक्शन देता है। एक पल के लिए टेम्पलेट तर्क की अनदेखी, सामान्य दृष्टिकोण फिर इन चरणों के लिए उबालता है:

  1. दिए गए टेम्प्लेट तर्कों पर टेम्पलेट लागू करें। उदाहरण के लिए बुला template<class T> T add(T a, T b) { … }के रूप में add<int>(1, 2)हमें वास्तविक समारोह देना होगा int __add__T_int(int a, int b)(या जो भी नाम-mangling दृष्टिकोण प्रयोग किया जाता है)।

  2. यदि उस फ़ंक्शन के लिए कोड वर्तमान संकलन इकाई में पहले से ही उत्पन्न हो गया है, तो जारी रखें। अन्यथा, कोड उत्पन्न करें जैसे कि कोई फ़ंक्शन int __add__T_int(int a, int b) { … }स्रोत कोड में लिखा गया था। इसमें टेम्प्लेट तर्क की सभी घटनाओं को इसके मूल्यों के साथ बदलना शामिल है। यह संभवतः एएसटी → एएसटी परिवर्तन है। फिर, उत्पन्न एएसटी पर टाइप चेकिंग करें।

  3. कॉल को ऐसे संकलित करें जैसे कि स्रोत कोड था __add__T_int(1, 2)

ध्यान दें कि C ++ टेम्प्लेट में ओवरलोड रिज़ॉल्यूशन तंत्र के साथ एक जटिल इंटरैक्शन होता है, जिसे मैं यहां वर्णन नहीं करना चाहता। यह भी ध्यान दें कि यह कोड-जेनरेशन एक टेम्प्लेटेड पद्धति को भी असंभव बना देता है जो आभासी भी है - एक प्रकार-इरेज़र आधारित दृष्टिकोण इस पर्याप्त प्रतिबंध से ग्रस्त नहीं है।


आपके कंपाइलर और / या भाषा के लिए इसका क्या अर्थ है? आप जिस तरह की जेनरिक पेशकश करना चाहते हैं, उसके बारे में आपको ध्यान से सोचना होगा। यदि आप बॉक्सिंग प्रकारों का समर्थन करते हैं, तो टाइप इंट्रेंस की अनुपस्थिति में टाइप इरेक्शन सबसे सरल संभव तरीका है। टेम्प्लेट विशेषज्ञता काफी सरल लगती है, लेकिन आम तौर पर आउटपुट में नाम जुड़ना और (कई संकलन इकाइयों के लिए) में पर्याप्त दोहराव शामिल होता है, क्योंकि टेम्प्लेट को कॉल साइट पर त्वरित किया जाता है, न कि परिभाषा साइट पर।

आपके द्वारा दिखाया गया दृष्टिकोण अनिवार्य रूप से C ++ है - जैसे टेम्पलेट दृष्टिकोण। हालाँकि, आप विशेष / तात्कालिक टेम्पलेट्स को मुख्य टेम्पलेट के "संस्करण" के रूप में संग्रहीत करते हैं। यह भ्रामक है: वे समान रूप से वैचारिक नहीं हैं, और किसी फ़ंक्शन के विभिन्न इंस्टेंसेस में बेतहाशा भिन्न प्रकार हो सकते हैं। यह लंबे समय में चीजों को जटिल करेगा यदि आप फ़ंक्शन को ओवरलोडिंग की अनुमति देते हैं। इसके बजाय, आपको एक अधिभार सेट की धारणा की आवश्यकता होगी जिसमें एक नाम साझा करने वाले सभी संभावित फ़ंक्शन और टेम्पलेट शामिल हों। ओवरलोडिंग को हल करने के अलावा, आप विभिन्न तात्कालिक टेम्पलेट्स पर विचार कर सकते हैं जो एक दूसरे से पूरी तरह से अलग हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.