DSL العالمي. هل هذا ممكن؟


لغة الموضوع. غير مثقلة مع بنيات لغة للأغراض العامة. في الوقت نفسه ، يسمح لك بتنفيذ منطق معقد للغاية مع بضعة أسطر. كل هذا هو DSL.

ومع ذلك ، فإن إنشاء DSL يتطلب أن يكون المطور مؤهلاً. يتحول الاستخدام المنتظم لهذا النهج إلى روتين لتطوير لغة أخرى. قد يكون الحل هو إنشاء أداة عالمية - محرك سيكون قابلاً للتطبيق على مهام مختلفة تمامًا وسهلة التعديل. في هذه المقالة ، سنطور في C # أبسط من وجهة نظر التنفيذ ، ولكن في نفس الوقت محرك لغوي قوي للغاية ، حيث يمكنك حل مجموعة واسعة من المشاكل إلى حد ما.

مقدمة


هناك طريقتان لتطوير مشروع تطبيقي: لجعل الأمر بهذه البساطة بحيث يكون من الواضح أنه لا يوجد به عيوب أو جعله معقدًا لدرجة أنه لا يوجد به عيوب واضحة. سى ار رور (كار هور)
في هذه المقالة ، أود أن أشارك إحدى تقنيات التطوير التي تساعدني وفريقي ، من ناحية ، على التعامل مع تعقيد المشاريع. ومن ناحية أخرى - يسمح لك بتطوير تطبيقات النموذج الأولي بسرعة. للوهلة الأولى ، يبدو تطوير لغة برمجة معقدة للغاية. لذلك ، إذا كنا نتحدث عن أداة عالمية. إذا كان الهدف هو تغطية مجال موضوعي ضيق ، فإن تطوير لغة معينة غالباً ما يبرر نفسه.

ذات مرة واجهتني مهمة تطوير تطبيق لغة صناعية (IEC 61131-3) للتضمين في برنامج العميل. خلال هذا العمل ، أصبحت مهتمة بموضوع بنية المترجم الشفوي ومنذ ذلك الحين ، كتبت مترجمين فوريين للغة الباطنية وليس لغات بعينها كهواية. في المستقبل ، جاء فهم لكيفية استخدام المترجمين الفوريين المكتتبين ذاتيًا لتبسيط الحياة اليومية.


الهدف الرئيسي من لغات البرمجة عاقل هو تبسيط عملية البرمجة وقراءة البرنامج. الكتابة في asm أسهل من رموز الآلة ، الكتابة في C أسهل من في asm ، في C # أبسط وهكذا.

يتم تحقيق ذلك بشكل رئيسي بسبب الطريقة الأكثر شيوعًا في الاختزال - تقسيم مهمة معقدة إلى مكونات بسيطة يمكن التعرف عليها - توحيد تفاعلها وبناء جملة معين.

تتكون لغة البرمجة من مجموعة من العوامل ، والتي هي في جوهرها أساس اللغة ، ولبنات البناء الأولية ، وبناء الجملة التي تحدد طريقة كتابة مجموعات من العوامل ، وكذلك المكتبة القياسية. يتم تجميع تسلسل الإجراءات الأولية وفقًا للقواعد النحوية في وظائف ، ويتم تجميع الوظائف في فئات (إذا كان هناك OOP) ، ويتم دمج الفئات في مكتبات ، وتلك بدورها في حزم. هذا هو ما تبدو عليه اللغة السائدة التقليدية. من حيث المبدأ ، هذه التقنيات كافية لحل معظم المهام اليومية. ومع ذلك ، ليس هذا هو الحد الأقصى ، لأنه يمكنك اتخاذ خطوة إلى الأمام - إلى مستوى أعلى من التجريد ، وسوف تضطر إلى تجاوز حدود اللغة المستخدمة إذا كانت لا تدعم metaprogramming في شكل وحدات ماكرو.


في الوقت الحاضر ، فإن معظم المشاريع تتلخص في مزيج من المكونات الجاهزة وجزء ضئيل من المستوى المكتوب ذاتيًا. عادة ما يتم الجمع بين المكونات عن طريق لغة برمجة عالمية - C # و Java و Python وغيرها. على الرغم من أن هذه اللغات عالية المستوى ، فهي أيضًا عالمية ، وبالتالي تحتوي بالضرورة على إنشاءات نحوية للعمليات منخفضة المستوى ، وإنشاء وظائف ، وفئات ، ووصف الأنواع المعممة ، والبرمجة غير المتزامنة ، وغير ذلك الكثير. وبسبب هذا ، فإن المهمة "افعلها مرة واحدة ، افعل اثنين ، أفعل ثلاثة" تكبر مع كتلة من التركيبات النحوية ويمكن أن تصل إلى مئات أسطر الكود وأكثر.

يمكنك تبسيط إعادة استخدام المكونات إذا كررت تقنية الاختزال ، ولكن بالفعل على هذه المكونات ذاتها. يتم تحقيق ذلك من خلال تطوير لغة متخصصة لها بناء جملة مبسّط وتخدم فقط لوصف تفاعل هذه المكونات. يسمى هذا النهج YaOP (البرمجة الموجهة للغات) ، وتسمى اللغات DSL (لغة خاصة بالمجال - لغة خاصة بالمجال).

نظرًا لعدم وجود إنشاءات زائدة عن الحاجة ، يمكن فقط لعدد قليل من الأسطر الموجودة على خط المشترك الرقمي (DSL) تنفيذ وظائف معقدة للغاية ، الأمر الذي يؤدي إلى عواقب إيجابية: زيادة سرعة التطوير وتناقص عدد الأخطاء وتبسيط اختبار النظام.

إذا تم تطبيقه بنجاح ، يمكن لهذا النهج أن يزيد بشكل كبير من مرونة المنتج الجاري تطويره نظرًا لإمكانية كتابة نصوص مضغوطة تحدد وتوسّع سلوك النظام. يمكن أن يكون هناك العديد من التطبيقات لهذا النهج ، كما يتضح من انتشار هذا النهج ، لأن DSL موجود في كل مكان. HTML الشائعة هي لغة وصف للمستند ، SQL هي لغة استعلام منظمة ، JSON هي لغة وصف بيانات منظمة ، XAML ، بوستسكريبت ، Emacs Lisp ، nnCron وغيرها الكثير.


مع كل المزايا ، لدى DSL عيبًا كبيرًا - متطلبات عالية لمطور النظام.

ليس كل مطور لديه المعرفة والخبرة في تطوير حتى لغة بدائية. يمكن لعدد أقل من المتخصصين تطوير لغة مرنة ومنتجة بما فيه الكفاية. هناك مشاكل أخرى. على سبيل المثال ، عند نقطة معينة في تطوير الوظيفة المحددة أصلاً ، قد لا يكون ذلك كافياً وسيكون من الضروري إنشاء وظائف أو OOP. وحيثما توجد وظائف ، قد تكون هناك حاجة إلى تحسين ذيل العودية للاستغناء عن الحلقات ، وما إلى ذلك. في الوقت نفسه ، يجب مراعاة التوافق مع الإصدارات السابقة حتى تستمر النصوص المكتوبة مسبقًا في العمل مع الإصدار الجديد.

مشكلة أخرى هي أن اللغة المصممة لحل مشكلة واحدة غير مناسبة تمامًا للآخرين. لذلك ، يجب عليك تطوير DSL جديد من البداية ، بحيث يصبح تطوير لغات جديدة أمرًا معتادًا. يؤدي هذا مرة أخرى إلى تعقيد عملية الصيانة وتقليل إعادة استخدام التعليمات البرمجية التي يصعب مشاركتها بين تطبيقات DSL المختلفة والمشاريع التي تستخدمها.


المخرج هو إنشاء DSL لإنشاء DSL. أنا هنا لا أقصد RBNF ، بل اللغة التي يمكن تغييرها عن طريق وسائل مدمجة إلى لغة موضوع الموضوع. العائق الرئيسي في إنشاء لغة مرنة وقابلة للتحويل هو وجود بناء جملة ونظام الكتابة محددة بدقة. على مدار فترة التطوير الكاملة لصناعة الكمبيوتر ، تم اقتراح العديد من اللغات المرنة بدون بناء الجملة ، لكنها ظلت باقية حتى يومنا هذا وما زالت لغتا Forth و Lisp تتطوران بنشاط. السمة الرئيسية لهذه اللغات هي أنه ، بسبب بنيتها ومبدعها المتماثل ، يمكن ، بسبب الأدوات المدمجة ، تغيير سلوك المترجم الشفوي ، وإذا لزم الأمر ، تحليل الإنشاءات النحوية التي لم يتم وضعها في الأصل.

هناك حلول لـ Forth لتوسيع بناء الجملة الخاص به إلى C أو إلى المخطط. غالبًا ما يتم انتقاد "الحصن" بسبب التسلسل غير العادي للبوستفيج للوسائط والعمليات ، والذي تمليه استخدام المكدس لتمرير الوسائط. ومع ذلك ، فإن "Fort" لديه حق الوصول إلى مترجم نص ، وهذا يسمح لك بإخفاء السجل العكسي من المستخدم إذا لزم الأمر. وأخيرا ، هذه مسألة عادة ، ويتم تطويرها بسرعة كبيرة.

تعتمد عائلة لغات Lisp على وحدات ماكرو تسمح لك بإدخال DSL إذا لزم الأمر. ويساهم الوصول إلى المترجم والقارئ في تنفيذ المترجمين الشفهيين بميزات التفسير المحددة. على سبيل المثال ، يتم وضع تطبيق Scheme lisp Racket كبيئة لتطوير اللغات ولديه لغات غير مخصصة لإنشاء خوادم الويب وإنشاء واجهات واجهة المستخدم الرسومية ولغة الاستدلال وغيرها.

هذه المرونة تجعل هذه اللغات مرشحة جيدة لدور محرك DSL العالمي.

يتطور كل من "Fort" و Lisp بشكل أساسي كلغات للأغراض العامة ، وإن كانت لغوية خاصة ، كنتيجة لذلك - فهي تعتمد على وظائف يمكن أن تكون زائدة عن الحاجة إلى لغة DSL. ولكن في الوقت نفسه ، فهي بسيطة بما يكفي للتنفيذ ، مما يعني أنه يمكنك تطوير إصدار محدود مع إمكانية توسيعه. سيتيح لك ذلك إعادة استخدام جوهر هذه اللغة مع تعديلات صغيرة (من الناحية المثالية - بدون) لمهمة محددة.

أريد أيضًا أن أشير إلى أن هذه اللغات رائعة ليس فقط لكتابة النصوص ، ولكن أيضًا للتفاعل التفاعلي مع النظام عبر REPL. التي ، من ناحية ، يمكن أن تكون مناسبة للتصحيح ، ومن ناحية أخرى ، تعمل كواجهة يمكن الوصول إليها من قبل المستخدم مع النظام. من المعتقد أن واجهة النص مع النظام في بعض الحالات يمكن أن تكون أكثر فعالية من الواجهة الرسومية ، حيث أنه من الأسهل بكثير تنفيذها وأكثر مرونة ، يتيح للمستخدم تعميم العمليات النموذجية في وظائف ، وهلم جرا. مثال صارخ على واجهة النص قد يكون Bash. وإذا كانت اللغة متجانسة ، فيمكن أن يكون بناءها سهلاً نسبيًا وتحليله وبأدنى جهد ممكن ، يتم تنفيذ لغة رسومية أعلى المترجم الشفهي - قد يكون ذلك مفيدًا عندما يكون المستخدم المستهدف بعيدًا عن البرمجة.

في الوقت الحاضر ، تستخدم لغات وصف بيانات XML و JSON على نطاق واسع كـ DSL للتهيئة. بالطبع ، هذه ممارسة رائعة ، لكن في بعض الحالات لا تكون البيانات وحدها كافية وتحتاج ، على سبيل المثال ، لوصف العمليات عليها.


في هذا المنشور ، أقترح إنشاء مترجم بسيط للغة Fort وإظهار كيفية تكييفها لحل مشاكل محددة.

تم اختيار لغة Fort باعتبارها الأسهل في التنفيذ والاستخدام ، في حين أنها قوية بما يكفي لاستخدامها كرقم DSL لعدد من المهام. في الواقع ، فإن جوهر اللغة هو مترجم العنوان ، والذي حتى في المجمّع لا يأخذ سوى بضعة أسطر ، ويقع الجزء الأكبر من التنفيذ على العناصر البدائية ، والتي هي أكثر ، كلما كان التنفيذ أكثر عالمية وسريعة ومرنة. جزء مهم آخر من اللغة هو مترجم النص ، والذي يسمح لك بالتفاعل مع مترجم العنوان.


عنوان المترجم


العنصر الأساسي للغة Fort هو كلمة مفصولة عن الكلمات والذرات الأخرى (الأرقام) بمسافات ، ونهايات الأسطر وعلامات التبويب.

كلمة لها نفس المعنى والخصائص كدالة من لغات أخرى ، على سبيل المثال C. الكلمات السلكية في التنفيذ ، والتي يتم تنفيذها بنفس الوسائل مثل المترجم الشفهي ، تشبه العوامل من اللغات الأخرى. في الواقع ، فإن البرنامج بأي لغة برمجة ليس أكثر من مزيج من مشغلي اللغة والبيانات. لذلك ، يمكن اعتبار إنشاء لغة برمجة بمثابة تعريف للمشغلين وكيفية دمجها. علاوة على ذلك ، فإن لغات مثل C تحدد طريقة مختلفة لكتابة عوامل التشغيل ، والتي تحدد بناء جملة اللغة. في معظم اللغات ، عادة ما يكون تعديل العبارات غير ممكن - على سبيل المثال ، لا يمكنك تغيير بناء الجملة أو سلوك عبارة if.

في لغة Fort ، جميع المشغلين ومجموعاتهم (كلمات المستخدم) لديهم نفس طريقة الكتابة. تنقسم كلمات الحصن إلى بدائية وعرفية. يمكنك تحديد كلمة من شأنها أن تفرط في البدائية ، وبالتالي تغيير سلوك البدائيين. على الرغم من أنه في الواقع سيتم تنفيذ الكلمة المعاد تعريفها من خلال الأوليات المحددة في البداية. في تنفيذنا ، ستكون الوظيفة في C # هي البدائية. تتكون الكلمة المعرفة من قبل المستخدم من قائمة عناوين الكلمات التي سيتم تنفيذها. نظرًا لوجود نوعين من الكلمات ، يجب على المترجم التمييز بينهما. يتم تنفيذ الفصل بين البدائية وكلمات المستخدم من خلال نفس البدائية ، كل كلمة مستخدم تبدأ بعملية DoList وتنتهي بعملية خروج.

من الممكن وصف كيفية حدوث مثل هذا الفصل لفترة طويلة ، ولكن من الأسهل فهم ذلك من خلال دراسة ترتيب تنفيذ برنامج المترجم الفوري. للقيام بذلك ، نقوم بتنفيذ مترجم شفوي بسيط ، ونحدد برنامجًا بسيطًا ونرى كيف سيتم تنفيذه خطوة بخطوة.


آلة الحصن الخاصة بنا تتكون من ذاكرة خطية ، كومة بيانات ، كومة عودة ، مؤشر تعليمات ، مؤشر كلمة. سيكون لدينا أيضا مكان منفصل لتخزين البدائية.

public object[] Mem; //   public Stack<int> RS; //   public Stack<object> DS; //   public int IP; //   public int WP; //   public delegate void CoreCall(); public List<CoreCall> Core; //   

جوهر التفسير هو الانتقال إلى العنوان في الذاكرة وتنفيذ التعليمات المشار إليها هناك. سيتم تعريف مترجم العنوان بالكامل - قلب اللغة - في حالتنا في وظيفة واحدة التالي ().

 public void Next() { while (true) { if (IP == 0) return; WP = (int)Mem[IP++]; Core[(int)Mem[WP]](); } } 

تبدأ كل كلمة مستخدم بأمر DoList ، تتمثل مهمته في حفظ عنوان التفسير الحالي على المكدس وتعيين عنوان التفسير للكلمة التالية.

 public void DoList() { RS.Push(IP); IP = WP + 1; } 

للخروج من الكلمة ، استخدم الأمر "إنهاء" ، الذي يستعيد العنوان من مكدس الإرجاع.

 public void Exit() { IP = RS.Pop(); } 

للحصول على عرض مرئي لمبدأ المترجم الشفهي ، نقدم لك أمرًا ، سيحاكي العمل المفيد. دعنا نسميها مرحبا ().

 public void Hello() { Console.WriteLine("Hello"); } 

تحتاج أولاً إلى تهيئة الجهاز وتحديد البدائل لكي يعمل المترجم بشكل صحيح. يجب عليك أيضا تحديد عناوين البدائية في ذاكرة البرنامج.

 Mem = new Object[1024]; RS = new Stack<int>(); DS = new Stack<object>(); Core = new List<CoreCall>(); Core.Add(Next); Core.Add(DoList); Core.Add(Exit); Core.Add(Hello); const int opNext = 0; const int opDoList = 1; const int opExit = 2; const int opHello = 3; // core pointers Mem[opNext] = opNext; Mem[opDoList] = opDoList; Mem[opExit] = opExit; Mem[opHello] = opHello; 

الآن يمكننا إعداد برنامج بسيط ، في حالتنا ، سيبدأ رمز المستخدم في العنوان 4 ويتكون من برنامجين فرعيين. يبدأ الروتين الأول من العنوان 7 ويستدعي الثاني ، والذي يبدأ من العنوان 4 ويعرض كلمة مرحبًا.

 // program Mem[4] = opDoList; // 3)    IP = 9   ,   IP = WP + 1 = 5 Mem[5] = opHello; // 4)     Mem[6] = opExit; // 5)   ,  IP = 9    Mem[7] = opDoList; // 1)     Mem[8] = 4; // 2)     4,  WP = 4 Mem[9] = opExit; // 6)   ,  IP = 0    

لتنفيذ البرنامج ، يجب عليك أولاً حفظ القيمة 0 في مكدس الإرجاع ، حيث يقوم مترجم العنوان بمقاطعة دورة التفسير ، وتعيين نقطة الإدخال ، ثم بدء تشغيل المترجم.

 var entryPoint = 7; //    IP = 0; //  IP = 0,        WP = entryPoint; //  WP = 7      DoList(); //     ,  IP = 0    Next(); //    

كما هو موضح ، سيتم تخزين بدائل المترجم الشفهي في ذاكرة منفصلة. بالطبع ، كان يمكن تنفيذه بشكل مختلف: على سبيل المثال ، في ذاكرة البرنامج ، تم تخزين مندوب إلى وظيفة المشغل. فمن ناحية ، لم يكن مثل هذا المترجم الشفوي أسهل ، ولكن من ناحية أخرى ، كان سيكون أبطأ بشكل واضح ، لأن كل خطوة في التفسير ستتطلب فحص النوع ، والصب ، والتنفيذ ، والحصول على مزيد من العمليات.

تبدأ كل كلمة مستخدم لمترجمنا بدائية DoList ، وتتمثل مهمتها في حفظ العنوان الحالي للتفسير والانتقال إلى العنوان التالي. يتم تنفيذ الخروج من الروتين الفرعي بواسطة عملية الخروج ، التي تستعيد العنوان من رصة العودة لمزيد من التفسير. في الواقع ، لقد وصفنا مترجم العنوان بالكامل. لتنفيذ البرامج التعسفية ، يكفي لتوسيعها مع البدائية. لكن عليك أولاً التعامل مع مترجم نص ، والذي يوفر واجهة لمترجم العنوان.


مترجم النص


لا تحتوي لغة Fort على بناء جملة ؛ البرامج المكتوبة فيها عبارة عن كلمات مفصولة بمسافات أو علامات تبويب أو نهايات أسطر. لذلك ، تتمثل مهمة مترجم النص في تقسيم دفق الإدخال إلى كلمات (رموز) ، أو العثور على نقطة إدخال لها أو تنفيذها أو الكتابة إلى الذاكرة. ولكن ليست كل الرموز قابلة للتنفيذ. إذا لم يجد المترجم الكلمة ، فسيحاول تفسيرها على أنها ثابت رقمي. بالإضافة إلى ذلك ، يحتوي مترجم النص على وضعين: وضع الترجمة الشفوية ووضع البرمجة. في وضع البرمجة ، لا يتم تنفيذ عناوين الكلمات ، ولكن يتم كتابتها في الذاكرة ، وبالتالي يتم تحديد كلمات جديدة.

عادةً ما تجمع التطبيقات الكنسي لـ "Fort" بين القاموس (إدخال القاموس) وذاكرة البرنامج ، مما يحدد ملف شفرة واحد في شكل قائمة متصلة ببساطة. في تطبيقنا ، سيتم وضع رمز قابل للتنفيذ فقط في الذاكرة ، وسيتم تخزين نقاط إدخال الكلمات في بنية منفصلة - قاموس.

 public Dictionary<string, List<WordHeader>> Entries; 

في هذا القاموس ، يتم تعيين الكلمة إلى عدة عناوين ، بحيث يمكنك تحديد عدد تعسفي من البرامج الفرعية بنفس الاسم ، ثم حذف هذا التعريف والبدء في استخدام القديم. كما يتيح لك العنوان القديم المحفوظ العثور على اسم كلمة في القاموس ، حتى لو تم إعادة تعريفها ، وهو أمر مفيد بشكل خاص لإنشاء تتبع مكدس أو لتصحيح الأخطاء لدراسة الذاكرة. WordHeader هي فئة تخزن عنوان إدخال روتين فرعي وعلم تفسير فوري.

 public class WordHeader { public int Address; public bool Immediate; } 

يرشد العلم الحالي المترجم الفوري إلى ضرورة تنفيذ هذه الكلمة في وضع البرمجة ، وليس كتابتها إلى الذاكرة. من الناحية التخطيطية ، يمكن تمثيل منطق المترجم الشفهي على النحو التالي: اليد اليمنى هي نعم ، اليسار هو لا.


سوف نستخدم TextReader لقراءة دفق الإدخال و TextWriter لإخراجها.

 public TextReader Input; public TextWriter Output; 

سيتم تنفيذ المترجم الفوري وفقًا للمخطط أعلاه في وظيفة واحدة.

 void Interpreter() { while (true) { var word = ReadWord(Input); if (string.IsNullOrWhiteSpace(word)) return; // EOF var lookup = LookUp(word); if (IsEvalMode) { if (lookup != null) { Execute(lookup.Address); } else if (IsConstant(word)) { DS.Push(ParseNumber(word)); } else { DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } else { // program mode if (lookup != null) { if (lookup.Immediate) { Execute(lookup.Address); } else { AddOp(lookup.Address); } } else if (IsConstant(word)) { AddOp(LookUp("doLit").Address); AddOp(ParseNumber(word)); } else { IsEvalMode = true; DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } } } 

يتم إجراء التفسير في حلقة ، يتم تنفيذ إخراجها عند الوصول إلى نهاية دفق الإدخال (على سبيل المثال ، نهاية الملف) ، بينما ترجع الدالة ReadWord سلسلة فارغة. مهمة ReadWord هي إرجاع الكلمة التالية مع كل مكالمة.

 static string ReadWord(TextReader sr) { var sb = new StringBuilder(); var code = sr.Read(); while (IsWhite((char)code) && code > 0) { code = sr.Read(); } while (!IsWhite((char)code) && code > 0) { sb.Append((char)code); code = sr.Read(); } return sb.ToString(); } static bool IsWhite(char c) { return " \n\r\t".Any(ch => ch == c); } 

بعد قراءة الكلمة ، يتم إجراء محاولة للعثور عليها في القاموس. إذا نجحت ، يتم إرجاع عنوان الكلمة ؛ وإلا ، تكون خالية.

 public WordHeader LookUp(string word) { if (Entries.ContainsKey(word)) { return Entries[word].Last(); } return null; } 

يمكنك التحقق مما إذا كانت القيمة التي تم إدخالها هي رقم من أول حرفين. إذا كان الحرف الأول هو رقم ، فإننا نفترض أنه رقم. إذا كان الحرف الأول علامة "+" أو "-" ، والثاني عبارة عن رقم ، فعلى الأرجح يكون هذا أيضًا رقمًا.

 static bool IsConstant(string word) { return IsDigit(word[0]) || (word.Length >= 2 && (word[0] == '+' || word[0] == '-') && IsDigit(word[1])); } 

لتحويل سلسلة إلى رقم ، يمكنك استخدام الطرق القياسية Int32.TryParse و Double.TryParse. لكنها لا تختلف في السرعة لعدد من الأسباب ، لذلك أنا استخدم حل مخصص.

 static object ParseNumber(string str) { var factor = 1.0; var sign = 1; if (str[0] == '-') { sign = -1; str = str.Remove(0, 1); } else if (str[0] == '+') { str = str.Remove(0, 1); } for (var i = str.Length - 1; i >= 0; i--) { if (str[i] == '.') { str = str.Remove(i, 1); return IntParseFast(str) * factor * sign; } factor *= 0.1; } return IntParseFast(str) * sign; } static int IntParseFast(string value) { // An optimized int parse method. var result = 0; foreach (var c in value) { if (!(c >= '0' && c <= '9')) return result; // error result = 10 * result + (c - 48); } return result; } 

يمكن لطريقة ParseNumber تحويل كل من القيم الصحيحة وأرقام الفاصلة العائمة ، على سبيل المثال ، "1.618".

يحدث تنفيذ الكلمة بنفس الطريقة التي استخدمناها في تشغيل مترجم العنوان. في حالة حدوث استثناء ، سيتم طباعة تتبع مكدس لمترجم العنوان.

 public void Execute(int address) { try { if (address < Core.Count) { // eval core Core[address](); // invoke core function } else { // eval word IP = 0; // set return address WP = address; // set eval address DoList(); // fake doList Next(); // run evaluator } } catch (Exception e) { Output.WriteLine(e.Message); var wpEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == WP)); var ipEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == SearchKnowAddress(IP))); Output.WriteLine($"WP = {WP:00000} - '{wpEntry.Key}', IP = {IP:00000} - '{ipEntry.Key}'"); if (RS.Any()) { Output.WriteLine("Stack trace..."); foreach (var a in RS) { var ka = SearchKnowAddress(a); var sEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == ka)); Output.WriteLine($"...{a:00000} -- {sEntry.Key}"); } RS.Clear(); DS.Clear(); } else if (address < Core.Count) { var entry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == address)); Output.WriteLine($"Core word is {entry.Key}"); } IP = WP = 0; } } 

عندما يكون المترجم في وضع الترجمة ولم يتم وضع علامة على الكلمة للتنفيذ الفوري ، يجب كتابة عنوانها على الذاكرة.

 public void AddOp(object op) { Mem[Here++] = op; } 

المتغير هنا يخزن عنوان الخلية الحرة التالية. نظرًا لأنه يجب الوصول إلى هذا المتغير من بيئة وقت التشغيل كمتغير للغة Fort ، يتم تخزين القيمة هنا في ذاكرة البرنامج عند إزاحة معينة.

 public int _hereShift; public int Here { get => (int)Mem[_hereShift]; set => Mem[_hereShift] = value; } 

للتمييز بين الثابت العددي وعنوان الكلمة أثناء التفسير ، يتم تجميع تجميع للكلمة doLit قبل كل ثابت ، والذي يقرأ القيمة التالية في الذاكرة ويضعها في حزمة البيانات.

 public void DoLit() { DS.Push(Mem[IP++]); } 

لقد وصفنا مترجمي النصوص والعناوين ؛ ويتضمن المزيد من التطوير ملء النواة بالذرات. تحتوي الإصدارات المختلفة من "Fort" على مجموعة مختلفة من الكلمات الأساسية ، وسيكون التنفيذ الأكثر دقة هو ، ربما ، الذي يحتوي على 31 بدائية فقط. نظرًا لأن البدائية تعمل بشكل أسرع من كلمات المستخدم المركبة ، فإن تطبيقات Fort الدنيا تكون عادةً أبطأ من تطبيقات verbose. يمكن العثور هنا على مقارنة لمجموعة كلمات العديد من إصدارات المترجمين الفوريين.

في المترجم الموصوف هنا ، حاولت أيضًا عدم تضخيم قاموس الكلمات الأساسية بشكل غير ضروري. ولكن لسهولة التكامل مع منصة .net ، قررت تطبيق الرياضيات ، والعمليات المنطقية ، وبالطبع التفكير من خلال مجموعة من الأوليات. في الوقت نفسه ، هناك بعض الكلمات التي غالبًا ما تكون بدائية في تطبيقات Fort مفقودة هنا ، مما يعني التنفيذ بواسطة المترجم الفوري.

في وقت كتابة هذا التقرير ، كانت المجموعة الأساسية هي 68 كلمة.
 // Core SetCoreWord("nop", Nop); SetCoreWord("next", Next); SetCoreWord("doList", DoList); SetCoreWord("exit", Exit); SetCoreWord("execute", Execute); SetCoreWord("doLit", DoLit); SetCoreWord(":", BeginDefWord); SetCoreWord(";", EndDefWord, true); SetCoreWord("branch", Branch); SetCoreWord("0branch", ZBranch); SetCoreWord("here", GetHereAddr); SetCoreWord("quit", Quit); SetCoreWord("dump", Dump); SetCoreWord("words", Words); SetCoreWord("'", Tick); SetCoreWord(",", Comma); SetCoreWord("[", Lbrac, true); SetCoreWord("]", Rbrac); SetCoreWord("immediate", Immediate, true); // Mem SetCoreWord("!", WriteMem); SetCoreWord("@", ReadMem); SetCoreWord("variable", Variable); SetCoreWord("constant", Constant); // RW SetCoreWord(".", Dot); SetCoreWord(".s", DotS); SetCoreWord("cr", Cr); SetCoreWord("bl", Bl); SetCoreWord("word", ReadWord, true); SetCoreWord("s\"", ReadString, true); SetCoreWord("key", Key); // Comment SetCoreWord("(", Comment, true); SetCoreWord("\\", CommentLine, true); // .net mem SetCoreWord("null", Null); SetCoreWord("new", New); SetCoreWord("type", GetType); SetCoreWord("m!", SetMember); SetCoreWord("m@", GetMember); SetCoreWord("ms@", GetStaticMember); SetCoreWord("ms!", SetStaticMember); SetCoreWord("load-assembly", LoadAssembly); SetCoreWord("invk", invk); // Boolean SetCoreWord("true", True); SetCoreWord("false", False); SetCoreWord("and", And); SetCoreWord("or", Or); SetCoreWord("xor", Xor); SetCoreWord("not", Not); SetCoreWord("invert", Invert); SetCoreWord("=", Eql); SetCoreWord("<>", NotEql); SetCoreWord("<", Less); SetCoreWord(">", Greater); SetCoreWord("<=", LessEql); SetCoreWord(">=", GreaterEql); // Math SetCoreWord("-", Minus); SetCoreWord("+", Plus); SetCoreWord("*", Multiply); SetCoreWord("/", Devide); SetCoreWord("mod", Mod); SetCoreWord("1+", Inc); SetCoreWord("1-", Dec); // Stack SetCoreWord("drop", Drop); SetCoreWord("swap", Swap); SetCoreWord("dup", Dup); SetCoreWord("over", Over); SetCoreWord("rot", Rot); SetCoreWord("nrot", Nrot); 


لتعريف كلمات مستخدم جديدة ، يتم استخدام كلمتين kernel: ":" و "؛". تقرأ الكلمة ":" اسم كلمة جديدة من دفق الإدخال ، وتقوم بإنشاء رأس بهذا المفتاح ، ويضاف عنوان الكلمة الأساسية doList إلى ذاكرة البرنامج ، ويتم وضع المترجم الشفهي في وضع الترجمة. سيتم تجميع جميع الكلمات اللاحقة ، باستثناء الكلمات التي تم تعليمها على أنها فورية.

 public void BeginDefWord() { AddHeader(ReadWord(Input)); AddOp(LookUp("doList").Address); IsEvalMode = false; } 

ينتهي التجميع بكلمة "؛" ، التي تكتب عنوان كلمة "خروج" في ذاكرة البرنامج وتضعها في وضع التفسير. يمكنك الآن تحديد الكلمات المخصصة - على سبيل المثال ، الحلقات ، وبيان شرطي ، وغيرها.

 Eval(": ? @ . ;"); Eval(": allot here @ + here ! ;"); Eval(": if immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": then immediate dup here @ swap - swap ! ;"); Eval(": else immediate [ ' branch , ] , here @ 0 , swap dup here @ swap - swap ! ;"); Eval(": begin immediate here @ ;"); Eval(": until immediate doLit [ ' 0branch , ] , here @ - , ;"); Eval(": again immediate doLit [ ' branch , ] , here @ - , ;"); Eval(": while immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": repeat immediate doLit [ ' branch , ] , swap here @ - , dup here @ swap - swap ! ;"); Eval(": // immediate [ ' \\ , ] ;"); // C like comment 

لن أصف بقية الكلمات القياسية هنا - فهناك معلومات كافية عنها على الشبكة حول الموارد المواضيعية المقابلة. للتفاعل مع المنصة ، حددت 9 كلمات:

  • "خالية" - يدفع فارغة على المكدس.
  • "النوع" - يدفع نوع الفصل إلى كومة "word TrueForth.MyClass type" ؛
  • "جديد" - يأخذ النوع من المكدس ، ويقوم بإنشاء مثيل للفئة ويضعه على المكدس ، ويجب أن تكون وسيطات المُنشئ ، إن وجدت ، موجودة أيضًا في المجموعة "word TrueForth.MyClass type new" ؛
  • "M!" - يأخذ مثيل كائن ، اسم الحقل ، القيمة من المكدس ويعين قيمة إلى الحقل المحدد ؛
  • "M @" - يلتقط مثيل كائن من المكدس ، واسم الحقل ويعيد قيمة الحقل إلى المكدس ؛
  • "Ms!" و "ms @" - على غرار الحقول السابقة ، ولكن بالنسبة للحقول الثابتة ، بدلاً من مثيل ، يجب أن يكون هناك نوع في الحزمة.
  • "تحميل التجميع" - يأخذ من المكدس ، والسماح لها بالتجمع وتحميله في الذاكرة ؛
  • "Invk" - يأخذ المفوض ، وسيطات من المكدس ويطلق عليه "1133 كلمة SomeMethod word TrueForth.MyClass type new m @ invk".

وصفت النقاط الرئيسية لتطبيق لغة Fort ، هذا التطبيق لا يسعى لدعم معايير ANSI للغة ، حيث أن مهمته هي تنفيذ محرك لبناء DSL ، وليس لتنفيذ لغة للأغراض العامة. في معظم الحالات ، يكون المترجم الفوري المتطور كافياً لبناء لغة بسيطة للموضوع.

هناك عدة طرق لاستخدام المترجم أعلاه. على سبيل المثال ، يمكنك إنشاء مثيل للمترجم الشفهي ، ثم إرسال برنامج نصي للتهيئة إلى الإدخال ، يتم فيه تحديد الكلمات الضرورية. هذا الأخير من خلال التفكير تتفاعل مع النظام.

 public static bool Init4Th() { Interpreter = new OForth(); if (File.Exists(InitFile)) { Interpreter.Eval(File.ReadAllText(InitFile)); return true; } else { Console.WriteLine($"  {InitFile}  !"); return false; } } 

تقرير تكوين نظام توزيع مثال

 ( *****   ***** ) word GetFReporter word ReportProvider.FlexReports.FReporterEntry type new m@ invk constant fr //       :  word ReportProvider.FlexReports.FDailyReport type new ; //       :  word AddReport fr m@ invk ; //          :  [ ' word , ] ; //   :  [ ' word , ] ; //   :  [ ' s" , ] ; //  ,      " :  ; //  :  dup [ ' word , ] swap word MailSql swap m! ; :  dup [ ' word , ] swap word XlsSql swap m! ; ( *****    ***** ) cr s"   " . cr cr    "  08:00  mail@tinkoff.ru   seizure.sql    ,    "  08:00  mail@tinkoff.ru   fixed-errors-top.sql   fixed-errors.sql         WO"  08:00  mail@tinkoff.ru   wo-wait-complect-dates.sql       "  07:30  mail@tinkoff.ru   top-previous-input-errors.sql   previous-input-errors.sql        "  10:00  mail@tinkoff.ru   collection-report.sql       BPM   "  08:00  mail@tinkoff.ru   bpm-inbox-report.sql       ScanDoc3   7 "  07:50  mail@tinkoff.ru   new-sd3-complects-prevew.sql   new-sd3-complects.sql  ( ******************************** ) cr s"  " . cr 

يمكنك القيام بخلاف ذلك: تمرير الكائنات الجاهزة إلى إدخال المترجم الشفهي من خلال مكدس البيانات ثم التفاعل معهم من خلال المترجم الفوري. على سبيل المثال ، فعلت لاستعادة إعدادات الجهاز لتلقي بمسح المستندات أو الماسح الضوئي أو كاميرا ويب أو جهاز افتراضي (للتصحيح أو التدريب). في هذه الحالة ، تكون مجموعة المعلمات والإعدادات وترتيب التهيئة للأجهزة المختلفة مختلفة تمامًا ويتم حلها بسهولة من خلال المترجم الحصن.

 var interpreter = new OForth(); interpreter.DS.Push(this); // Push current instance on DataStack interpreter.Eval("constant arctium"); // Define constant with the instance if (File.Exists(ConfigName)) { interpreter.Eval(File.ReadAllText(ConfigName)); } 

يتم إنشاء التكوين برمجيًا ، ويظهر شيء مثل هذا:

 s" @device:pnp:\\?\usb#vid_2b16&pid_6689&mi_00#6&1ef84f63&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" s" Doccamera" word Scanning.Devices.PhotoScanner.PhotoScannerDevice type new dup s" 3264x2448, FPS:20, BIT:24" swap word SetSnapshotMode swap m@ invk dup s" 1280x720, FPS:30, BIT:24" swap word SetPreviewMode swap m@ invk word SetActiveDevice arctium m@ invk 

بالمناسبة ، يتم إنشاء البرامج النصية * .ps و * .pdf بطريقة مماثلة ، لأن كلا من PostScript و Pdf يمثلان في الأساس مجموعة فرعية من "Fort" ، لكنهما يستخدمان حصريًا لتقديم المستندات على الشاشة أو الطابعة.

من السهل تنفيذ الوضع التفاعلي لوحدة التحكم وليس للتطبيقات فقط. للقيام بذلك ، يجب أولاً تهيئة النظام من خلال البرنامج النصي المعد ، ثم البدء في التفسير عن طريق تعيين المترجم الشفوي على المدخلات القياسية STDIN.

 var interpreter = new OForth(); const string InitFile = "Init.4th"; if (File.Exists(InitFile)) { interpreter.Eval(File.ReadAllText(InitFile)); } else { Console.WriteLine($"  {InitFile}  !"); } interpreter.Eval(Console.In); // Start interactive console 

يمكن أن يكون نص التهيئة مثل هذا:

 ( *****   ***** ) word ComplectBuilder.Program type constant main //     : mode! [ ' word , ] word Mode main ms! ; //    : init word Init main ms@ invk ; //  : load [ ' word , ] word LoadFile main ms@ invk ; //   : start word StartProcess main ms@ invk ; //   : count word Count main ms@ invk ; //   : all count ; //  ( *****  ***** ) init cr cr s"    ,     help" . cr cr ( *****  ***** ) : help s"         :" . cr s" load scandoc_test.csv 0 all start" . cr bl bl s" load scandoc_test.csv --    " . cr bl bl s" 0 all start --  ,  0      all " . cr cr s"     DEV TEST PROD:" . cr s" mode! DEV init" . cr s"     :" . cr s" word Mode main ms@ . cr" . cr ; 

كمدخلات ، لا يمكن أن يكون هناك وحدة تحكم أو نص فقط من تطبيق TextBox مع واجهة مستخدم ، ولكن أيضًا شبكة. في هذه الحالة ، يمكنك تطبيق تحكم تفاعلي بسيط ، على سبيل المثال ، خدمة لتصحيح الأخطاء وبدء المكونات وإيقافها. وتقتصر إمكانيات هذا الاستخدام من خيال المطور والمهمة في متناول اليد. , UI - .

. , , .

, :

 public void Callback(string word, MulticastDelegate action) { if (string.IsNullOrWhiteSpace(word) || word.Any(c => " \n\r\t".Any(cw => cw == c))) { throw new Exception("invalid format of word"); } DS.Push(action); Eval($": {word} [ ' doLit , , ] invk ;"); } 

DS.Push(action), . , , [ ], , . ' Tick , doLit, , . Comma «,» doLit, .

, . , :

 public class WoConfItem { public string ComplectType; public string Route; public string Deal; public bool IsStampQuery; } 

— , :

 public class WoConfig { private OForth VM; private List<WoConfItem> _conf; public WoConfig(string confFile) { _conf = new List<WoConfItem>(); VM = new OForth(); //      VM.Callback("new-conf", new Action(ClearConf)); VM.Callback("{", new Func<WoConfItem>(NewConf)); VM.Callback("}", new Action<WoConfItem>(AddConf)); VM.Callback("complect-type", new Func<WoConfItem,string,WoConfItem>(ConfComplectType)); VM.Callback("route", new Func<WoConfItem,string,WoConfItem>(ConfRoute)); VM.Callback("deal", new Func<WoConfItem,string,WoConfItem>(ConfDeal)); VM.Callback("is-stamp-query", new Func<WoConfItem,bool,WoConfItem>(ConfIsStampQuery)); //  ,   ,       var initScript = new StringBuilder(); initScript.AppendLine(": complect-type [ ' word , ] swap complect-type ;"); initScript.AppendLine(": route [ ' word , ] swap route ;"); initScript.AppendLine(": deal [ ' word , ] swap deal ;"); initScript.AppendLine(": is-stamp-query ' execute swap is-stamp-query ;"); VM.Eval(initScript.ToString()); //   WatchConfig(confFile); } private void ReadConfig(string path) { using (var reader = new StreamReader(File.OpenRead(path), Encoding.Default)) { VM.Eval(reader); } } readonly Func<string, bool> _any = s => s == "*"; public WoConfItem GetConf(string complectType, string routeId) { return _conf?.FirstOrDefault(cr => (cr.ComplectType == complectType || _any(cr.ComplectType)) && (cr.Route == routeId || _any(cr.Route)) ); } public bool IsAllow(string complectType, string routeId) { return GetConf(complectType, routeId) != null; } void WatchConfig(string path) { var directory = Path.GetDirectoryName(path); var fileName = Path.GetFileName(path); //   ,     if (!File.Exists(path)) { if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var sb = new StringBuilder(); sb.AppendLine("\\ WO passport configuration"); sb.AppendLine("new-conf"); sb.AppendLine(""); sb.AppendLine("\\ Config rules"); sb.AppendLine("\\ { -- begin config item, } -- end config item, * -- match any values"); sb.AppendLine("\\ Example:"); sb.AppendLine("\\ { complect-type * route offer deal 100500 is-stamp-query true }"); sb.AppendLine(""); File.WriteAllText(path, sb.ToString(), Encoding.Default); } //   ReadConfig(path); //     var fsWatcher = new FileSystemWatcher(directory, fileName); fsWatcher.Changed += (sender, args) => { try { fsWatcher.EnableRaisingEvents = false; //        , //     ,   //     Thread.Sleep(1000); ReadConfig(path); } catch (Exception e) { Console.WriteLine(e); } finally { fsWatcher.EnableRaisingEvents = true; } }; fsWatcher.EnableRaisingEvents = true; } //  ,    void ClearConf() { _conf.Clear(); } void AddConf(WoConfItem conf) { _conf.Add(conf); } static WoConfItem NewConf() { return new WoConfItem(); } static WoConfItem ConfComplectType(WoConfItem conf, string complectType) { conf.ComplectType = complectType; return conf; } static WoConfItem ConfRoute(WoConfItem conf, string route) { conf.Route = route; return conf; } static WoConfItem ConfDeal(WoConfItem conf, string deal) { conf.Deal = deal; return conf; } static WoConfItem ConfIsStampQuery(WoConfItem conf, bool isStampQuery) { conf.IsStampQuery = isStampQuery; return conf; } } 


:

 \ WO passport configuration new-conf \ Config rules \ { -- begin config item, } -- end config item, * -- match any values \ Example: \ { complect-type * route offer deal 100500 is-stamp-query true } \ ***** offer ***** { complect-type offer route offer is-stamp-query false deal 5c18e87bfeed2b0b883fd4df } { complect-type KVK route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type quick-meeting route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type exica route offer is-stamp-query true deal 5d03a894e2f5850001435492 } { complect-type reissue route offer is-stamp-query true deal 5d03a894e2f5850001435492 } \ ***** offer-flow ***** { complect-type KVK route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type reissue route offer-flow is-stamp-query true deal 5d03a894e2f5850001435492 } 

, , DSL — .

, «». DSL.

, , — , , , , — . , .

— , . — , — !

, .

- .

حظا سعيدا

Source: https://habr.com/ru/post/ar477902/


All Articles