صناعة المفسّرات › أنواع القيم 5 / 35 السابق التالي

أنواع القيم

حين تكون دبّاً دماغه صغيرٌ جداً وتفكّر في الأشياء، تجد أحياناً أنّ الشيء الذي بدا "شيبه بالأشياء" إلى حدٍّ كبير في داخلك يختلف تماماً حين يخرج إلى العلن، وينظر إليه آخرون.

إيه. إيه. ميلن، وينّي-ذي-بو

كانت الفصول القليلة الماضية ضخمة، مليئةً بتقنيّاتٍ معقّدة وصفحات من الشيفرة. أمّا في هذا الفصل فلا يوجد إلّا مفهومٌ واحدٌ جديد يتعلّمه، وبعض الشيفرة المباشرة. لقد كسبتَ استراحةً.

Lox مُنمَّطة ديناميكياً. فمتغيّرٌ واحد يستطيع أن يحمل قيمةً منطقيّة أو عدداً أو نصّاً في لحظاتٍ مختلفة من الزمن. على الأقلّ، هذه هي الفكرة. أمّا الآن، في clox، فكلّ القيم أعداد. ومع نهاية الفصل، ستدعم أيضاً القيم المنطقيّة و nil. ولئن لم تكنا شديدتَي الدلالة، فإنّهما تُجبراننا على اكتشاف كيف يمكن لتمثيل قيمنا أن يتعامل مع الأنواع المختلفة ديناميكياً.

الاتّحاديات المُوسَمة

الفائدة الجميلة من العمل بـC أنّنا نستطيع بناء بنى بياناتنا من البتات الخام صعوداً. والسيّئة أنّنا مضطرّون إلى ذلك. فلا تعطيك C ما يُذكر مجاناً وقت التصريف، بل أقلّ بكثير وقت التشغيل. وفيما يخصّ C، فالكون مصفوفةُ بايتاتٍ غير متمايزة، وعلىنا علينا أن نقرّر كم بايتاً من تلك نستعمل وما الذي تعنيه.

ولكي نختار تمثيلاً للقيمة، نحتاج إلى الإجابة عن سؤالين رئيسيّين:

  1. كيف نمثّل نوع القيمة؟ فإذا حاولتَ مثلاً أن تضرب عدداً في true، فيجب أن نكتشف ذلك الخطأ في وقت التشغيل ونبلّغ عنه. ولكي نفعل ذلك، نحتاج إلى أن نتمكّن من معرفة نوع أيّ قيمة.

  2. كيف نخزّن القيمة نفسها؟ فنحن لا نحتاج فقط إلى أن نعرف أنّ ثلاثة عدد، بل أنّه مختلف عن العدد أربعة. أعرف، يبدو بديهياً، صحيح؟ لكنّنا نعمل على مستوىٍ يكون من المفيد فيه توضيح هذه الأمور وكتابتها بعباراتٍ صريحة.

ولأنّنا لا نصمّم هذه اللّغة فحسب بل نبنيها بأنفسنا، فإنّنا عند الإجابة عن هذين السؤالين علينا أن نضع في الحسبان أيضاً سعي المنفّذ الأبديّ: إنجاز ذلك بكفاءة.

ابتكر قراصنة اللّغات على مرّ السنين طرقاً ذكيةً متنوّعة لحشر المعلومات أعلاه في أقلّ عددٍ ممكن من البتات. سنبدأ الآن بأبسط الحلول الكلاسيكية: اتّحادٍ مُوسَّم (tagged union). تحتوي القيمة على جزأين: "وسم" للنوع، وحمولة (payload) للقيمة الفعلية. ولتخزين نوع القيمة، نعرّف تعداداً (enum) لكلّ نوعٍ من قيم تدعمه الآلة الافتراضية.

^code value-type (2 before, 1 after)

أمّا الآن، فلدينا حالتان فقط، لكنّ هذا سينمو مع إضافتنا نصوصاً ودوالّ وأصنافاً إلى clox. وإلى جانب النوع، نحتاج أيضاً إلى تخزين بيانات القيمة -- الـdouble للعدد، و true أو false للقيمة المنطقيّة. يمكننا تعريف بنية (struct) فيها حقول لكلّ نوعٍ ممكن.

بنية (struct) بحقلين متجاورَين في الذاكرة.

لكنّ هذا مضيعةٌ للذاكرة. فلا يمكن أن تكون القيمة عدداً ومنطقيّةً في الوقت نفسه. لذا فإنّ واحداً من هذين الحقلين فقط هو ما سيُستعمل في أيّ لحظة. تتيح لك C أن تُحسّن هذا بتعريف اتّحاد (union). يبدو الاتّحاد كالبنية، لكنّ كلّ حقوله تتداخل في الذاكرة.

اتّحاد (union) بحقلين متداخلَين في الذاكرة.

حجم الاتّحاد هو حجم أكبر حقلٍ فيه. ولأنّ الحقول كلها تعيد استعمال البتات ذاتها، عليك توخّي بالغ الحذر عند العمل معها. فإن خزّنتَ البيانات باستخدام حقلٍ ثمّ فعّلتها باستخدام حقلٍ آخر، فستُعيد تفسير ما تعنيه البتات التحتيّة.

وكما يوحي اسم "الاتّحاد المُوسَّم"، فإنّ تمثيل القيمة الجديد يدمج هذين الجزأين معاً في بنيةٍ واحدة.

^code value (2 before, 2 after)

هناك حقلٌ لوسم النوع، ثمّ حقلٌ ثانٍ يحتوي الاتّحاد من كلّ القيم التحتيّة. وعلى آلةٍ بعرض 64 بت مع مصرّف C معتاد، يبدو التخطيط هكذا:

بنية القيمة الكاملة، مع حقلَي type وas متجاورَين في الذاكرة.

يأتي وسم النوع ذي البتات الأربعة أولاً، ثمّ الاتّحاد. تفضّل معظم البنى المحاذاةَ (alignment) على أن تكون القيم محاذاةً لحجمها. وبما أنّ حقل الاتّحاد يحتوي عدداً مزدوجاً من ثمانية بتات، يضيف المصرّف أربعة بتاتٍ من الحشو (padding) بعد حقل النوع لإبقاء ذلك العدد المزدوج على أقرب حدّ ثماني البتات. وهذا يعني أنّنا في الواقع ننفق ثمانية بتاتٍ على وسم النوع، مع أنّ كلّ ما يحتاجه هو تمثيل عددٍ بين الصفر والثلاثة. يمكننا حشر التعداد في حجمٍ أصغر، لكنّ ذلك كلّه يزيد الحشو فحسب.

إذن قيمنا ست عشر بايت، وهذا يبدو كبيراً قليلاً. سنُحسّنها لاحقاً. وفي هذه الأثناء، ما زالت صغيرةً بما يكفي لتُخزَّن في مكدّس C وتُمرَّر بالقيمة. ودلالات Lox تسمح بذلك لأنّ الأنواع الوحيدة التي ندعمها حتى الآن هي أنواع غير قابلة للتغيير (immutable). فإذا مرّرتَ إلى دالةٍ نسخةً من قيمة تحتوي العدد ثلاثة، فلا داعي للقلق من أن يرى المستدعي تعديلات على القيمة. فلا يمكنك "تعديل" ثلاثة. إنه ثلاثة إلى الأبد.

قيم Lox وقيم C

هذه هي تمثيل القيمة الجديد، لكنّنا لم ننتهِ بعد. فالجزء الباقي من clox يفترض حالياً أنّ Value اسمٌ بديل لـdouble. ولدينا شيفرة تُجري صبّاً مباشراً من C من أحدهما إلى الآخر. وهذه الشيفرة كلّها معطوبة الآن. محزن.

مع تمثيلنا الجديد، يستطيع Value أن يحتوي عدداً مزدوجاً، لكنّه ليس مكافئاً له. فهناك خطوة تحويل إلزامية للانتقال من أحدهما إلى الآخر. ونحتاج إلى المرور على الشيفرة وإدراج تلك التحويلات حتى يعمل clox من جديد.

سننفّذ هذه التحويلات بوصفها حفنةً من الماكروهات، ماكرو لكلّ نوع وعملية. أوّلاً، لترقية قيمة C أصلية إلى قيمة clox:

^code value-macros (1 before, 2 after)

كلّ واحدٍ من هذه يأخذ قيمة C من النوع المناسب وينتج قيمةً تحمل وسم النوع الصحيح وتحتوي القيمة التحتيّة. وهذا يرفع القيم المُنمَّطة ساكناً إلى كون Lox المُنمَّطة ديناميكياً. لكن لكي نفعل أيّ شيءٍ بقيمة، نحتاج إلى تفكيكها واستخراج قيمة C منها.

^code as-macros (1 before, 2 after)

تسير هذه الماكروهات في الاتجاه المعاكس. فمعطى قيمةٍ من النوع الصحيح، تفكّها وتُعيد قيمة C الخام المناظرة. وجزء "النوع الصحيح" مهمّ! فهذه الماكروهات تصل إلى حقول الاتّحاد مباشرةً. فلو فعلنا شيئاً مثل:

Value value = BOOL_VAL(true);
double number = AS_NUMBER(value);

فقد فتحنا بوابةً مشتعلة إلى عالم الظلال. وليس آمناً استعمال أيّ من ماكروهات AS_ ما لم نعرف أنّ القيمة تحتوي النوع المناسب. ولهذا نعرّف بضعة ماكروهات أخيرة للتحقّق من نوع القيمة.

^code is-macros (1 before, 2 after)

تُعيد هذه الماكروهات true إن كانت القيمة من ذلك النوع. وكلّما استدعينا أحد ماكروهات AS_، نحتاج إلى حمايته باستدعاء أحد هذه أوّلاً. ومع هذه الماكروهات الثمانية، صار بإمكاننا أن نتنقّل بأمان بين عالم Lox الديناميكي وعالم C الساكن.

الأعداد المُنمَّطة ديناميكياً

حصلنا على تمثيل القيمة والأدوات للتحويل إليه ومنه. ولم يبقَ إلّا أن نمشي في الشيفرة ونصلح كلّ موضعٍ تنقل فيه البيانات عبر ذلك الحدّ كي يعمل clox من جديد. وهذه إحدى أقسام الكتاب التي ليست مذهلة تماماً، لكنّني وعدتُك بأن أريك كلّ سطرٍ واحد من الشيفرة، فنحن هنا.

أوّل القيم التي نُنشئها هي الثوابت التي تتولّد عند تصريف القيم الحرفية للأعداد. وبعد أن نحوّل الرمز النصّي إلى عدد C مزدوج، نكتفي بلفّه في قيمة قبل تخزينه في جدول الثوابت.

^code const-number-val (1 before, 1 after)

وفي وقت التشغيل، لدينا دالة لطباعة القيم.

^code print-number-value (1 before, 1 after)

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

النفي الأحادي وأخطاء وقت التشغيل

أبسط عمليةٍ تالية هي النفي الأحادي. فهي تنزع قيمةً من المكدّس، تنفيها، وتدفع النتيجة. وبعد أن أصبح لدينا أنواعٌ أخرى من القيم، لم يعد بوسعنا افتراض أنّ المعامل عدد. فبوسع المستخدم أن يفعل:

print -false; // Uh...

نحتاج إلى معالجة ذلك بلباقة، وهذا يعني أنّه حان وقت أخطاء وقت التشغيل. فقبل إجراء عمليةٍ تتطلّب نوعاً معيّناً، نحتاج أن نتأكّد أنّ القيمة هي ذلك النوع.

وأما فحص النفي الأحادي فيبدو هكذا:

^code op-negate (1 before, 1 after)

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

للوصول إلى القيمة، نستعمل دالةً صغيرة جديدة.

^code peek

هي تُعيد قيمةً من المكدّس لكنّها لا تنزعها. ووسيط distance هو بُعد النظر إلى أسفل من أعلى المكدّس: الصفر هو الأعلى، وواحدٌ هو خانةٌ واحدة أسفل، وهكذا.

نُبلّغ عن خطأ وقت التشغيل باستعمال دالةٍ جديدة سنستفيد منها كثيراً في بقيّة الكتاب.

^code runtime-error

لقد استدعيتَ على الأرجح دوالّ متغيّرة الوسائط (variadic) -- أي التي تأخذ عدداً متغيّراً من الوسائط -- في C من قبل: printf() إحداها. لكنّك ربّما لم تعرِّف واحدةً خاصةً بك. وهذا الكتاب ليس دليلاً تعليمياً لـC tutorial، لذا سأمرّ عليها مروراً خاطفاً هنا، لكنّ الأساس هو أنّ ... و va_list يتيحان لنا تمرير عددٍ اعتباطيٍّ من الوسائط إلى runtimeError(). وهي توجّهها إلى vfprintf()، وهي النسخة من printf() التي تأخذ va_list صريحاً.

يستطيع المستدعون تمرير سلسلة تنسيق إلى runtimeError() يتبعها عددٌ من الوسائط، تماماً كما يستطيعون عند استدعاء printf() مباشرةً. ثمّ تُنسّق runtimeError() تلك الوسائط وتطبعها. ولن نستفيد من ذلك في هذا الفصل، لكنّ فصولاً لاحقة ستُنتج رسائل أخطاء وقت تشغيل منسّقة تحتوي بياناتٍ أخرى.

وبعد أن نعرض رسالة الخطأ المفيدة، نُخبر المستخدم بأيّ سطر من شيفرته كان يُنفَّذ لحظة وقوع الخطأ. وبما أنّنا تركنا الرموز في المصرّف، فنبحث عن السطر في معلومات التنقيب المُصرَّفة داخل القطعة (chunk). فإن أدّى مصرّفنا عمله على الوجه الصحيح، فذلك يوافق سطر الشيفرة المصدرية التي صُرِّفت منها شيفرة البايت.

ونبحث في مصفوفة أسطر التنقيب للقطعة باستخدام فهرس تعليمات شيفرة البايت الحالي ناقص واحد. والسبب أنّ المفسّر يتجاوز كلّ تعليمة قبل تنفيذها. لذا، عند النقطة التي نُنادي فيها runtimeError()، تكون التعليم الفاشلة هي التي سبقتها.

ولكي نستعمل va_list والماكروهات الخاصة بالعمل معها، نحتاج إلى استيراد ملفّ ترويسة قياسي.

^code include-stdarg (1 after)

وبهذا لا تستطيع آلتنا الافتراضية فحسب أن تفعل الشيء الصحيح حين ننفِي الأعداد (كما كانت تفعل قبل أن نكسرها)، بل تتعامل أيضاً بلباقة مع المحاولات الخاطئة لنفي أنواعٍ أخرى (وهي أنواعٌ لا نملكها بعد، لكن رغم ذلك).

معاملات الحساب الثنائيّة

أصبحت آليّة أخطاء وقت التشغيل في مكانها، لذا فإنّ إصلاح المعاملات الثنائيّة أسهل وإن كانت أعقد. نحن ندعم اليوم أربعة معاملات ثنائيّة: + و - و * و /. والفرق الوحيد بينها هو معامل C التحتيّ الذي تستعمله. ولتقليل التكرار في الشيفرة بين المعاملات الأربعة، لففنا القواسم المشتركة في ماكروٍ كبير للمعالج المسبق (macro) يأخذ رمز المعامل كوسيط.

بدا ذلك الماكرو مبالغاً فيه قبل [بضعة فصول][]، لكنّنا نُفيد منه اليوم. فهو يتيح لنا إضافة فحص الأنواع والتحويلات اللازمة في مكانٍ واحد.

^code binary-op (1 before, 2 after)

أجل، أدرك أنّ ذلك ماكرو وحش. ليس هو ما أعتبره عادةً ممارسةً جيدة في C، لكنّنا لنتّفق على ذلك. التغييرات شبيهة بما فعلناه للنفي الأحادي. أوّلاً، نفحص أنّ المعاملَين كلاهما عددان. فإن لم يكن أيٌّ منهما، نُبلّغ عن خطأ وقت التشغيل ونتشنّج مقبض مقعد الطرد.

وإن كان المعاملان على ما يرام، فنزعهما معاً ونفكّهما. ثمّ نطبّق المعامل المعطى، ونلفّ النتيجة، ونعيد دفعها على المكدّس. ولاحظ أنّنا لا نلفّ النتيجة باستعمال NUMBER_VAL() مباشرةً. بدلاً من ذلك، يُمرَّر اللافّ الواجب استعماله كـوسيط ماكرو. وبالنسبة إلى معاملاتنا الحسابية القائمة، النتيجة عدد، فمرّرنا ماكرو NUMBER_VAL.

^code op-arithmetic (1 before, 1 after)

قريباً سأريك لماذا جعلنا الماكرو اللافّ وسيطاً.

نوعان جديدان

عاد كلّ شيفرتنا القائمة في clox إلى العمل. وأخيراً حان وقت إضافة بعض الأنواع الجديدة. لدينا الآن حاسبةُ أرقامٍ تعمل، وأصبحت تُجري عدداً من فحوص أنواع وقت التشغيل عديمة الفائدة ومبالغاً فيها بحذر. يمكننا تمثيل أنواعٍ أخرى داخلياً، لكن لا سبيل أمام برنامج المستخدم كي ينشئ قيمةً من أحد تلك الأنواع.

ليس قبل الآن. سنبدأ بإضافة دعم المصرّف للقيم الحرفية الثلاث الجديدة: true و false و nil. وكلّها بسيطةٌ إلى حدٍّ كبير، لذلك سنفعلها الثلاث دفعةً واحدة.

وقد تطلّبت الأعداد الحرفية أن نتعامل مع حقيقة أنّه توجد مليارات القيم العددية الممكنة. وقد عالجنا ذلك بتخزين قيمة القيمة الحرفية في جدول ثوابت القطعة (constant table) وإصدار تعليمة شيفرة بايت تُحمّل ذلك الثابت ببساطة. ويمكننا أن نفعل الشيء نفسه بالأنواع الجديدة. فمثلاً، نخزّن true في جدول الثوابت ونستعمل OP_CONSTANT لإخراجه.

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

^code literal-ops (1 before, 1 after)

مُحلِّلنا المعجمي يتعامل أصلاً مع true و false و nil ككلماتٍ مفتاحية، لذا يمكننا القفز إلى المحلّل النحوي مباشرةً. ومع محلّلنا النحوي المعتمد على جداول (Pratt parser)، نحتاج فقط إلى إدخال دوالّ التحليل في الصفوف المرتبطة بأنواع الرموز المفتاحية تلك. وسنستعمل الدالة نفسها في الخانات الثلاث جميعها. وهنا:

^code table-false (1 before, 1 after)

وهنا:

^code table-true (1 before, 1 after)

وهنا:

^code table-nil (1 before, 1 after)

وحين يصادف المحلّل النحوي false أو nil أو true في موضع سابق، فإنّه ينادي دالة التحليل الجديدة هذه:

^code parse-literal

وبما أنّ parsePrecedence() استهلك رمز الكلمة المفتاحية أصلاً، فكلّ ما علينا فعله هو إخراج التعليمة الصحيحة. ونحن نحدّد ذلك بناءً على نوع الرمز الذي حلّلناه. أصبحت واجهتنا الأماميّة الآن قادرة على تصريف القيم المنطقيّة و nil إلى شيفرة بايت. ونزولاً في خطّ التنفيذ، نصل إلى المفسّر.

^code interpret-literals (5 before, 1 after)

هذا واضحٌ إلى حدٍّ كبير. فكلّ تعليمة تستدعي القيمة المناسبة وتدفعها على المكدّس. ولا ينبغي لنا أن ننسى أداة التفكيك (disassembler) أيضاً.

^code disassemble-literals (2 before, 1 after)

ومع ذلك في مكانه، يمكننا تشغيل هذا البرنامج المُزلزل للأرض:

true

إلّا أنّ المفسّر ينهار حين يحاول طباعة النتيجة. نحتاج إلى توسيع printValue() ليتعامل مع الأنواع الجديدة أيضاً:

^code print-value (1 before, 1 after)

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

النفي المنطقي و"الكاذبية"

أبسط معاملٍ منطقيّ هو صديقنا القديم المنفِه، النفي الأحادي.

print !true; // "false"

وهذه العملية الجديدة تحتاج إلى تعليمة جديدة.

^code not-op (1 before, 1 after)

ويمكننا إعادة استعمال دالة unary() التي كتبناها للنفي الأحادي لتصريف تعبير النفي. لا نحتاج سوى إدخالها في جدول التحليل.

^code table-not (1 before, 1 after)

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

^code compile-not (1 before, 3 after)

هذا كلّ ما في الواجهة الأماميّة. فلننتقل إلى الآلة الافتراضية ونُحيي هذه التعليم.

^code op-not (1 before, 1 after)

وكما في معاملنا الأحادي السابق، فإنّها تنزع المعامل الواحد، وتُجري العملية، وتدفع النتيجة. وكما فعلنا هناك، علينا أن نهتمّ بالتنميط الديناميكي. فالنفي المنطقي لـ true سهل، لكن لا شيء يمنع مبرمجاً فوضوياً من كتابة شيءٍ مثل:

print !nil;

وللنفي الأحادي، جعلنا خطأً نفي أيّ شيءٍ ليس عدداً. لكنّ Lox، مثل معظم لغات النصوص، أكثر تساهلاً مع ! ومع السياقات الأخرى التي يُتوقَّع فيها قيمةٌ منطقيّة. والقاعدة الخاصة بكيفية التعامل مع بقية الأنواع تُسمّى "الكاذبية" (falsiness)، وسننفّذها هنا:

^code is-falsey

وتتبع Lox Ruby في أنّ nil و false كاذبتان، وأنّ كلّ قيمةٍ أخرى تتصرّف كـtrue. ولدينا الآن تعليمة جديدة يمكننا توليدها، لذلك نحتاج أيضاً إلى أن نتمكّن من عدم توليدها في أداة التفكيك.

^code disassemble-not (2 before, 1 after)

معاملات التساوي والمقارنة

لم يكن ذلك سيئاً إلى هذا الحدّ. فلنُواصل الزخم ونُنهي معاملات التساوي والمقارنة أيضاً: == و != و < و > و <= و >=. فهذا يغطّي كلّ المعاملات التي تُعيد نتائج منطقيّة باستثناء المعاملين المنطقيَّين and و or. وبما أنّ هذين يحتاجان إلى تحقّق شرطي (short-circuit) -- أي ينفّذ القفزة إلى فرع تحكّم صغير -- فلسنا جاهزين لهما بعد.

وهذه هي التعليمات الجديدة لتلك المعاملات:

^code comparison-ops (1 before, 1 after)

مهلاً، ثلاثٌ فقط؟ وماذا عن != و <= و >=؟ يمكننا إنشاء تعليماتٍ لتلك أيضاً. وبصراحة، ستنفّذ الآلة الافتراضية بشكلٍ أسرع لو فعلنا ذلك، لذا ينبغي أن نفعله إن كان الهدف الأداء.

لكنّ هدفي الأساسي هو أن أعلّمك عن المصرّفات التي تُنتج شيفرة بايت. أريدك أن تبدأ باستيعاب الفكرة أنّ تعليمات شيفرة البايت لا تحتاج إلى أن تتبع عن قرب شيفرة المستخدم المصدرية. فللآلة الافتراضية حرّيةٌ تامّة في استعمال أيّ مجموعة تعليمات وتسلسل شيفرة تشاء، ما دام سلوكها المرئي للمستخدم صحيحاً.

والتعبير a != b له الدلالة نفسها التي للتعبير !(a == b)، فالمصرّف حرّ في تصريف الأوّل كأنّه الثاني. فبدلاً من تعليمة OP_NOT_EQUAL، يستطيع إخراج OP_EQUAL تليها OP_NOT. وعلى نحوٍ مشابه، a <= b هو نفسه !(a > b) و a >= b هو !(a < b). لذا، لا نحتاج إلّا إلى ثلاث تعليمات جديدة.

غير أنّه في المحلّل النحوي لدينا فعلاً ستة معاملاتٍ جديدة لإدخالها في جدول التحليل. ونستعمل دالة binary() نفسها السابقة. وهذا هو الصفّ الخاص بـ !=:

^code table-equal (1 before, 1 after)

والمعاملات الخمسة الباقية أبعد قليلاً في الجدول.

^code table-comparisons (1 before, 1 after)

وداخل binary() لدينا switch أصلاً لتوليد شيفرة البايت الصحيحة لكلّ نوع رمز. ونضيف حالاتٍ للمعاملات الستّة الجديدة.

^code comparison-operators (1 before, 1 after)

تُخرج معاملات == و < و > تعليمةً واحدة. أما البقية فتُخرج زوجاً من التعليمات، واحدةً لتقييم العملية العكسية، ثمّ OP_NOT لقلب النتيجة. ستّة معاملات مقابل ثلاث تعليمات!

وهذا يعني أنّ مهمّتنا في الآلة الافتراضية تصبح أبسط. فالتساوي هو العملية الأكثر عمومية.

^code interpret-equal (1 before, 1 after)

يمكنك تقييم == على أيّ زوجٍ من الأشياء، حتى الأشياء ذات الأنواع المختلفة. وهناك قدرٌ من التعقيد يجعل من المنطقي تحويل ذلك المنطق إلى دالةٍ منفصلة. وتُعيد تلك الدالة دائماً قيمة C من نوع bool، فيمكننالفّ النتيجة بأمان في BOOL_VAL. ولأنّ الدالة تتعلّق بالقيم فإنّها تعيش في وحدة "value".

^code values-equal-h (2 before, 1 after)

وهذا هو التنفيذ:

^code values-equal

أوّلاً، نفحص الأنواع. فإن كانت القيمتان من أنواع مختلفة، فهما بالتأكيد غير متساويتين. وإلّا فنحن نفكّ القيمةَين ونقارن بينهما مباشرةً.

ولكلّ نوع قيمة، لدينا حالةٌ منفصلة تتولّى مقارنة القيمة نفسها. وبالنظر إلى تشابه الحالات، قد تتساءل لماذا لا نستطيع ببساطة استعمال memcmp() على بنية القيمة ومكتفين بذلك. والمشكلة أنّه بسبب الحشو وحقول الاتّحاد ذات الأحجام المختلفة، تحتوي القيمة على بتاتٍ غير مستعملة. ولا تُعطي C أيّ ضمانٍ عمّا فيها، لذا من الممكن أن تختلف قيمتان متساويتان فعلاً في ذاكرةٍ غير مستعملة.

تمثيلَان في الذاكرة لقيمتَين متساويَتين تختلفان في البتّات غير المستخدمة.

(لن تُصدّق كم من الألم مررتُ به قبل أن أتعلّم هذه الحقيقة.)

على أيّ حال، ومع إضافتنا المزيد من الأنواع إلى clox، ستنمو هذه الدالة حالاتٍ جديدة. أمّا الآن فهذه الحالات الثلاث كافية. وبقية معاملات المقارنة أسهل لأنّها تعمل على الأعداد وحدها.

^code interpret-comparison (3 before, 1 after)

لقد وسّعنا ماكرو BINARY_OP أصلاً ليتعامل مع معاملاتٍ تُعيد أنواعاً غير عددية. والآن حان وقت استعمال ذلك. نمرّر BOOL_VAL لأنّ نوع قيمة النتيجة منطقيّ. وإلّا فلا فرق بينه وبين الجمع أو الطرح.

وكما هي العادة، خاتمة قطعنا هذا اليوم هي تفكيك التعليمات الجديدة.

^code disassemble-comparison (2 before, 1 after)

وبهذا صارت حاسبتنا العددية شيئاً أقرب إلى مُقيِّم تعبيرات عامّ. شغّل clox واكتب:

!(5 - 4 > 3 * 2 == !nil)

حسناً، أعترف بأنّ هذا ربما ليس التعبير الأكثر نفعاً، لكنّنا نُحرز تقدّماً. لدينا نوعٌ مدمجٌ واحدٌ ناقص له شكلُه الحِرَفي الخاص: النصوص (strings). وهذه أعقد بكثير لأنّ النصوص قد يختلف حجمها. وقد اتضح أنّ ذلك الاختلاف الصغير له آثارٌ بالغة جدّاً لدرجة أنّنا نُفرد للنصوص فصلاً خاصاً بها.

التحديات

  1. يمكننا تقليص معاملاتنا الثنائيّة أكثر مما فعلنا هنا. ما هي التعليمات الأخرى التي تستطيع إلغاؤها، وكيف سيتعامل المصرّف مع غيابها؟

  2. وبالمقابل، يمكننا تحسين سرعة آلتنا الافتراضية لشيفرة البايت بإضافة تعليماتٍ أكثر تخصّيصاً تقابل عملياتٍ أعلى مستوى. ما التعليمات التي ستصمّمها لتسريع نوع شيفرة المستخدم الذي أضفنا دعمه في هذا الفصل؟