منذ عدة سنوات ، كنت أعمل على مشروع شخصي لإنشاء (والبحث الفعلي) عن "محاكي وهمي" ، أي محاكي مكتوب بلغة JavaScript لجهاز كمبيوتر لم يكن موجودًا من قبل. كان من المفترض أن تكون هذه الآلة بمثابة تكريم لأجهزة الكمبيوتر ذات الثمانية والسادسة عشر بت في الثمانينيات والتسعينيات.
ومع ذلك ، فأنا أحب التعقيد: فقد استخدم هذا الجهاز أيضًا مجموعة جديدة من التعليمات. إنه مشابه للأطقم المستخدمة في تلك الحقبة ، لكن العمل معها أسهل قليلاً. ولد Retroputer . على مر السنين ، كان المحاكي يتوسع ويتحسن ، ولكن على الأرجح لن يتم "الانتهاء" أبدًا (بعد كل شيء ، هذا مشروع بحث شخصي).
عندما ظهر bbcmicrobot، أردت إنشاء شيء مشابه لجهاز Retroputer. اقتصرت مهارات تطوير JS الخاصة بي في الغالب على الواجهة الأمامية ، لذا ستكون هذه فرصة رائعة للحصول على بعض الخبرة الخلفية. توجد مشكلة واحدة فقط: يمكن للكمبيوتر Retroputer فقط فهم لغة التجميع الخاصة به. ليس لديها دعم BASIC حتى الآن.
لذلك توصلت إلى إنشاء مترجم BASIC بأسلوب الثمانينيات ، أي بلغة التجميع بالكامل ، كما تمت كتابتها في ذلك الوقت. قررت أن الأمر يستحق مشاركة عملي لأننا لا نضطر غالبًا إلى الغوص في مناطق بعيدة جدًا عن التجريدات المعتادة. أداتي اليومية (JavaScript) تجعل العديد من الجوانب تافهة وأحيانًا تبدو وكأنها سحرية. غالبًا ما يساعد فهم أدنى مستوى من العمليات في فهم هذه التجريدات.
اذا هيا بنا نبدأ.
خصائص الكمبيوتر الخلفي
- 16 ROM (KERNEL) c 1 (scratch space)
- 512 , 64
- 16- 6516, 512 4 64
- 4025, ,
- 1125
- 9800
عندما كنت أكتب المجمّع لـ Retroputer ، كان بإمكاني استخدام أداة Pegjs سهلة الاستخدام. لقد وفرت تركيبًا تجميعيًا أصليًا سريعًا ، ولكن للأسف لا يوجد شيء مثل هذا لـ Retroputer ASM. وهذا يعني أنه سيتعين علينا أن نسير في الطريق الصعب.
يتم إجراء التحليل نفسه على عدة مراحل. تقوم اللغة التي تستخدم مترجمًا بتوزيع الكود في شجرة بناء جملة مجردة (AST) (أو تمثيل آخر) ، ومن ثم يمكن استخدام هذه الشجرة لإنشاء كود أصلي نهائي. لذلك ، يجب أن يكون البرنامج صحيحًا نحويًا للترجمة الناجحة.
بعض المترجمين الفوريين لديهم هذا المفهوم أيضًا ، لأنه غالبًا ما يكون من المفيد إنشاء AST وسيط وتنفيذه بدلاً من شفرة المصدر.
ولكن بالنسبة للمترجم الشفهي الأساسي على جهاز ذي موارد محدودة ، سيكون التحليل الأكثر فاعلية على عدة مراحل ، وبعضها يحدث في وقت التشغيل. ومع ذلك ، هذا يعني أنه لا يمكن اكتشاف أخطاء في بناء الجملة حتى تقوم بتشغيل البرنامج والانتقال إلى منطقة التعليمات البرمجية التي بها الخطأ.
يتكون التحليل الأساسي للكمبيوتر الخلفي من ثلاث مراحل:
- تحويل الأوتار
- الترميز
- فحص صيغة وقت التشغيل
تحدث المرحلتان الأوليان عند دخول المستخدم إلى البرنامج (أو تنزيله). هذا الأخير يحدث أثناء تنفيذ البرنامج. في الأساس ، يقوم الأولان بإنشاء جسم الطائرة ، لكن لا تضمن أنه سيقلع. المحطة الأخيرة هي طيار تجريبي - نأمل أن يقلع ، لكننا لسنا متأكدين من ذلك حتى يحاول.
ملاحظة: الكود المصدري Retroputer BASIC (قيد التطوير) متاح على GitHub .
تحويل الأوتار
هذا هو أبسط جزء من العملية برمتها. يتم تحويل السلسلة التي أدخلها المستخدم إلى أحرف كبيرة لتبسيط (وتسريع) العمليات اللاحقة. BASIC ليست حساسة لحالة الأحرف ، لذلك يمكننا الاستفادة من ذلك.
<pre>print 2+2
' becomes:
PRINT 2+2</pre>
من السهل جدًا القيام بذلك في JavaScript ، أليس كذلك؟
theLine = theLine.toUpperCase();
لكن في لغة التجميع ، تكون هذه العملية أكثر تفصيلاً. نحتاج إلى قراءة الحرف وتحويله إلى أحرف كبيرة ثم تخزينه في مكان ما.
ld y, 0 # y is our index
_loop: ld al, [d, x, y] # [d,x] is pointer to string
cmp al, 97 # is al (char) in range?
brs n _continue # Not a lowercase char; continue
cmp al, 123 # high portion
brs !n _continue # not a lowercase char; continue
and al, 0b1101_1111 # uppercase!
st [d, x, y], al # store back (we modify the original)
_continue: inc y # move our index along
cmp al, 0 # check for NULL
brs !z _loop # No? Go back for more.
لا تتطابق الكود أعلاه تمامًا مع دلالات إصدار JavaScript من التعليمات البرمجية. يتمثل أحد الاختلافات المهمة في أننا نستخدم اليوم Unicode للعمل مع النص ، لذا فإن تحويل الإدخال من الأحرف الصغيرة إلى الكبيرة يمكن أن يكون أكثر صعوبة وأحيانًا مستحيل (اعتمادًا على اللغة). يعيش Retroputer في عالم ASCII (بشكل أكثر دقة ، في عالم تباين ASCII الخاص به المسمى RetSCII) ، أي أن جميع الأحرف المدعومة مشفرة في ثمانية بتات. بالنسبة للعديد من اللغات ، هذا غير كافٍ تمامًا ، لكنه يلبي حقائق ذلك الوقت.
هذا يعني أيضًا أنه يمكننا استخدام ميزة ASCII اللطيفة للتحويل من الأحرف الصغيرة إلى الأحرف الكبيرة. يتم تمثيل الأحرف الكبيرة "A" في رمز ASCII
65، ويتم تمثيل الحرف الصغير "a" بالرمز97... إذا كنت معتادًا على قوى العدد اثنين ، فلا بد أنك لاحظت هذا الاختلاف.
لذلك اتضح أن الأحرف الصغيرة يتم تحديدها برقم يزيد بمقدار 32 بالضبط عن عدد الأحرف الصغيرة. إذا علمنا أن القيمة تقع في النطاق الصحيح ، فسنحتاج فقط إلى طرح 32!
يعمل هذا النهج ، لكن يمكننا فقط تعديل البتات قليلاً. في حالة Retroputer ، لن يكون هذا أسرع من الطرح ، ومع ذلك ، في حالة عدم وجود الطرح ، لن نضطر إلى الانتباه إلى علامة الحمل / الاستعارة أثناء العمليات الحسابية. اتضح أنه يمكننا استخدام أحادي المعامل
andلإيقاف تشغيل البت بقيمة 32.
and al, 0b1101_1111 # turn off bit in 32-place
# versus
clr c # clear carry
sub al, 32 # subtract 32
ولكن هناك دقة واحدة: لا يمكن تحويل كل شيء إلى أحرف كبيرة. على سبيل المثال ، إذا أضاف المستخدم سلسلة حرفية ، فسنحتاج إلى أن نكون أكثر حرصًا ، لأننا لا نريد أن يصيح Retroputer BASIC باستمرار على المستخدم باستخدام الأحرف الكبيرة. (في حين أن العديد من أجهزة الكمبيوتر في ذلك العصر لم يكن بها أحرف صغيرة ، فإن Retroputer لم يكن بها أحرف صغيرة).
على سبيل المثال:
print "Hello, World!"
' should become:
PRINT "Hello, World!"
' and not
PRINT "HELLO, WORLD!"
هذا يعني أننا بحاجة إلى تتبع ما إذا كنا في منتصف سلسلة حرفية. في BASIC ، هناك تسمية واحدة فقط لهذا: علامات الاقتباس المزدوجة. إذا كان الحرف عبارة عن علامة اقتباس مزدوجة ، فيمكننا تعيين العلم ، واعتمادًا على قيمة العلم ، يمكنك التحويل إلى أحرف كبيرة أو تركها كما هي.
اتضح أن JavaScript لا يحتوي على وظائف مضمنة لذلك ، ولكن يمكننا إنشائه بأنفسنا:
const len = theLine.length;
let insideString = false;
for (let i = 0; i < len; i++) {
const ch = theLine[i];
if (ch === `"`) insideString = !insideString;
if (!insideString) {
const newCh = ch.toUpperCase();
if (ch !== newCh) theLine[i] = newCh;
}
}
الآن يتطابق المنطق في JS بشكل وثيق مع إصدار المجمع ، على الرغم من أننا نستخدم مرة أخرى القليل من دعم Unicode في JS.
يبدو إصدار المجمع كما يلي:
ld y, 0 # y is our index
ld bl, 0 # === insideString (false)
_loop: ld al, [d, x, y] # [d,x] is pointer to string
cmp al, 34 # is al a double quote?
brs !z check_char # no? should we uppercase it?
xor bl, 0xFF # yes? toggle insideString
_check_char:
cmp bl, 0xFF # inside a string?
brs z _continue # yes? don't modify it
cmp al, 97 # is al (char) in range? "a"
brs n _continue # Not a lowercase char; continue
cmp al, 123 # high portion "z"
brs !n _continue # not a lowercase char; continue
and al, 0b1101_1111 # uppercase!
st [d, x, y], al # store back (we modify the original)
_continue: inc y # move our index along
cmp al, 0 # check for NULL
brs !z _loop # No? Go back for more.
لقد قمنا حتى الآن فقط بتحويل نص الإدخال إلى أحرف كبيرة ، ولكن يمكنك استخدام تعريف سلسلة حرفية لاحتمال آخر. يمكننا إجراء فحص بناء جملة آخر!
إذا اكتشفنا في نهاية العملية ما
inStringلا يزال صحيحًا ( bl = 0xFF) ، فيمكننا إرجاع خطأ ، لأن هذا يعني وجود سلسلة حرفية غير مكتملة في مكان ما في السلسلة.
ملاحظة: اتضح أن العديد من إصدارات BASIC متساهلة نوعًا ما في عدم وجود علامات اقتباس نهائية على السلاسل. لقد اكتشفت ذلك أثناء إنشاء مترجمي الخاص. على الرغم من أنه لا يبدو صحيحًا بالنسبة لي ، لذلك لا يسمح Retroputer BASIC بذلك.
الترميز
الخطوة التالية في التحليل هي تحويل سلسلة الإدخال إلى شيء أكثر كفاءة ليتم تنفيذه من قِبل مترجم Retroputer BASIC. نحاول الاقتراب قدر الإمكان من مفهوم شجرة التركيب المجردة ، لكن النتيجة لن تكون شجرة. لكنه سيكون شيئًا يمكننا التعامل معه بسرعة في وقت التشغيل.
كانت السمة المشتركة للحواسيب الصغيرة المبكرة هي الذاكرة المحدودة للغاية. يمتلك جهاز Retroputer ذاكرة أكبر من معظم الأجهزة القياسية في ذلك الوقت ، ولكنه لا يزال أقل بكثير من أجهزة الكمبيوتر الحديثة. لذلك ، يمكن لبرامج BASIC الطويلة أن تستهلك الكثير من الذاكرة بسهولة إذا تم حفظها أثناء إدخال المستخدم.
لتوفير مساحة ، يتم ترميز الكلمات الرئيسية أثناء دخول البرنامج في الذاكرة... تقوم هذه العملية بتحويل الكلمات الأساسية إلى رموز أحادية البايت. دائمًا ما يكون طول الكلمات الرئيسية 2 بايت على الأقل ، لذلك سيكون هناك المزيد من التوفير أثناء الكتابة. هذا يعني أيضًا أنه يمكننا استخدام جدول البحث في وقت التشغيل لاستدعاء إجراءات لغة التجميع المناسبة.
ومع ذلك ، ذهب Retroputer BASIC إلى أبعد قليلاً من معظم إصدارات BASIC في ذلك الوقت. علاوة على ذلك ، فإنه يحول الأرقام إلى نموذج ثنائي ، ويضع علامات على السلاسل ، ويحسب المراجع المتغيرة ، وأكثر من ذلك بكثير. بصراحة ، هذا يهدر مساحة أكبر ، لكن فوائد السرعة (وسهولة التنفيذ) تفوق ذلك.
لذلك يتم استخدام الخطوات التالية هنا:
-
, , . , , , , . -
, , , . ,PRINT "Hello, World"«Hello, World» , .
, . -
, , , . JavaScript, !
( ). ,PRINT! -
Retroputer BASIC ( ). . , , .
Retroputer BASIC . , . , Retroputer BASIC.
في المنشور لن أخوض في التفاصيل حول تنفيذ هذه المرحلة في لغة التجميع ، سأتركها للمستقبل. لكن لا تقلق ، فهناك الكثير من الأكواد .
التحقق من بناء الجملة في وقت التشغيل
أخيرًا وليس آخرًا ، يكون فحص بنية وقت التشغيل هو. بعد تحويل الكود إلى نموذج رمزي ، يكون من السهل تنفيذه.
أولاً ، في وقت التشغيل ، يتحقق BASIC لمعرفة ما إذا كان يعالج حاليًا رمزًا. تم تمكين البت الأكثر أهمية لجميع الرموز المميزة (أي أن قيمتها 128 وما فوق). إذا تم العثور على رمز مميز ، فيمكننا تحديد الإجراء الفرعي الذي يجب الاتصال به من خلال إيجاده في جدول المتجه . كما أنه يجعل من السهل عرض أخطاء بناء الجملة - فبعض الكلمات الرئيسية لا يكون لها معنى كمعاملين ، لذلك يشير جدول المتجه ببساطة إلى الإجراء الذي يولد خطأ في بناء الجملة.
بعد استدعاء معالج الرمز المميز للمشغل ، يأخذ المعالج كل أعمال التحليل الإضافية. لالرموز ويمكن استخدامه الانتقال بينهما
gettok، gettok-raw،peektok الخ إذا كان الرمز المميز شيئًا لا يتوقعه الإجراء ، فإن الإجراء يقوم ببساطة بإرجاع رمز خطأ. يتم في هذه المرحلة اكتشاف أخطاء بناء الجملة وأخطاء مطابقة النوع.
إذا كان يجب على المشغل تقييم التعبير ، فسيتم إجراء مرحلة أخرى من التحليل. عند تحليل تعبير ، يتم استخدام جدول بحث متجه آخر ، أي يمكننا اعتراض الكلمات الأساسية التي لا تتعلق بتعبير رياضي وإرجاع الأخطاء المقابلة. على سبيل المثال ، إذا حاولت الدخول
PRINT 2+CLS، ثم نحصل على خطأ في بناء الجملة CLS( CLS- هذا هو اختصار "مسح الشاشة").
ملاحظة: من هذا الجدول ، يمكننا أيضًا تحديد أولوية العوامل الحسابية وعدد المعلمات المطلوبة. هذا مهم للتقييم الفعلي للتعبير ، ولكن يمكن استخدامه أيضًا للقبض على الحالات التي لم يقدم فيها المستخدم الوسائط الكافية.
نظرًا لأن الرمز المميز يرسم مباشرة إلى إدخال جدول بحث المتجه ، يمكن تنفيذ البرنامج بسرعة إلى حد ما وبأقل جهد ممكن. إن مهمة تحليل كل نوع من المشغلات موكلة إلى المعالج نفسه ، وبشكل عام لا يسبب هذا أي مشاكل معينة. ربما يكون أصعب تحليل هو
PRINTوINPUT، ولكن في كل خطوة يتم أخذ رمز مميز واحد في كل مرة.
نظرًا لأن العديد من عمليات التحقق لا يتم إجراؤها حتى وقت التشغيل ، فهذا يعني أنه قد تكون لدينا نتائج جزئية قبل حدوث الخطأ. على سبيل المثال:
PRINT "Hello";CLS
Hello
?Syntax Error
بالإضافة إلى ذلك ، هذا يعني أنه إذا ترك البرنامج الشاشة في حالة لا يمكننا فيها رؤية النص ، فقد نواجه مشاكل في التخلص من الأخطاء. إذا تم عرض خطأ نحوي ولكننا لا نراه ، فماذا نفعل؟
هذه الطريقة في التحقق من النحو لها بالتأكيد عيوبها ، لكنها تسمح لمترجم بسيط إلى حد ما.
تشفير إدخال المستخدم
كما ذكرنا سابقًا ، كان من الشائع بالنسبة للإصدارات الأساسية من تلك الحقبة استخدام تكتيكات الترميز. لتوفير مساحة وزيادة سرعة التنفيذ ، تم "ضغط" الكلمات الرئيسية في الرموز المميزة أحادية البايت (أو ثنائية البايت ، إذا كانت هناك حاجة إلى مزيد من الكلمات الرئيسية).
دعنا نتظاهر بأن لدينا سطرًا بسيطًا من التعليمات البرمجية يمسح الشاشة ويطبع تحية قياسية:
CLS: PRINT "Hello, World"
في حين أن هذا في حد ذاته لا يستهلك الكثير من الذاكرة ، إذا قمت بكتابة برنامج طويل ، فإن العديد من الكلمات في هذا البرنامج ستكون كلمات رئيسية. إذا قمت بضغطها (تحويلها إلى رمز مميز) ، فيمكنك حفظ جزء ثابت من المساحة. على سبيل المثال ، بعد ترميز السطر الموضح أعلاه ، سيكون هناك شيء مثل هذا في الذاكرة:
8A E3 B5 FC Hello, World 00 00
لا ينبغي أن يكون من الصعب للغاية إعادة هذا إلى البنية الأصلية:
| بايت | الكلمة الرئيسية | ملاحظات |
|---|---|---|
| 8 أ
|
CLS
|
|
| ه 3 | ":" | نهاية البناء |
| 32 | "" | نحن نخزن مساحة واحدة على الأكثر |
| ب 5 | طباعة |
|
| FB | سطر في الكود | بعد ذلك تأتي السلسلة الحرفية |
| مرحبًا بالعالم 00 | تم إنهاء السلاسل فارغة | |
| 00 | نهاية سطر الكود | يتم أيضًا إنهاء سطور التعليمات البرمجية فارغة |
قد تعتقد أن هذا يتطلب الكثير من العمل ، لكن توفير المساحة يمكن أن يكون كبيرًا. هذا ليس ملحوظًا بشكل خاص في المثال ، ولكن حتى منه يمكنك تخيل مدى السرعة التي يمكن أن تتراكم بها المساحة المحفوظة. في هذه الحالة ، تكون النتيجة المضغوطة 19 بايت. يتكون النص المصدر من 26 بايت (بما في ذلك البايت الفارغ المنتهي).
يصبح توفير المساحة أمرًا مهمًا عندما يتعلق الأمر بالمترجم الشفهي الأساسي الذي يعمل على جهاز به ذاكرة وصول عشوائي صغيرة جدًا لبرامج المستخدم. لذلك ، يعد هذا الضغط مهمًا جدًا وجذابًا ، حتى لو لم يكن له فوائد إضافية.
إذن كيف يمكننا ترميز شيء مثل هذا؟ في البداية ، يبدو أن JavaScript تافه جدًا للقيام بذلك. باستخدام مجموعة من السلاسل ، يمكنك استبدال كل كلمة رئيسية بسرعة بالرمز المميز المقابل. صحيح؟
يبدو أن هذه وظيفة
String#replace؟ باستخدام نهج ساذج ، يمكنك تجربة شيء مثل هذا:
const tokens = ["CLS", "PRINT", ":" ];
function tokenize (inStr) {
const newStr = inStr;
tokens.forEach((token, idx) => newStr = newStr.replace(
new RegExp(token, "g"), String.fromCharCode(128+idx)
);
return newStr;
}
لسوء الحظ ، سرعان ما ندرك أنه لا يمكننا استبدال الأحرف الحرفية بالسلسلة بالرموز المميزة. هذا يعني أنك تحتاج إلى إجراء معالجة حرفًا بحرف ، مع مراعاة السياق ، حتى لا تضغط على العناصر التي ليست كلمات رئيسية.
هذه الخوارزمية الجديدة أقرب إلى رمز لغة التجميع في Retroputer ، لكن JS لا تزال تجعل الأمور أسهل كثيرًا. ها هي بداية كود JS (سنقوم بتوسيعه تدريجيًا في هذا المنشور):
const tokens = ["CLS", "PRINT", ":" ];
function tokenize(inStr) {
let out = []; // return value is "crunched" array
let idx = 0; // index into inStr
let ch = ""; // current character
while (idx < inStr.length) {
ch = inStr.charCodeAt(idx); // get the current character code
// our logic is going to go here
out.push(ch); // for now push the character
idx++; // and keep going (next char)
}
out.push(0); // we end with NUL
return out;
}
نبدأ بقائمة مبسطة جدًا من الرموز المميزة ، لأن لا أحد يريد رؤية الجدول بأكمله بهذا التنسيق ( إنه طويل ، ويقوم Retroputer بالفعل بإنشاء جداول رمزية من ملف JS! ) ، ولكن لأغراضنا سيكون هذا كافيًا. المبدأ هنا هو أنه عندما نرى رمزًا ، نكتب فهرسه إلى مصفوفة الإخراج.
في هذه المرحلة ، لدينا وظيفة تقوم ، في الوقت الحالي ، ببساطة بتحويل السلسلة إلى رموز الأحرف المكافئة لها. في حين أنه ليس مفيدًا بشكل خاص ، إلا أنه يمكن أن يكون بمثابة إطار عمل مناسب.
إصدار لغة التجميع مشابه تمامًا للإصدار الموضح أعلاه.
do {
call _get-source-index # get next character index (in y)
dl := <BP+source>,y # read next char from our source string (ch)
call _adv-source-index # advance our source index
call _get-target-index # get position in target ("out" in JS)
<BP+target>,y := dl # store to target (out[y] = ch)
call _adv-target-index # move it along
cmp dl, 0 # was char 0? if so, break
} while !z
لم أقم بتضمين المثال أعلاه
_get-source-indexأو وظائف أخرى ، لأن مهامهم واضحة من الاسم: فهم ببساطة يحصلون على مؤشرات المصدر والهدف ، ويعينونها ، ويتنقلون خلالها أيضًا. من الجدير بالذكر أنه على عكس JS ، لا توجد مصفوفات مخصصة ديناميكيًا في لغة التجميع ، لذلك تخصص هذه الخوارزمية مخزنًا مؤقتًا بحجم كافٍ. عندما ننتقل إلى أسفل خط الإدخال ، نحتاج إلى معرفة مكان كتابة الرمز المميز التالي في المخزن المؤقت الهدف ، وما الذي يفعله مؤشر الهدف أعلاه. تقوم كل وظيفة من الوظائف المذكورة أعلاه بإرجاع فهرس بتنسيق Y. على سبيل المثال ، _adv-target-indexتزيد الوظيفة من فهرس الهدف بمقدار واحد (مكافئ y++).
كن حذرا مع الحرفية
يجب أن نكون حذرين: يمكن أن تكون سلسلة الأحرف الحرفية مربكة للمميزات - لا يمكننا ببساطة استبدال كل سلسلة أحرف تتطابق مع كلمة رئيسية بقيمة رمزية. إذا رأينا سلسلة حرفية بها "CLS" ، فلا ينبغي أن نسعى إلى ترميزها. لا ينبغي أن يكون قابلاً للتنفيذ ، وإذا أخرجناه ... فبدلاً من ذلك نخرج البايت المقابل للرمز المميز. على الأرجح ، ليس هذا ما قصده المطور.
من ناحية أخرى ، لا ينبغي أن تواجه القيم الحرفية الرقمية هذه المشكلة ، لأن BASIC لا تحتوي على كلمات رئيسية رقمية. ومع ذلك ، لا فائدة من البحث عن كلمة رئيسية عند مواجهة رقم - فلماذا تضيع وقتك؟
ترميز حرفية السلسلة
لذا ، دعنا نتحقق أولاً مما إذا كان السطر قد بدأ - في JS ليس من الصعب القيام بذلك:
if (ch === 34) {
out.push (0xFB); // string-in-code token
idx++;
ch = inStr.charCodeAt(idx); // get next character after the quote
while (ch !== 34 && idx < inStr.length) {
out.push(ch);
idx++;
ch = inStr.charCodeAt(idx);
};
idx++; // go past the quote
out.push(0); // end of string
continue;
}
يتم تمثيل علامة الاقتباس المزدوجة برمز الحرف 34. يمكن أن تتعرف لغات البرمجة الأخرى على العديد من أنماط علامات الاقتباس (مثل JS أو C) ، ولكن مع BASIC يكون الأمر بسيطًا: إما علامات اقتباس مزدوجة أو لا شيء.
عندما نرى أن سلسلة ما تبدأ ، يمكننا ببساطة أن نأخذ الأحرف المتبقية ونضيفها إلى مصفوفة الإخراج حتى نواجه اقتباسًا آخر.
بعد قراءة السلسلة بأكملها ، يجب إضافة صفر بايت لأن Retroputer BASIC يستخدم سلاسل من النمط C. إذا أردنا استخدام سلاسل بنمط باسكال ، فيمكننا تخزين عداد وإدخال طول السلسلة الحرفية. على أي حال ، هذا لا يمثل أي مشكلة معينة. السبب الوحيد الذي جعلني أستخدم سلاسل منتهية بقيمة خالية هو أنه من السهل جدًا التعامل معها في لغة التجميع - نحتاج فقط إلى المقارنة مع بايت فارغ ، وليس تخزين عداد.
لذلك لم تكن JavaScript صعبة بشكل خاص. أعتقد أن معظمكم سيقرر استخدام شيء أكثر تجريدًا ، مثل التعبير العادي. أعتقد أن هذا الرمز سيجعل نوايانا أكثر وضوحًا.
if (ch === 34) {
out.push (0xFB); // string-in-code token
const stringLiteral = inStr.substr(idx+1).match(/^[^"]*/)[0];
idx += stringLiteral.length+1;
out.push(...Array.from(stringLiteral, ch => ch.charCodeAt(0)));
idx++; // go past the quote
out.push(0); // end of string
continue;
}
الكود أعلاه هو نفسه تقريبًا ، ولكن بدلاً من التحقق من صحة حرفًا بحرف ، نسمح لـ JS بالتنفيذ
match. نعلم أننا سنحصل على تطابق (نحن في سلسلة) ، لذلك لا نحتاج حتى للتحقق مما إذا كانت المباراة ناجحة. نقوم بعد ذلك بزيادة الفهرس بطول السلسلة ونسخ الأحرف في مصفوفة الإخراج. في رأيي ، هذا الرمز أسهل في الفهم (ومع ذلك ، فهو يعني فهمًا للتعبيرات العادية ، بالإضافة إلى خصائص بناء جملة ES6 ، على سبيل المثال ، …ووظائف الأسهم).
بالطبع ، في لغة التجميع ، علينا القيام يدويًا بكل الأعمال التي تقوم بها JS. النتيجة مشابهة جدًا لمحاولتنا الأولى لكتابة كود JS:
cmp dl, constants.QUOTE # is dl a quote?
brs !Z not-a-string # nope; not a string
call _get-target-index # get next position in crunch target
dl := brodata.TOK_CODE_STRING # "String" token
<bp+target>,y := dl # Store it into the crunch target
call _adv-target-index
still-a-string:
call _get-source-index
dl := <bp+source>,y # get string character
call _adv-source-index
cmp dl, constants.QUOTE # if it's a quote, we can zero it
if Z {
dl := 0
}
call _get-target-index
<bp+target>,y := dl # write to target
call _adv-target-index
cmp dl, 0 # are we done?
brs !Z still-a-string # no, keep going
continue # next!
not-a-string:
يجدر إضافة ملاحظة حول محلل تجميع Retroputer - فهو يحتوي على دعم أساسي للإنشاءات عالية المستوى مثل الكتل والحلقات. لذلك ،
if Z {…}سيتم تنفيذ المحتوى داخل الكتلة عند تعيين علامة الصفر ، continueويمكن استخدامها للتفرع مرة أخرى إلى بداية الكتلة ( breakتُستخدم أيضًا للخروج من الكتلة). يترجم المُجمِّع هذا إلى العديد من التعليمات المتفرعة والمقارنة ، بحيث لا ترى وحدة المعالجة المركزية الأجزاء عالية المستوى من الكود ، ولكنها تجعل كتابة الكود أسهل قليلاً.
أرقام رمزية
أيضًا ، ليس من المنطقي محاولة البحث في قائمة الكلمات الرئيسية عن الأرقام ، حتى نتمكن من تخطيها. معظم إصدارات BASIC تفعل شيئًا مشابهًا لمعالجة السلسلة الموضحة أعلاه - إذا كانت قراءة الحرف عبارة عن رقم ، فسيتم ربطه بالمخرجات ، وبعد ذلك سيتولى الضاغط المسؤولية.
يخطو Retroputer BASIC (وبعض الإصدارات الأخرى من BASIC ، مثل Atari BASIC) خطوة أخرى إلى الأمام: فهو يحول رقمًا إلى التنسيق الثنائي المناسب. من السهل جدًا القيام بذلك - إذا قابلنا رقمًا ، فإننا نضرب المركب في 10 ونضيف الرقم ، ونتابع ذلك طالما استمرت الأرقام في الظهور. (مع ذلك ، تجدر الإشارة إلى أنه بينما يعمل Retroputer BASIC مع القيم الصحيحة فقط ، فإن إضافة أرقام الفاصلة العائمة موجودة بالفعل في قائمة المهام الخاصة بي.)
(يجب أن أقول أن Retroputer BASIC حاليًا لا تفعل شيئًا عندما يحدث تجاوز عدد صحيح ، سواء كان موقعة أو غير موقعة. عندما أقوم بإضافة أرقام الفاصلة العائمة ، سيتحول Retroputer أيضًا إلى النقطة العائمة.)
Retroputer BASIC يذهب خطوة أخرى إلى الأمام. : يتعرف أيضًا على الأرقام السداسية العشرية ويحولها إلى مكافئها الثنائي. يتم استخدامه كتسمية لهذه الأرقام
0x(كما هو الحال في JS) ، واللغة نفسها لها منطق إضافي لضمان إرجاع خطأ إذا تم تحديد عدة أحرف x.
في لغة التجميع ، فإن التحقق مما إذا كان الحرف هو رقم ليس بهذه الصعوبة ، ولكنه يتطلب مقارنات: تحتاج إلى التأكد من أن رمز الحرف يقع بين
0x30و0x39... (هذه هي رموز الأحرف "0" و "9".)
بعد استلام رقم محرف ، يمكننا استخدام خاصية ملائمة أخرى لمجموعة الأحرف.
0x30- هذا هو رمز الحرف "0" ، 0x31- الرمز "1" ، وهكذا. يمكننا أن نطرح إذا أردنا 0x30، ولكن هناك طريقة أبسط: فقط تجاهل أهم أربع بتات:
and dl, 0b0000_1111
للأسف، هذا سوف لا تعمل إذا نحن بحاجة إلى أرقام عرافة التعامل معها. في هذه الحالة ، يمكنك طرح ثم مقارنتها بـ 10 ، ثم تقليلها بمقدار 7 مرة أخرى إذا كان الرمز أكبر من 10 (مع الأخذ في الاعتبار أن الأرقام السداسية العشرية هي "A" - "Z" بالأحرف الكبيرة).
في JS ، يمكنك استخدام التعبيرات العادية مرة أخرى ، وبعد ذلك عندما نحصل على تطابق للرقم ، يمكنك الاتصال
Number()، مما يعطينا ميزة أخرى: يتم تحويل الأرقام السداسية العشرية أيضًا بشكل تافه ، لأنها Number()تفعل ذلك تلقائيًا إذا كان الرقم يبدأ بـ 0x.
كيف تبدو في JavaScript؟
if (ch >= 48 && ch <= 57) {
out.push (0xFD); // number token
const numberLiteralStr = inStr.substr(idx).match(/^[\d|A-F|a-f|x|X]+/)[0];
idx += numberLiteralStr.length;
const numberLiteral = Number(numberLiteralStr);
const bytes = new Uint8Array(new Uint16Array([numberLiteral]).buffer);
out.push(...bytes)
continue;
}
يسمح لك التعبير العادي باستخدام أي مجموعة من الأرقام أو الأرقام السداسية العشرية (بالإضافة
xإلى التبديل إلى الوضع السداسي عشري). التعبير ليس مثاليًا ، على سبيل المثال ، فهو يسمح لك باستخدام العديد x، لكن هذا يكفي في الوقت الحالي.
في الكود أعلاه ، قد يكون تحويل الأرقام إلى بايت أمرًا مشكوكًا فيه.
Number()قام بكل العمل الشاق لتحويل سلسلة من الأرقام إلى رقم يمكن لـ JavaScript العمل معه ، لكننا الآن نحتاج إلى تمثيلها على أنها سلسلة من البايتات. يمكنك استخدام الرياضيات للقيام بذلك:
const hiByte = (numberLiteral & 0xFF00) >> 8;
const loByte = (numberLiteral & 0x00FF);
... ومن السهل القيام به لعدد صحيح. ولكن بفضل استخدام مصفوفات من نوع JS، يمكنك تجنب كل هذه الحسابات، ولكن في نفس الوقت الاستعداد للتعامل مع أرقام النقطة العائمة في المستقبل (ونحن سوف يحل محل ببساطة
Uint16Arrayمع Float64Array).
رمز لغة التجميع لهذه المهمة أطول قليلاً ، لكنه يفعل أكثر قليلاً. يستخدم Retroputer تحسينًا آخر: إذا كان الرقم صغيرًا ، فسيتم حفظه بتنسيق أصغر. هذا يعني أنه يمكن تخزين 0-255 في بايت واحد ، بينما تتطلب الأرقام الأطول اثنين.
ابحث عن الكلمات الرئيسية
لقد أنجزنا قدرًا كبيرًا من العمل ، لكننا حتى الآن لم نضغط على كلمة رئيسية واحدة. بعد أن تعاملنا مع الأرقام والسلسلة الحرفية ، يمكننا التأكد من أن كل شيء آخر هو إما كلمة رئيسية أو اسم متغير. (أو مسافة ، لكن من السهل التحقق منها).
في BASIC ، لا تبدأ الكلمات الرئيسية دائمًا بحرف أبجدي ؛ تعتبر عوامل التشغيل والمحددات أيضًا رموزًا مميزة. لكن المتغيرات تبدأ أيضًا بحرف أبجدي. لذلك لا يمكننا تحديد ما إذا كنا سنضغط كلمة رئيسية أو متغيرًا على الفور. هذا يناسبنا - إذا لم نجد تطابقًا في قائمة الرموز المميزة ، سنفترض أن هذا متغير.
إذن كيف نتحقق من أن الكلمة الرئيسية المحتملة هي بالفعل كلمة رئيسية؟ إذا كنا نكتب بلغة JavaScript ، فيمكننا استخدام الامتداد
Array#findIndex.
const tokenMatch = tokens.findIndex(token =>
inStr.substr(idx).startsWith(token));
if (tokenMatch > -1) {
out.push(tokenMatch + 128);
idx += tokens[tokenMatch].length;
continue;
}
Array#findIndexتتخطى
الطريقة المصفوفة بشكل متكرر tokensويمكننا التحقق مما إذا كانت تبدأ inStr(من خلال الطريقة الحالية idx) بالرمز الذي نتحقق منه. بالعمل مع قائمتنا المبسطة من الرموز المميزة ، سنفعل شيئًا مثل (افترض ذلك inStr.substr(idx)===”PRINT”):
| رمز | .startsWith (رمز)؟ | فهرس |
|---|---|---|
| CLS | خاطئة | 0 |
| طباعة | صحيح | 1 |
ملاحظة: كما هو الحال
indexOfمع JS ، إذا لم يتم العثور على شيء ، نحصل عليه -1.
إذا تم العثور على تطابق ، يمكنك تخزين فهرسها في المصفوفة التي تم إرجاعها. ولكن كيف يمكننا لاحقًا التمييز بين رمز مميز ورمز؟ سهل: قم بتشغيل البت الأكثر أهمية ، والذي يمكن إجراؤه عن طريق إضافة 128 إلى قيمة الرمز المميز.
(ملاحظة: إذا احتجنا إلى عدد أكبر من الرموز المميزة أكثر من 128 ، فسيتعين علينا استخدام اثنين بايت في بعض الرموز. وهذا يعقد الأمور قليلاً ، ولكن ليس كثيرًا. تقوم بعض إصدارات BASIC بهذا ، على سبيل المثال ، العديد من Microsoft BASICs.)
لقد انتهينا من العمل في JavaScript ، ولكن كيف نفعل ذلك بلغة التجميع؟
اتضح أنه متماثل تقريبًا ، لكن الرمز سيكون أطول من ذلك بكثير.
search-keywords:
bl := [d, x] # get first character of current token
cmp bl, constants.NUL # if it's NUL, we've exhausted the list
brs Z exit-keyword-search # so we're clearly not a keyword
clr Z # compare strings, but with partial equality
call [vectors.STRCMP] # so that our source doesn't need NUL between
# tokens; c will now be how many chars got
# compared
if !Z {
do { # no match; advance past our token in the list
inc x # character by character
bl := [d, x] # tokens are terminated with NULs
cmp bl, constants.NUL # so if we see it, we can stop
} while !z
inc x # go past the token #
inc x # in the lookup table
brs search-keywords # and keep looking for a match
}
clr c # found the match!
add x, c # c is the length of the match
inc x # past the NUL
bl := [d, x] # bl is now the token #
call _get-target-index
<bp+target>,y := bl # write the token #
call _adv-target-index
call _get-source-index # advance past the token in the source
clr c
add y, c # by adding the character count
dec y # back off by one (we'll advance again later)
call _set-source-index
continue
حسنًا ، لا يبدو الأمر بهذا السوء. الخوارزمية هي نفسها تقريبًا ، فقط جدول رمز لغة التجميع منظم بشكل مختلف قليلاً. يبدو شيء من هذا القبيل:
CLS 00 80
PRINT 00 81
: 00 82
يتم إنهاء كل كلمة رئيسية ببايت فارغ متبوعًا برقم رمزي.
ومع ذلك ، فإننا نفتقد شيئًا مهمًا هنا - كيف تتم مقارنة السلسلة هذه على الإطلاق؟ يوجد إجراء في جوهر Retroputer
STRCMPيمكننا استخدامه ، ولكن كيف يبدو؟
strcmp: {
enter 0x00
push a
push b
push d
push y
pushf
if Z {
bl := 0x00 # Checking for full equality
} else {
bl := 0x01 # only checking for partial equality
}
_main:
y := 0 # start of string
top:
cl := [d, x, y] # character in string A
al := <bp+4>,y # character in string B
cmp bl, 0x01 # check if we're doing full equality
if Z {
cmp cl, 0 # we're not, so check for an early nul
# in string b
brs Z strings-are-equal # if it's NUL, we calling them equal
}
cmp cl, al # check character
if Z {
cmp cl, 0 # equal, but check for NUL
brs Z strings-are-equal # NUL reached, strings are equal
inc y # next character
brs top # not NUL, so keep going...
}
# if here, the strings aren't equal
if N {
popf # string is less than
set N
clr Z
brs _out
} else {
popf # string is greater than
clr N
clr Z
brs _out
}
strings-are-equal:
popf
clr N # Not less than
set Z # but Equal
_out:
c := y # make sure we know how many chars
# were compared
pop y
pop d
pop b
pop a
exit 0x00
ret
}
لا أعرف عنك ، لكنني بدأت أحب الطريقة
String#startsWithمن JS أكثر وأكثر . نعم ، إنها تفعل الشيء نفسه ، لكن لا يتعين علينا كتابة كل المنطق الداخلي!
التعامل المتغير
اكتمل ضغط المفاتيح الآن ، حتى نتمكن من الانتهاء. لكن Retroputer BASIC يأخذ خطوة أخرى إلى الأمام - المتغيرات الرمزية. يبدو لي أن عددًا قليلاً فقط من إصدارات BASIC من الثمانينيات والتسعينيات فعلت ذلك ، لأنه في ظروف الذاكرة المحدودة يمكن أن يكون ضارًا. ولكن إذا كان هناك الكثير من الذاكرة ، فإن ترميز المتغيرات يساعد على تحسين الأداء.
إليك ما يفعله Retroputer BASIC:
- يقرأ ما يصل إلى أول حرفين من اسم المتغير. (كان هذا مصدر إزعاج قياسي للإصدارات الأساسية للعصر بسبب قيود الذاكرة.)
- من هذين الحرفين ، فإنه يحدد فهرس المتغير. "A" متغير 0 ، "A0" متغير 53 ، وهكذا. المعادلة بسيطة جدًا ، لكنها لا تهمنا الآن.
- BASIC . ,
$BASIC . . - BASIC , . !
(ملاحظة: عندما يتعلم Retroputer BASIC تخصيص الذاكرة ديناميكيًا للمتغيرات ، سنستبدل الفهرس بمؤشر إلى متغير. عنصر آخر في قائمة المهام!)
وهذا يجعل العثور على المتغيرات سريعًا بشكل لا يصدق في وقت التشغيل: ليس علينا تحليل اسم المتغير وحساب الفهرس في كل مرة عندما نلتقي بمتغير. في الدورات المستمرة ، يمكن أن تكون المدخرات كبيرة. لكن هذا يأتي بتكلفة عالية: نحتاج إلى تخزين المؤشر والاسم المتغير معًا ، لذلك لكل متغير نواجهه ، نحتاج إلى إضافة أربعة بايتات إضافية إلى الإخراج.
الأمر متروك لك لتقرر ما إذا كان الأمر يستحق ذلك.
مهما كان الأمر ، في JavaScript ، من السهل أيضًا تحديد ما إذا كان باقي تدفق الأحرف متغيرًا:
const variableMatch = inStr.substr(idx).match(/^[A-Z]+[A-Z0-9]*[\$]*/);
if (variableMatch) {
const variableName = variableMatch[0];
idx += variableName.length;
// tokenize it (omitted; nothing new here)
continue;
}
لن أصف بالتفصيل الكود الذي يستخدمه Retroputer حاليًا لترميز المتغيرات ، فهو مطول جدًا. سنعود إليه عندما أقوم بإضافة تخصيص ذاكرة ديناميكي للمتغيرات.
اكتمل الترميز
إذا اختبرنا الآن الكود الخاص بنا في JS ، فسنحصل على مصفوفة بايت مماثلة لتلك التي يستخدمها Retroputer BASIC داخليًا:
console.log(toHex(tokenize(`CLS: PRINT "Hello, World"`)));
// 80 82 20 81 20 FB 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 00 00
واو ، يا له من عمل كثير لحفظ بضع بايت. ولكن عندما يكون هناك بضعة كيلوبايت فقط من الذاكرة الخالية ، فإن الأمر يستحق ذلك! لكن هذه ليست الميزة الوحيدة لضغط النص الذي يدخله المستخدم ، فنحن أيضًا نقوم بتسريع تنفيذ البرنامج.
لشرح سبب حدوث ذلك ، نحتاج إلى فهم كيفية تنفيذ Retroputer للكود. لن أخوض في التفاصيل في الوقت الحالي ، ولكن باختصار ، ما يلي مطلوب لتنفيذ الكود:
- احصل على بايت من الذاكرة
- إذا تم تشغيل البايت الأكثر أهمية ، فهو رمز مميز ، وإلا فهو خطأ في بناء الجملة (أو NUL ؛ على أي حال ، هذا هو المكان الذي ننتهي فيه مع سطر البرنامج)
- نحن نبحث عن معالج الرمز المميز في مصفوفة - تحتوي هذه المصفوفة على مؤشرات إلى الوظائف نفسها التي تعالج الرمز المميز
- نسمي المعالج (مسؤول عن تلقي الحجج وما شابه)
- نكرر من جديد
هذا سبب آخر لترميز الكلمات الرئيسية - يصبح تنفيذ منطق الكلمات الرئيسية أمرًا بسيطًا ، ما عليك سوى العثور على العنوان في المصفوفة والاتصال به.
أود أن أؤكد أنه في حين أن الترميز مهم لتوفير مساحة ، فإنه يحسن أيضًا سرعة التنفيذ.
على سبيل المثال ، قد تبدو حلقة تشغيل JS كما يلي:
const handlers = [ cls, print, endOfStmt ];
bytes.forEach(byte => handlers[byte] && handlers[byte]());
بالطبع ، في الواقع ، كل شيء ليس بهذه البساطة ، إنه أكثر من ذلك بكثير ، لكن هذا بالفعل موضوع لمشاركة أخرى!
مصادر
- كود JavaScript الكامل لهذا المنشور
- شفرة مصدر الكمبيوتر الخلفي (قيد التطوير حاليًا)
- قائمة الموارد المستخدمة لهذا المشروع
إعلان
VPS قوي مع حماية DDoS وأحدث الأجهزة. كل هذا يتعلق بخوادمنا الملحمية . الحد الأقصى للتهيئة هو 128 نواة وحدة المعالجة المركزية ، وذاكرة وصول عشوائي 512 جيجابايت ، و 4000 جيجابايت من ذاكرة الوصول العشوائي NVMe
