
في هذه الأيام الرائعة لشهر يناير ، نحن جميعًا ، بالطبع ، نشعر بالقلق إزاء مسألة تقليل الكود المصدري مع الحفاظ على المتغير. يعني لا يهمني؟!؟ عبثا ... هنا سقط المترجم ، والبرنامج ضخم - من غير المناسب للمطورين إرسال شيء من هذا القبيل. ثم تبدأ المتعة: ماذا لو رميتها بعيدًا؟ أوه ، هذا لا يسقط - حسناً ، نحن نغادر ، لكن ماذا لو كان كذلك؟ - لا يزال يتعطل ، وهذا ، وهذا ، وذاك ... أوه ، لقد بدأت المترجم على المصادر القديمة.
في نفس الوقت ، فإن أتمتة البحث عن الأخطاء هي مسألة حياة: git bisect ، llvm-bugpoint ، creduce ، ... في هذه المقالة ، سأشرح طريقة أخرى لحل مشكلة تبسيط حالة الاختبار ، عالمية بشكل أو بآخر فيما يتعلق بلغة البرمجة ، وإظهار بعض أمثلة للاستخدام.
Universal، say ... ربما ، بالطبع ، تم تنفيذه بالفعل عشر مرات ، لكن هل سيوقف الدراج المتمرس. وعندما تكون في يد المجهر ، كل المشكلات تبدو وكأنها أظافر. في دور المجهر - PMD .
بشكل عام ، هناك مثل هذه اللغة لكتابة النماذج التي وصفتها المعادلات التفاضلية - Modelica. انها متقدمة جدا ، وهناك تطبيق مفتوح جدا وبصفة عامة. ولكن هناك مشكلة صغيرة: في بعض الأحيان يحدث خطأ مترجم داخلي في المحول البرمجي. في مقال سابق ، لقد أوضحت بالفعل كيفية إضافة دعم Modela إلى محلل ثابت PMD (بالمناسبة ، ظهرت بعض أخطائي أثناء عملية المراجعة) بالإضافة إلى الوحدات اللغوية العشر الموجودة. لذلك قررت الآن - ما هو جيد أن تختفي - وأرسلت أداة إثبات صحة مفهوم شفرة المصدر ، أعيد استخدام وحدات اللغة الحالية PMD. لسوء الحظ ، تم إرسالي ، وإن لم يكن بعيدًا ، إلى مستودع تخزين مجاور: قال المعلم إنه ما زال لا يقرر دعم ذلك حتى نهاية القرن ، وقد ضاع في قاعدة الكود الموحد ، لذلك سرعان ما وجدت دعوة للمتعاون في صندوق الوارد الخاص بي تم إنشاؤه حديثا pmd / pmd-scm .
أولاً ، ما هو بيان المشكلة؟ يجب تقليل حجم الكود المصدري ، مع الحفاظ على بعض ممتلكاته. بشكل أكثر تحديدا ، أريد
- الرمز الناتج كان متوقعًا
- لا يجب أن يكون الحد الأدنى ممكنًا ، "إتمام الملف" مسموح به ، لكن يجب ألا يتحول إلى عذاب
- لا يعتبر التعتيم التلقائي
- يمكن تقسيم البرنامج المصغر إلى عدة ملفات مع شفرة المصدر
- على سبيل المثال ، في Java ، يجب أن تكون كل فئة
public
في ملف منفصل بالاسم الصحيح ، إلخ. - أريد أن يظل كل ملف مرئيًا في النهاية
- في عملية التحولات ، يجب الحفاظ على الثابت المشار إليه
الجهاز الداخلي
في هذا القسم ، سأصف التطبيق تقريبًا. بادئ ذي بدء ، ألاحظ مرة أخرى أن هذه الفكرة ، بعبارة ملطفة ، ليست جديدة. وإذا استشهدت بـ git bisect كمثال على "آلية تصحيح الأخطاء التلقائية" ، فقد صادفت ذلك أو شيء مشابه لبعض الوقت (على الرغم من أنني لم أستخدمه). لكن كان علي استخدام llvm-bugpoint - إنه أمر مثير للإعجاب: أنت تجلس وتصحح LLVM Pass الخاص بك ، وهي - مثل هذه العدوى - تتعطل في بعض أنظمة الملفات. لذلك ، يمكنك ترجمة هذا الملف في LLVM bitcode ، والتشغيل على ملف .bc مع البرنامج المساعد الخاص بك والتأكد من تعطله. وبعد ذلك ، تقريبًا ، استبدلت للتو بـ llvm-bugpoint
، وبعد دقيقة حصلت على "هيكل عظمي" ضخم لإحدى الوظائف ، يتكون من سحابة من الكتل الأساسية والتحولات بينها ؛ كل شيء ولكن تم إلقاء الفروع بنجاح. بالمناسبة ، كما هو الحال في بياني للمشكلة ، وبعد مرور عشر دقائق من التبسيط اليدوي ، كل ذلك يرجع إلى حقيقة أنني عالجت بشكل غير صحيح أحد أنواع الفروع ، وكان كل شيء آخر مجرد مشهد. بشكل عام ، الفكرة ليست جديدة.
والآن للتنفيذ. نظرًا لأنه كان من المفترض أن يكون أداة عالمية إلى حد ما ، فقد أردت إنشاء نوع من الإطار الذي يمكننا من خلاله تنفيذ تطبيقات محسنة للغات المختلفة. ونتيجة لذلك ، برز مفهومان متعامدان إلى حد ما:
- ثابت دعم
- استراتيجية التقليل
ثابتة
أحد الخيارات للملكية التي قد ترغب في حفظها ، لقد سبق أن وصفت: "قام المحول البرمجي بطباعة عبارة إلى وحدة التحكم" ("خطأ المحول البرمجي الداخلي" ، على سبيل المثال). أيضًا ، يمكن جمع الأفكار من مجموعة متنوعة من الألغاز: AFL و Killerbeez وغيرها:
- انتهت العملية ببعض كود الإرجاع (على وجه الخصوص ، "تعطل الإشارة" على نظام Linux)
- استغرقت العملية أكثر من مللي ثانية. هنا ، للأسف ، قد تحدث حالة عدم الاستقرار بسبب الحمل العائم للنظام. لمزيد من الدقة ، يمكنك استخدام وقت وحدة المعالجة المركزية ، على الرغم من أن عدادات الأداء مفيدة بشكل مثالي
- يمكن للمترجم (لا) إنشاء بعض ملفات الإخراج
- ...
لقد قمت بالفعل بتنفيذ بعضها (رمز الإرجاع ، الخط المطبوع) ، البعض الآخر ، قد يكون بعضها خاصًا باللغات الفردية. بشكل عام ، يعد هذا جزءًا من المهمة المستقلة إلى حد ما ، وغالبًا ما يكون مستقلاً تمامًا عن لغة معينة.
استراتيجية التقليل
للوهلة الأولى ، كل شيء هنا يعتمد فقط على اللغة. تحتاج في مكان ما إلى التخلص من المتغيرات جنبًا إلى جنب مع حالات الاستخدام ، في مكان ما - للحفاظ على نفس العدد من المعادلات والمجهول. الأهم من ذلك هو خلاصة هذا الجزء من الكود. ولكن يمكن جعل شيء أساسي مشتركًا للجميع: على سبيل المثال ، مع حركة بسيطة في اليد ، يتحول محرك قواعد XPath ... يتحول ... ... أوه ، بالنسبة إلى الإصدار 1.0 من XPath ، لا يتم تشغيله. عذرًا ، مشكلة تقنية صغيرة - في أداة عالمية لتقليص أشجار بناء الجملة لفصل الشتاء . بشكل عام ، تعد واجهة برمجة التطبيقات لإستراتيجية التصغير بسيطة للغاية: إلى حد كبير ، تحتوي الوظيفة على وظيفة خطوة (يتم منحها قائمة بالعقد الجذرية لأشجار بناء الجملة) ، والتي يمكن أن تسأل "لكن حاول التخلص من هذه الفروع للخارج" من خلال واجهة العملية. في حالة انتهاك المتغير ، تُرجع الدالة التحكم ؛ وإذا لم يحدث ذلك ، فإنها تدور في بنية تخزين العناصر عن طريق طرح استثناء خاص ، ويتم اعتبار الإصدار الناتج هو التقريب التالي. تعتبر العملية مكتملة إذا أرجعت دالة الخطوة التحكم وليس من خلال استثناء. قد تقول أن هذا غير فعال بشكل رهيب - ربما يكون كذلك ، زاتو كونفينينت! ولكن ماذا عن كل ذلك ، إذا كان من المفترض أن تبدأ عملية التحويل البرمجي الخارجية بانتظام مئات المرات في دورة ما.
هذا يطرح السؤال: كيفية الحصول على مصدر من AST ضعيفة؟ بالطبع ، يمكنك محاولة كتابة رمز خاص لكل لغة تقوم بإنشاء ملف نصي. ولكن ، كما تعلمون ، يجب أن يكون المبرمج كسولًا. لذلك لن ينجح - من الواضح أنه ليس من السلسلة "اختار التطبيقات الحالية وانطلق". لحسن الحظ ، توجد في عُقد الشجرة معلومات حول صفوف وأعمدة البداية والنهاية لهذه العقدة - وهذا يعني ، إذا كانت وحدة اللغة تشير إلى هذه المعلومات بشكل صحيح ، فيمكنك أخذ النص المصدر وقطع الأجزاء بعناية منه (وبالتالي ، بالمناسبة ، وبعض الصعوبة في التعتيم: لا يكفي رمي شيء ما ، ولكن يجب استبداله: معرفات ، على سبيل المثال). بالمناسبة ، في قاعدة كود PMD ، وجدنا حتى فصلاً لتحرير ملف باستخدام عمليات قطع نص غير متقاطعة (بالإضافة إلى الإضافة ، لكن هذا ليس مهمًا جدًا لهذه المهمة المحددة).
النظرية: ملخص
في هذه اللحظة قمت بتنفيذ استراتيجيتين. أحدها هو XPath trimming ، وهو ، إلى حد ما ، حالة متدهورة ، لأنه يكتب النتيجة بالقوة ، حتى لو لم يعد مصدرًا صحيحًا بناءً عليها. والثاني هو بالفعل تكرارية وتفاعلية "صادقة" (بمعنى أنها تتفاعل مع المترجم وتتحقق من المتغير): إنها تحاول فقط رمي الفروع الواحدة تلو الأخرى في حلقة. إليك المزيد حول هذا الموضوع:
- من حيث المبدأ ، فإن التحقق من القيمة الثابتة للمصدر ، الذي لم يتم تحليله ، لا معنى له لاستراتيجية "صادقة": حتى إذا نجا المترجم من هذا ، فسيتعين على المصغر إعادة تشغيل المصدر. لذلك ، من المنطقي التخلص من الملفات "المعطلة" مقدمًا: تحليل العملية لديك أسرع من تشغيل برنامج التحويل البرمجي بالكامل
- في الحالة العامة ، قد يكون من المريح استخدام coroutines هنا (جيدًا ، أو تحويل تدفق التحكم من الداخل إلى الخارج) ، ولكن نظرًا لأن هذا بعيد عن الجزء الأكثر صعوبة من الناحية الحسابية من العمل ، في كل خطوة في وظيفة الخطوة ، أسير حول الشجرة ، وأعد المسافة قمم. أنا فقط أتذكر العداد. لذا ، في البداية اعتقدت أن القمة أكبر ، القمة أقل - ما الفرق الذي تحدثه ، أي شيء على مجريات الأمور! اتضح أن الخطأ لكل وحدة يمكن أن يغير معدل "التقارب" في بعض الأحيان. في جوهر الأمر ، هذا منطقي: غالبًا ما يكون طرح وظائف كاملة من الفصل أمرًا فعالًا. ولكن لتخطي بعض الشيء ، وفي كل مرة للذهاب إلى داخل الوظيفة ، وفي معظم الحالات لا علاقة لها بالمشكلة ، تبدو مثل هذه الفكرة.
- بالمناسبة حول coroutines ونشر تدفق التحكم: ستكون هناك بعض المشاكل مع هذا ، لأنه بعد إعادة تشغيل النص الذي تم تغييره ، قد لا يتغير AST بطريقة واضحة للغاية (أي ، لن يتم إلقاء هذه الفروع فقط ، ولكن في مكان ما ستختفي العقد الفارغة ، في مكان ما تحليل عموما سوف تذهب في الاتجاه الآخر). ناهيك عن أن عقد AST الجديد لن يكون متطابقًا بالرجوع إلى القديم ، والمطابقة المنطقية على
equals
- تبدو أيضًا مهمة صعبة - من حيث المبدأ ، يمكنك استخدام قدرات PMD بدرجات متفاوتة: يمكنك استخدام الأنواع المحسوبة ، تبعيات تعريف الاستخدام ، إلخ. وتفعل شيئًا ذكيًا جدًا (ولكن عليك التفكير في واجهة برمجة تطبيقات عالمية). من ناحية أخرى ، بالنسبة للاستراتيجية الموصوفة ، يكفي الحصول على شجرة تحليل. وهنا يمكنك ربط بعض هيكل Kaitai ومحاولة دفع afl-tmin لتقليل الملفات الثنائية :)
ممارسة
للبدء ، دعونا نبني مصغرًا:
git clone https://github.com/pmd/pmd-scm.git cd pmd-scm ./mvnw clean verify unzip pmd-scm-dist/target/pmd-scm-bin-1.0.0-SNAPSHOT.zip
أنت الآن بحاجة إلى بعض المصدر. لنأخذ GreedyStrategy.java على سبيل المثال.
باستخدام Rule Designer ، تعرف على شكل AST النموذجي لـ Java ، ثم قم بتشغيله
$ ./bin/run.sh scm --language java \ --input-file ../pmd-scm/src/main/java/net/sourceforge/pmd/scm/strategies/GreedyStrategy.java \ --output-file GreedyStrategy-min.java \ --strategy xpath \ --xpath-expression '//BlockStatement[count(../BlockStatement) > 1]' Original file(s): 6155 bytes, 1099 nodes. After initial white-space cleanup: size 4548 bytes (73%), 1099 nodes (100%) After pass #1: size 1984 bytes (32%), 1099 nodes (100%) After final white-space cleanup: size 1984 bytes (32%), 325 nodes (29%) After blank line clean up: size 1767 bytes (28%), 325 nodes (29%)
package net.sourceforge.pmd.scm.strategies; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sourceforge.pmd.lang.ast.Node; public class GreedyStrategy extends AbstractMinimizationStrategy { public static class Configuration extends AbstractConfiguration { @Override public MinimizationStrategy createStrategy() { return new GreedyStrategy(this); } } public static final MinimizationStrategyConfigurationFactory FACTORY = new AbstractFactory("greedy") { @Override public MinimizationStrategyConfiguration createConfiguration() { return new Configuration(); } }; private GreedyStrategy(Configuration configuration) { super(configuration); } private final Map<Node, HashSet<Node>> directlyDependingNodes = new HashMap<>(); private final Map<Node, Set<Node>> transitivelyDependingNodes = new HashMap<>(); private void fetchDirectDependentsFromSubtree(Node node) {
وهذا هو ، أفرغنا جميع الوظائف التي تحتوي مباشرة على أكثر من عبارة واحدة . ومع ذلك ، حذف التعليقات ، الأسطر الفارغة ، إلخ. لم أقم بتحديدها - بعد كل شيء ، هل هذه (حتى الآن؟) أداة في المقام الأول لتصحيح الأخطاء ، بدلاً من إنشاء أوصاف واجهة جميلة.
دعنا ننظر إلى شيء أكثر إثارة للاهتمام الآن: حاول تقليل ملفين بتعليقات من المترجم في نفس الوقت:
أصل / TestResource.java:
class TestResource { int func() { System.err.println("Hello World!"); return 123; } void unused() {
orig / Main.java:
class Main { public static void main(String[] args) { String str = new TestResource().func(); return 123; } }
كما ترون ، فهي لا تجمع:
$ javac orig/TestResource.java orig/Main.java orig/Main.java:3: error: incompatible types: int cannot be converted to String String str = new TestResource().func(); ^ orig/Main.java:5: error: incompatible types: unexpected return value return 123; ^ 2 errors
دعنا نتخيل أن هناك خطأ ما في الخطأ الأول ، وحاول تقديم مثال بسيط ينتج عنه
error: incompatible types: int cannot be converted to String
للقيام بذلك ، تشغيل
$ bin/run.sh scm --language java \ --input-file orig/TestResource.java orig/Main.java \ --output-file TestResource.java Main.java \ --invariant message \ --printed-message "error: incompatible types: int cannot be converted to String"\ --command-line "javac TestResource.java Main.java" \ --strategy greedy Original file(s): 290 bytes, 77 nodes. After initial white-space cleanup: size 258 bytes (88%), 77 nodes (100%) After pass #1: size 255 bytes (87%), 64 nodes (83%) After pass #2: size 244 bytes (84%), 57 nodes (74%) After pass #3: size 205 bytes (70%), 51 nodes (66%) After pass #4: size 192 bytes (66%), 46 nodes (59%) After pass #5: size 181 bytes (62%), 39 nodes (50%) After pass #6: size 179 bytes (61%), 39 nodes (50%) After final white-space cleanup: size 149 bytes (51%), 39 nodes (50%) After blank line clean up: size 147 bytes (50%), 39 nodes (50%)
نتيجة لذلك ، نحصل عليه
TestResource.java:
class TestResource { int func() { } }
Main.java:
class Main { public static void main() { String str = new TestResource().func(); } }
$ javac TestResource.java Main.java TestResource.java:3: error: missing return statement } ^ Main.java:3: error: incompatible types: int cannot be converted to String String str = new TestResource().func(); ^ 2 errors
كل شيء كما طلب!
يؤدي
حتى الآن ، لا يزال المشروع في مرحلة مبكرة إلى حد ما ، ولكن يوجد بالفعل شيء لإثباته. في المستقبل ، هناك أفكار لإنشاء واجهة برمجة التطبيقات للغة اللاأدائية التي تشير إلى التبعيات بين عقد AST ، لتمكين توفير استراتيجيات خاصة باللغة. سيكون من الجيد أيضًا وضع إستراتيجية عالمية تدير برنامج Groovy / Kotlin / script شيئًا آخر - بعد كل شيء ، فإن الأشخاص الذين يستخدمون Java ربما لم يروه أبدًا ، لكنهم يعرفون جيدًا تمامًا ، على سبيل المثال ، Modelika و لدينا في رؤوسهم طرقهم المتقدمة للضغط على المصدر.