المقال السابقأخطاء ، أخطاء ، أخطاء ...
يجب حماية برنامج جيد من أخطاء المستخدم. هذا أمر مؤكد تماما. يجب التعامل مع الأخطاء ، وحتى التحذير بشكل أفضل (الوقاية دائمًا أفضل من العلاج!). الأكروبات - لذا قم ببناء حوار مع المستخدم بحيث لا يتمكن الأخير من ارتكاب خطأ.
على سبيل المثال ، إذا احتاج المستخدم إلى إدخال عدد صحيح موجب في حقل الإدخال ، فيمكنك بالطبع تحليل الإجابة ، وإذا وجدت أحرفًا غير رقمية ، فقم بتقديم تحذير واطلب من المستخدم تكرار الإدخال. ولكن من الأفضل بكثير حظر إدخال الأحرف غير الرقمية ببساطة!
لسوء الحظ ، لا يمكن دائمًا تطبيق هذه التقنية. على وجه الخصوص ، تنوع التصاميم التي تدخل إدخال المترجم أكبر من أن "تقطع التصاميم الخاطئة" ببساطة عن طريق تعيين قناع الإدخال.
يتمتع الشخص بامتياز ارتكاب الأخطاء ، ويجب على المترجم ، في حالة إدخال تراكيب لغوية غير صحيحة ، أن يقدم تشخيصًا واضحًا ، وإذا أمكن ، يواصل تحليل النص المصدر لتحديد جميع الأخطاء. ربما لن يعجب المستخدم حقًا إذا قام المترجم بالتقاط الأخطاء "واحدة تلو الأخرى". ومن غير المقبول تمامًا التعرف على موقف "يتعطل فيه" برنامج برسالة خطأ في النظام.
في هذه المقالة ، سوف نراجع التعليمات البرمجية التي تم تطويرها مسبقًا بشكل نقدي ، ونحاول منع (معالجة) الأخطاء المحتملة.
لنبدأ بوظيفة البدء الأولى. ماذا تفعل؟ تأخذ اسم ملف الإدخال وتفتحه وتعالج سطرا بسطر. بالنسبة إلى هذه البرامج ، فإن سيناريو تفاعل المستخدم قد "استقر" بالفعل - يمكن اعتباره أساسيًا:
- إذا لم يكن اسم الملف محددًا ، فاتصل بمربع الحوار "فتح" القياسي ؛
- إذا قام المستخدم بالنقر فوق الزر "رفض" في مربع الحوار "فتح" - أغلق ؛
- تحقق من وجود الملف بالاسم المحدد / المُدخل. في حالة عدم وجودها ، قم بإصدار رسالة وإنهاء ؛
- إذا كان الملف المحدد موجودًا ، فقم بمعالجته.
نسختنا من إجراء البداية لا تفي بهذا السيناريو. في الواقع ، انظر إلى الرمز أدناه:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …)
لا يتم تحليل الرد السلبي للمستخدم ، لذلك إذا تم الضغط على زر "رفض" ، فإن البرنامج سوف "يتعطل". لا يتم تحليل وجود الملف. لسوء الحظ ، هذا العيب لا يقتصر على أوجه القصور. من الواضح ، إذا كان الإجراء الأساسي البسيط هو الأخير في ملف الإدخال ، فإن تحليل نهاية الملف سيؤدي إلى كسر الدورة قبل تحميل الوظيفة التي تم إنشاؤها في بيئة Lisp.
صحح هذه العيوب:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* … ) (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (if (and fname (filExistp fname)) (let ((fi (gensym 'fi))) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (return t)) (when curr-proc (eval curr-proc)) (when (filEOF fi) (return t)))) (filClose fi) (when *flagerr* (printsline "**** "))) (printsline (if fname (strCat "**** " fname " ") "**** "))) (unset '*numline*) (unset '*flagerr*) (unset '*oplist*))
إذا تم تحديد اسم الملف وكان الملف موجودًا ، يتم تنفيذ المعالجة. خلاف ذلك ، تتم طباعة إحدى الرسائل: "الملف غير موجود" أو "تم حذف اسم الملف".
يتم تنفيذ الإجراءات التالية بشكل متسلسل في جسم الحلقة الرئيسية:
- الوظيفة تتحقق proc. يتم تخزين نتيجة عملها في المتغير المحلي proc-proc ؛
- إذا تم رفع العلم * flagerr * ، تنقطع الحلقة ؛
- إذا أعادت دالة action-proc نتيجة غير فارغة ، فسيتم تحميل الوظيفة التي تم إنشاؤها في بيئة Lisp ؛
- إذا تم الوصول إلى نهاية الملف ، تنكسر الحلقة أيضًا.
يبدو أن الكود أفضل ... ولكن بقي خلل خطير آخر دون حل - بعد اكتمال معالجة الإجراء الذي يحتوي على خطأ واحد أو أكثر ، سيتم كسر الحلقة الرئيسية وينتهي البرنامج دون النظر إلى الجزء من العام الأصلي الموجود خلف الإجراء مع وجود أخطاء. هذا سيء - أود أن يقوم المترجم بإنتاج جميع الأخطاء التي يمكن اكتشافها في كل بداية.
لتصحيح هذا العيب ، دعونا نقدم المتغير العام "عداد الأخطاء" ، أثناء معالجة الإجراء بالأخطاء ، سنزيد هذا العداد. وسيتم إعادة تعيين علامة الخطأ بعد معالجة كل إجراء:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *errcount* 0) (setq *oplist* …) (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (if (and fname (filExistp fname)) (let ((fi (gensym 'fi))) (filCloseAll) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (setq *errcount* (add1 *errcount*))) (when (and curr-proc (not *flagerr*)) (eval curr-proc)) (setq *flagerr* nil) (when (filEOF fi) (return t)))) (filClose fi) (when (> *errcount* 0) (printsline "**** "))) (printsline (if fname (strCat "**** " fname " ") "**** "))) (unset '*numline*) (unset '*flagerr*) (unset '*oplist*) (unset '*errcount*))
الآن ، ستعمل وظيفة البداية مقبولة. دعونا نتأكد من ذلك. قم بإنشاء ملف المصدر التالي:
* * * proc test1(x) local y y=x^2 bla-bla end_proc * * * proc test2() local x,y input x y=test1(x) print y end_proc * * * proc test3(x) bla-bla-bla print x end_proc
و "دعها تمر" من خلال مترجمنا. نحصل على:
0001 * 0002 * 0003 * 0004 proc test1(x) 0005 local y 0006 y=x^2 0007 bla-bla **** (BLA - BLA) 0008 end_proc 0009 * 0010 * 0011 * 0012 proc test2() 0013 local x,y 0014 input x 0015 y=test1(x) 0016 print y 0017 end_proc 0018 * 0019 * 0020 * 0021 proc test3(x) 0022 bla-bla-bla **** (BLA - BLA - BLA) 0023 print x 0024 end_proc 0025 ****
نفترض أننا تعاملنا مع وظيفة البداية. لكن "العمل على البق" بدأ للتو. دعونا نلقي نظرة على بناء جملة ذلك الجزء من اللغة الذي قمنا بتطبيقه بالفعل.
ربما يكون الخطأ الأكثر شيوعًا في بناء الجملة الذي يرتكبه الأشخاص في الغالب هو بنية قوس غير صحيحة (غير متوازنة أو في أقواس ترتيب خاطئ). تذكر ما يحدث لسطر شفرة المصدر لبرنامج mini-basic بعد قراءته. يتم تحليل السلسلة (مقسمة إلى رموز مميزة) ، ثم يتم ترجمة قائمة الرموز المميزة إلى نموذج قائمة داخلي. في قائمة الرموز ، الأقواس هي رموز منفصلة ولا نتحقق من رصيدها. يمكن القيام بذلك كدالة منفصلة ، ولكن يتم إرسال قائمة الرموز إلى إدخال وظيفة الإدخال ، والتي تترجم قائمة الأسطر إلى قائمة Lisp. إذا تم تمرير تعبير سلسلة غير صحيح إلى إدخال دالة الإدخال ، فسوف تُرجع الدالة خطأ.
دعنا نتعامل مع هذا الخطأ.
في HomeLisp ، يتم استخدام البناء لمعالجة الأخطاء (جرب Expression-1 باستثناء Expression-1). يعمل على النحو التالي:
- جرت محاولة لحساب Expression-1. إذا نجحت المحاولة ، يتم إرجاع نتيجة الحساب كنتيجة لنموذج المحاولة بالكامل ؛
- إذا حدث خطأ ، فسيتم حساب Expression-2. في الوقت نفسه ، تتوفر وظيفة نظام بدون معلمات (errormessage) ، والتي تُرجع نص رسالة الخطأ.
بناءً على ما سبق ، يمكن إصدار التحويل إلى نموذج القائمة على النحو التالي:
(defun mk-intf (txt) (let ((lex (parser txt " ," "()+-*/\^=<>%")) (intf "")) (iter (for a in lex) (setq intf (strCat intf a " "))) (try (input (strCat "(" intf ")")) except (progn (printsline (strCat "**** " (errormessage))) `(,txt) ))))
في حالة حدوث خطأ في التحويل ، سيتم إصدار رسالة نظام ، ونتيجة لذلك ، سيتم إرجاع قائمة بعنصر واحد - السطر الأصلي من التعليمات البرمجية. علاوة على ذلك ، ستندرج هذه القائمة (مثل البيان التالي) في الإجراء الإجرائي. وبطبيعة الحال ، لن يتم التعرف عليه. سيؤدي هذا إلى ظهور رسالة خطأ أخرى ، وسيواصل المترجم العمل. سنقوم بتحضير كود المصدر التالي ، ومحاولة ترجمته:
* * * proc test1(x) local y y=(x^2)) end_proc * * * proc test2() local x,y input x y=test1(x) print y end_proc * * * proc test3(x) x=3+)x^2 print x end_proc
نحصل على النتيجة المتوقعة:
0001 * 0002 * 0003 * 0004 proc test1(x) 0005 local y 0006 y=(x^2)) **** **** ("y=(x^2))") 0007 end_proc 0008 * 0009 * 0010 * 0011 proc test2() 0012 local x,y 0013 input x 0014 y=test1(x) 0015 print y 0016 end_proc 0017 * 0018 * 0019 * 0020 proc test3(x) 0021 x=3+)x^2 **** **** ("x=3+)x^2") 0022 print x 0023 end_proc ****
الآن دعونا نلقي نظرة ناقدة على الكود الذي يحول التعبيرات الحسابية إلى تدوين البادئة. لا يحتوي هذا الرمز على أي وسيلة لإصلاح أخطاء المستخدم. لسوء الحظ ، يمكن أن تكون هذه الأخطاء كثيرة. لنصلح هذا الخطأ. للبدء ، دعنا نحاول ترجمة رمز بريء تمامًا (في المظهر):
proc test() local x,y x=6 y=-x print y end_proc
سينتهي البث بـ "سقوط" المترجم! السقوط سيتسبب في عامل y = -x. ما الأمر؟ في أحادية ناقص! بتحويل الصيغة من infix إلى البادئة ، بطريقة ما لم نعتقد أن ناقص "ذو وجهين" - هناك ناقص ثنائي (علامة عملية) ، وهناك ناقص أحادي (علامة رقم). لا يعرف المحلل اللغوي لدينا هذا الاختلاف - فهو يعتبر جميع السلبيات ثنائية ... ماذا تفعل الآن؟ لكي لا تدمر كود العمل بالفعل ، دعنا نحول كل السلبيات الأحادية إلى ثنائية. كيف؟ لكن بسيط جدا. من الواضح تمامًا أن الأحادي ناقص "يعيش" فقط في مثل هذه الإنشاءات:
"(-شيء"
"> - شيء"
"<- شيء"
"= شيء"
حسنًا ، في بداية الصيغة ، يمكنه أيضًا الاجتماع. لذلك ، إذا ، قبل اقتحام الرموز المميزة ، قمنا بإجراء عمليات الاستبدال التالية:
"(-شيء" => "(0-شيء")
"> -شيء" => "> 0-شيء"
"<-شيء" => "<0-شيء"
"= شيء" => "= 0 شيء"
وإذا بدأت الصيغة بعلامة الطرح ، فإننا نخصص صفرًا لبداية الصيغة ، ثم تصبح جميع السلبيات ثنائية وسيتم التخلص من الخطأ بشكل جذري. دعنا ندعو الوظيفة التي ستقوم بالتحويل فوق الاسم prepro. إليك ما قد يبدو عليه:
(defun prepro (s) (let* ((s0 (if (eq "-" (strLeft s 1)) (strCat "0" s) s)) (s1 (strRep s0 "(-" "(0-")) (s2 (strRep s1 "=-" "=0-")) (s3 (strRep s2 ">-" ">0-")) (s4 (strRep s3 "<-" "<0-"))) s4))
لا توجد تعليقات خاصة مطلوبة هنا. لكن محللنا البسيط لديه مشكلة أخرى ليست واضحة تمامًا للوهلة الأولى - علامات مزدوجة للعمليات. عند العمل بالصيغ ، تعني اللافتات ">" و "=" جنبًا إلى جنب عملية واحدة "> =" (ويجب أن تكون رمزا واحدا!). لا يرغب المحلل في معرفة ذلك - سيجعل كل من العلامات رمزا منفصلا. يمكنك التعامل مع هذه المشكلة من خلال النظر في قائمة الرموز المميزة المستلمة ، وإذا كانت الأحرف المقابلة بجوار بعضها البعض ، من خلال الدمج. نسمي الوظيفة التي ستؤدي الاتحاد باسم "postpro". فيما يلي رمز التنفيذ المحتمل:
(defun postpro (lex-list) (cond ((null (cdr lex-list)) lex-list) (t (let ((c1 (car lex-list)) (c2 (cadr lex-list))) (cond ((and (eq c1 ">") (eq c2 "=")) (cons ">=" (postpro (cddr lex-list)))) ((and (eq c1 "<") (eq c2 "=")) (cons "<=" (postpro (cddr lex-list)))) ((and (eq c1 "=") (eq c2 "=")) (cons "==" (postpro (cddr lex-list)))) ((and (eq c1 "<") (eq c2 ">")) (cons "<>" (postpro (cddr lex-list)))) ((and (eq c1 ">") (eq c2 "<")) (cons "<>" (postpro (cddr lex-list)))) ((and (eq c1 "!") (eq c2 "=")) (cons "/=" (postpro (cddr lex-list)))) ((and (eq c1 "/") (eq c2 "=")) (cons "/=" (postpro (cddr lex-list)))) (t (cons c1 (postpro (cdr lex-list)))))))))
أيضا ، كما نرى ، لا شيء خاص. ولكن الآن ستبدو الوظيفة الأخيرة لترجمة عامل التشغيل إلى نموذج القائمة الداخلية كما يلي:
(defun mk-intf (txt) (let ((lex (postpro (parser (prepro txt) " ," "()+-*/\^=<>%"))) (intf "")) (iter (for a in lex) (setq intf (strCat intf a " "))) (try (input (strCat "(" intf ")")) except (progn (printsline (strCat "**** " (errormessage))) `(,txt) ))))
الآن دعونا نلقي نظرة ناقدة على وظيفة inf2ipn. ما أخطاء المستخدم التي "تلومها"؟ لقد قطعنا بالفعل اختلال التوازن بين قوسين أعلاه. ماذا يمكن أن يكون أكثر؟ علامتان للعملية أو معاملتين واقفتين على التوالي. يمكن للمرء أن يحلل هذا في رمز inf2ipn (وأولئك الذين يرغبون في القيام بذلك بأنفسهم). لكننا "نلتقط" هذه الأخطاء في مرحلة تحويل الصيغة من SCR إلى البادئة. ودعونا (فقط في حالة) سنلتقط جميع الأخطاء التي قد تنشأ في عملية تحويل الصيغة من infix إلى البادئة. أفضل مكان لذلك هو وظيفة المجمع i2p. الآن قد يبدو مثل هذا:
(defun i2p (f) (try (ipn2pref (inf2ipn f)) except (progn (printsline "**** ") (printsline (strCat "**** " (errormessage))) (setq *flagerr* t) nil)))
والآن سنمنع ظهور علامتي تشغيل أو معاملين متتاليين في الصيغ. وصفت المقالة السابقة خوارزمية لترجمة صيغة من SCR إلى شكل بادئة. علامة على الإكمال الصحيح لهذه الخوارزمية هي أنه في الخطوة الأخيرة يجب أن تحتوي الحزمة على قيمة واحدة. إذا لم يكن الأمر كذلك ، فقد تم ارتكاب خطأ. وينشأ موقف خاطئ آخر في الحالة عندما يتم استدعاء الوظيفة بعدد خاطئ (أكثر أو أقل) من المعلمات. يجب "القبض" هذه الحالات:
(defun ipn2pref (f &optional (s nil)) (cond ((null f) (if (null (cdr s)) (car s) (progn (printsline "**** ") (setq *flagerr* t) nil))) ((numberp (car f)) (ipn2pref (cdr f) (cons (car f) s))) ((is-op (car f)) (let ((ar (arity (car f)))) (if (< (length s) ar) (progn (setq *flagerr* t) (printsline "**** ") nil) (ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar)))))) ((atom (car f)) (ipn2pref (cdr f) (cons (car f) s))) (t (ipn2pref (cdr f) (cons (list (car f) (car s)) (cdr s))))))
الآن دعونا نلقي نظرة فاحصة على معالج بيان proc. من الواضح أننا فوتنا نقطتين. أول شيء يجب فعله هو ألا تنسى عند معالجة الإجراء لحساب قيمته (عدد الحجج) وتعديل المتغير العام * oplist * وفقًا لذلك. والثاني هو أن الوظائف التي ننشئها لا تُرجع القيمة الصحيحة! بتعبير أدق ، نتيجة للوظائف التي تم إنشاؤها بواسطة مترجمنا ، سيتم إرجاع قيمة النموذج الأخير المحسوب قبل العودة. لضمان إرجاع القيمة المطلوبة ، أقترح نقل متغير النتيجة من باسكال. الآن ، إذا لزم الأمر ، قم بإرجاع القيمة المطلوبة ، يكفي للمستخدم تعيين القيمة المطلوبة لهذا المتغير قبل الخروج من الوظيفة ، وعند إنشاء نص الدالة ، نحتاج إلى إدراج نتيجة الاسم في نص الوظيفة مع التعبير الأخير. كل هذا يجلب وظيفة العمل إلى:
(defun action-proc (fi) (let ((stmt nil) (proc-name nil) (proc-parm nil) (loc-var nil) (lv '((result 0))) (body nil)) (loop (setq stmt (mk-intf (getLine fi))) (when (null stmt) (return t)) (cond ((eq (car stmt) 'proc) (setq proc-name (nth 1 stmt)) (setq proc-parm (nth 2 stmt)) (setq *oplist* (cons (list proc-name (length proc-parm)) *oplist*))) ((eq (car stmt) 'end_proc) (return t)) ((eq (car stmt) 'print) (setq body (append body (list (cons 'printline (cdr stmt)))))) ((eq (car stmt) 'input) (setq body (append body (list (list 'setq (cadr stmt) (list 'read) ))))) ((eq (car stmt) 'local) (setq loc-var (append loc-var (cdr stmt)))) ((eq (cadr stmt) '=) (setq body (append body (list (action-set stmt))))) (t (printsline (strCat "**** " (output stmt) " ")) (setq *flagerr* t)))) (iter (for a in (setof loc-var)) (collecting (list a 0) into lv)) (if proc-name `(defun ,proc-name ,proc-parm (let ,lv ,@body result)) nil)))
سنتوقف هنا الآن (على الرغم من أننا ما زلنا نواجه مشاكل ، ويجب الانتهاء من التعليمات البرمجية ؛ ولكن هذا هو الكثير من المبرمج ...) والآن سننظر في إدخال تحسينات على لغتنا مناسبة لإجراء ذلك الآن.
تحسينات طفيفة ...
في مقال سابق ، كتبت أنه من غير المناسب بالنسبة للمبرمج إذا كان عامل واحد في لغة يشغل سطرًا واحدًا بالضبط. من الضروري توفير القدرة على كتابة عبارات ضخمة على خطوط متعددة. دعونا ننفذ هذا. هذا ليس من الصعب القيام به على الإطلاق. في إجراء getLine ، سننشئ متغيرًا محليًا سنقوم فيه بتجميع النص المقروء (بشرط ألا يكون تعليقًا وينتهي بحرفين "_". بمجرد إصلاح سطر هام بنهاية مختلفة ، نعيد القيمة المتراكمة كقيمة. إليك الرمز:
(defun getLine (fil) (let ((stri "") (res "")) (loop (when (filEof fil) (return "")) (setq *numline* (add1 *numline*)) (setq stri (filGetline fil)) (printsline (strCat (format *numline* "0000") " " (strRTrim stri))) (unless (or (eq "" stri) (eq "*" (strLeft stri 1))) (setq stri (strATrim stri)) (if (eq " _"(strRight stri 2)) (setq res (strCat res (strLeft stri (- (strLen stri) 2)))) (setq res (strCat res stri))) (unless (eq " _"(strRight stri 2)) (return res))))))
والتحسن الأخير. في العديد من لغات البرمجة ، يمكنك استخدام المعاملات المنطقية في التعبيرات الحسابية (والتي يتم حسابها في هذه الحالة إلى صفر أو واحد). هذا يعطي اللغة تعبيرًا إضافيًا ، وبالمناسبة ، يتوافق تمامًا مع الروح الأساسية. في mini-BASIC ، فإن محاولة حساب هذا التعبير ، على سبيل المثال ، هي:
z=(x>y)*5+(x<=y)*10
سيؤدي إلى خطأ وقت التشغيل. وهذا أمر مفهوم: في Lisp ، يتم حساب التعبير (> xy) إلى Nil أو T. ولكن لا يمكن ضرب Nil / T في 5 ... ومع ذلك ، فإن هذه المشكلة سهلة المساعدة. لنكتب بعض وحدات الماكرو البسيطة التي تستبدل نتيجة تعبيرات المقارنة بـ 0/1 (بدلاً من Nil / T):
(defmacro $= (xy) `(if (= ,x ,y) 1 0)) (defmacro $== (xy) `(if (= ,x ,y) 1 0)) (defmacro $> (xy) `(if (> ,x ,y) 1 0)) (defmacro $< (xy) `(if (< ,x ,y) 1 0)) (defmacro $/= (xy) `(if (/= ,x ,y) 1 0)) (defmacro $<> (xy) `(if (/= ,x ,y) 1 0)) (defmacro $<= (xy) `(if (<= ,x ,y) 1 0)) (defmacro $>= (xy) `(if (>= ,x ,y) 1 0))
الآن ، ألق نظرة على السطر في وظيفة ipn2pref التي تقوم بمعالجة العملية. هنا هو الخط:
(ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar)))
هنا (السيارة و) هو اسم العملية. دعنا نكتب دالة صغيرة لاستبدال رموز المقارنة:
(defun chng-comp (op) (if (member op '(= == /= <> > < >= <=)) (implode (cons '$ (explode op))) op))
تتحقق الوظيفة مما إذا كانت وسيطتها عملية مقارنة ، وإذا لزم الأمر ، تلحق الحرف "$" في البداية. الآن قم بتسميته في المكان المناسب لوظيفة ipn2pref:
(ipn2pref (cdr f) (cons (cons (chng-comp (car f)) (reverse (subseq s 0 ar))) (subseq s ar)))
ماذا ستكون النتيجة؟ سيتم استبدال عمليات المقارنة باستدعاء الماكرو المقابل ، ولن تتغير جميع العمليات الأخرى. إذا قمت بترجمة هذه الوظيفة:
proc test() local x,y x=1 y=2 result=(x>y)*5+(x<=y)*10 end_proc
ثم نسميها ، نحصل على النتيجة المتوقعة.
هذا كل شيء لهذا اليوم.
يوجد كود هذه المقالة
هنا.يتبع.