Vorheriger ArtikelFehler, Fehler, Fehler ...
Ein gutes Programm sollte vor Benutzerfehlern geschützt werden. Das ist absolut sicher. Fehler müssen behandelt und noch besser gewarnt werden (Vorbeugen ist immer besser als Heilen!). Kunstflug - Bauen Sie also einen Dialog mit dem Benutzer auf, damit dieser einfach keinen Fehler machen kann.
Wenn der Benutzer beispielsweise eine positive Ganzzahl in das Eingabefeld eingeben muss, können Sie natürlich die Antwort analysieren. Wenn Sie nicht numerische Zeichen finden, geben Sie eine Warnung aus und bitten Sie den Benutzer, die Eingabe zu wiederholen. Es ist jedoch viel besser, die Eingabe nicht numerischer Zeichen einfach zu verbieten!
Leider kann eine solche Technik nicht immer angewendet werden. Insbesondere ist die Vielfalt der Designs, die an die Eingabe des Übersetzers gelangen, zu groß, um durch Einstellen der Eingabemaske einfach „die falschen abzuschneiden“.
Eine Person hat das Privileg, Fehler zu machen, und der Übersetzer sollte im Falle der Eingabe falscher Sprachkonstrukte eine eindeutige Diagnose stellen und den Quelltext nach Möglichkeit weiter analysieren, um alle Fehler zu identifizieren. Dem Benutzer wird es wahrscheinlich nicht wirklich gefallen, wenn der Übersetzer "nacheinander" Fehler entdeckt. Und es ist absolut inakzeptabel, eine Situation zu erkennen, in der ein Programm mit einer Systemfehlermeldung "abstürzt".
In diesem Artikel werden wir den zuvor entwickelten Code kritisch durchgehen und versuchen, mögliche Fehler zu verhindern (zu verarbeiten).
Beginnen wir mit der ersten Startfunktion. Was macht sie? Sie nimmt den Namen der Eingabedatei, öffnet sie und verarbeitet sie zeilenweise. Für solche Programme hat sich das Benutzerinteraktionsszenario bereits „beruhigt“ - es kann als kanonisch angesehen werden:
- Wenn der Dateiname nicht angegeben ist, rufen Sie das Standarddialogfeld "Öffnen" auf.
- Wenn der Benutzer im Dialogfeld "Öffnen" auf die Schaltfläche "Ablehnen" geklickt hat - Herunterfahren;
- Überprüfen Sie, ob die Datei mit dem angegebenen / eingegebenen Namen vorhanden ist. Wenn es nicht vorhanden ist, geben Sie eine Nachricht aus und beenden Sie das Programm.
- Wenn die angegebene Datei vorhanden ist, verarbeiten Sie sie.
Unsere Version des Startvorgangs erfüllt dieses Szenario nicht. Schauen Sie sich den folgenden Code an:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …)
Die negative Reaktion des Benutzers wird nicht analysiert. Wenn also die Schaltfläche "Ablehnen" gedrückt wird, stürzt das Programm ab. Das Vorhandensein der Datei wird ebenfalls nicht analysiert. Leider ist dieser Fehler nicht auf Mängel beschränkt. Wenn die Mini-Basic-Prozedur die letzte in der Eingabedatei ist, führt die Analyse des Dateiende offensichtlich dazu, dass der Zyklus unterbrochen wird, bevor die generierte Funktion in die Lisp-Umgebung geladen wird.
Korrigieren Sie diese Fehler:
(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*))
Wenn der Dateiname angegeben ist und die Datei vorhanden ist, wird die Verarbeitung durchgeführt. Andernfalls wird eine der Meldungen gedruckt: "Datei existiert nicht" oder "Dateiname weggelassen".
Die folgenden Aktionen werden nacheinander im Hauptteil der Hauptschleife ausgeführt:
- Funktion action-proc ist erfüllt. Das Ergebnis seiner Arbeit wird in der lokalen Variablen curr-proc gespeichert;
- Wenn das Flag * flagerr * gesetzt ist, wird die Schleife unterbrochen.
- Wenn die action-proc-Funktion ein nicht leeres Ergebnis zurückgegeben hat, wird die generierte Funktion in die Lisp-Umgebung geladen.
- Wenn das Ende der Datei erreicht ist, wird auch die Schleife unterbrochen.
Der Code schien besser zu sein ... Ein weiterer schwerwiegender Fehler blieb jedoch ungelöst. Nachdem die Verarbeitung der Prozedur mit einem oder mehreren Fehlern abgeschlossen ist, wird die Hauptschleife unterbrochen und das Programm wird beendet, ohne den Teil des ursprünglichen Jahres zu betrachten, der sich hinter der Prozedur mit Fehlern befindet. Das ist schlecht - ich möchte, dass der Übersetzer alle Fehler erzeugt, die bei jedem Start erkannt werden können.
Um diesen Fehler zu beheben, führen wir die globale Variable "Fehlerzähler" ein. Während wir die Prozedur mit Fehlern verarbeiten, erhöhen wir diesen Zähler. Und das Fehlerflag wird nach jeder Prozedur zurückgesetzt:
(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*))
Jetzt funktioniert die Startfunktion akzeptabel. Stellen wir das sicher. Erstellen Sie die folgende Quelldatei:
* * * 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
Und lassen Sie es durch unseren Übersetzer gehen. Wir bekommen:
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 ****
Wir gehen davon aus, dass wir mit der Startfunktion fertig geworden sind. Aber die „Arbeit an den Fehlern“ hat gerade erst begonnen. Schauen wir uns die Syntax des Teils der Sprache an, den wir bereits implementiert haben.
Der wahrscheinlich häufigste Syntaxfehler, den Menschen am häufigsten machen, ist eine falsche Klammerstruktur (unausgeglichen oder in Klammern in falscher Reihenfolge). Erinnern Sie sich daran, was mit einer Zeile Quellcode für ein Mini-Basisprogramm passiert, nachdem es gelesen wurde. Die Zeichenfolge wird analysiert (in Token unterteilt), und dann wird die Liste der Token in eine interne Listenform übersetzt. In der Liste der Token sind Klammern separate Token, und wir überprüfen nicht ihren Kontostand. Dies könnte als separate Funktion erfolgen, aber die Liste der Token wird an die Eingabe der Eingabefunktion übertragen, die die Liste der Zeilen in die Lisp-Liste übersetzt. Wenn ein falscher Zeichenfolgenausdruck an die Eingabe der Eingabefunktion übergeben wird, gibt die Funktion einen Fehler zurück.
Lassen Sie uns diesen Fehler behandeln.
In HomeLisp wird ein Konstrukt verwendet, um Fehler zu behandeln (versuchen Sie es mit Ausdruck-1 außer Ausdruck-1). Es funktioniert wie folgt:
- Es wird versucht, Ausdruck 1 zu berechnen. Wenn der Versuch erfolgreich ist, wird das Berechnungsergebnis als Ergebnis des gesamten Versuchsformulars zurückgegeben.
- Wenn ein Fehler auftritt, wird Ausdruck 2 berechnet. In diesem Fall steht eine Systemfunktion ohne Parameter (Fehlermeldung) zur Verfügung, die den Text der Fehlermeldung zurückgibt.
Auf der Grundlage des Vorstehenden kann die Übertragung auf das Listenformular wie folgt erfolgen:
(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) ))))
Im Falle eines Konvertierungsfehlers wird eine Systemnachricht ausgegeben, und als Ergebnis wird eine Liste eines Elements zurückgegeben - die ursprüngliche Codezeile. Ferner wird diese Liste (als nächste Aussage) in das action-proc-Verfahren fallen. Und natürlich wird es nicht erkannt. Dies erzeugt eine weitere Fehlermeldung und der Compiler arbeitet weiter. Wir werden den folgenden Quellcode vorbereiten und versuchen, ihn zu übersetzen:
* * * 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
Wir erhalten das erwartete Ergebnis:
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 ****
Schauen wir uns nun den Code an, der arithmetische Ausdrücke in eine Präfixnotation konvertiert. Dieser Code enthält keine Mittel zur Behebung von Benutzerfehlern. Leider können diese Fehler ziemlich viel sein. Beheben wir diesen Fehler. Versuchen wir zunächst, einen völlig unschuldigen Code (in Erscheinung) zu übersetzen:
proc test() local x,y x=6 y=-x print y end_proc
Die Sendung endet mit dem „Sturz“ des Übersetzers! Ein Sturz führt zum Operator y = -x. Was ist los? In einem unären Minus! Beim Konvertieren einer Formel von einer Infixform in eine Präfixformel dachten wir irgendwie nicht, dass Minus „zweiseitig“ ist - es gibt ein binäres Minus (ein Operationszeichen) und ein unäres Minus (ein Zeichen einer Zahl). Unser Parser kennt diesen Unterschied nicht - er betrachtet alle Nachteile als binär ... Was ist jetzt zu tun? Um den bereits funktionierenden Code nicht zu zerstören, verwandeln wir alle unären Nachteile in binäre. Wie? Aber sehr einfach. Es ist ziemlich offensichtlich, dass das unäre Minus nur in solchen Konstruktionen „lebt“:
"(-Etwas"
"> - Etwas"
"<-Something"
"= Etwas"
Nun, ganz am Anfang der Formel kann er sich auch treffen. Wenn wir daher vor dem Aufbrechen in Token die folgenden Ersetzungen durchführen:
"(-Etwas" => "(0-etwas")
"> -Something" => "> 0-etwas"
"<-Something" => "<0-etwas"
"= Etwas" => "= 0 etwas"
und wenn die Formel mit einem Minus beginnt, weisen wir dem Anfang der Formel Null zu, dann werden alle Minuspunkte binär und der Fehler wird radikal beseitigt. Rufen wir die Funktion auf, die die Konvertierung über dem Namen prepro durchführt. So könnte es aussehen:
(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))
Hier sind keine besonderen Kommentare erforderlich. Unser einfacher Parser hat jedoch ein anderes Problem, das auf den ersten Blick nicht ganz offensichtlich ist - doppelte Anzeichen von Operationen. Bei der Arbeit mit Formeln bedeuten die nebeneinander stehenden Zeichen „>“ und „=“ eine Operation „> =“ (und müssen ein Token sein!). Der Parser möchte dies nicht wissen - er macht jedes der Zeichen zu einem separaten Token. Sie können dieses Problem lösen, indem Sie sich die Liste der empfangenen Token ansehen und durch Kombinieren die entsprechenden Zeichen nebeneinander anzeigen. Wir benennen die Funktion, die die Vereinigung ausführt, mit dem Namen "postpro". Hier ist der Code für eine mögliche Implementierung:
(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)))))))))
Auch, wie wir sehen, nichts Besonderes. Die endgültige Funktion zum Übersetzen des Operators in das interne Listenformular sieht nun folgendermaßen aus:
(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) ))))
Schauen wir uns nun die inf2ipn-Funktion kritisch an. Welche Benutzerfehler können es "beschuldigen"? Wir haben das Ungleichgewicht der obigen Klammern bereits beseitigt. Was könnte mehr sein? Zwei Zeichen der Operation oder zwei Operanden, die in einer Reihe stehen. Man könnte dies im inf2ipn-Code analysieren (und diejenigen, die dies wünschen, können dies selbst tun). Diese Fehler werden jedoch bereits beim Konvertieren der Formel vom SCR in das Präfix "erfasst". Und lassen Sie uns (nur für den Fall) alle Fehler abfangen, die bei der Konvertierung der Formel vom Infix zum Präfix auftreten können. Der beste Ort dafür ist die i2p-Wrapper-Funktion. Jetzt könnte es so aussehen:
(defun i2p (f) (try (ipn2pref (inf2ipn f)) except (progn (printsline "**** ") (printsline (strCat "**** " (errormessage))) (setq *flagerr* t) nil)))
Und jetzt verhindern wir das Auftreten von zwei Operationszeichen oder zwei Operanden hintereinander in Formeln. Im vorherigen Artikel wurde ein Algorithmus zum Übersetzen einer Formel aus einem SCR in ein Präfixformular beschrieben. Ein Zeichen für die korrekte Vervollständigung dieses Algorithmus ist, dass der Stapel im letzten Schritt einen einzelnen Wert enthalten sollte. Ist dies nicht der Fall, wurde ein Fehler gemacht. Eine weitere fehlerhafte Situation tritt auf, wenn die Funktion mit der falschen (mehr oder weniger) Anzahl von Parametern aufgerufen wird. Diese Situationen sollten "gefangen" werden:
(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))))))
Schauen wir uns nun den proc-Anweisungshandler kritisch an. Wir haben zwei Punkte deutlich verpasst. Das erste, was Sie tun müssen, ist, bei der Verarbeitung der Prozedur zur Berechnung ihrer Arität (Anzahl der Argumente) nicht zu vergessen und die globale Variable * oplist * entsprechend zu ändern. Und das zweite ist, dass die von uns generierten Funktionen nicht den richtigen Wert zurückgeben! Genauer gesagt wird aufgrund der von unserem Übersetzer generierten Funktionen der Wert des zuletzt vor der Rückgabe berechneten Formulars zurückgegeben. Um die Rückgabe des gewünschten Wertes zu gewährleisten, schlage ich vor, die Ergebnisvariable von Pascal zu übertragen. Geben Sie nun bei Bedarf den gewünschten Wert zurück. Es reicht aus, wenn der Benutzer dieser Variablen vor dem Beenden der Funktion den gewünschten Wert zuweist. Wenn Sie den Hauptteil der Funktion generieren, müssen Sie den Namen result in den Funktionskörper mit dem letzten Ausdruck einfügen. All dies bringt die Action-Proc-Funktion zu:
(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)))
Wir werden hier vorerst aufhören (obwohl wir immer noch auf Probleme stoßen werden und der Code finalisiert werden muss; aber das ist das Los des Programmierers ...) Und jetzt werden wir zwei Verbesserungen unserer Sprache in Betracht ziehen, die angemessen sind, um sie jetzt vorzunehmen.
Kleinere Verbesserungen ...
In einem früheren Artikel habe ich geschrieben, dass es für einen Programmierer unpraktisch ist, wenn in einer Sprache ein Operator genau eine Zeile belegt. Es ist erforderlich, die Möglichkeit bereitzustellen, sperrige Anweisungen in mehrere Zeilen zu schreiben. Lassen Sie uns dies implementieren. Dies ist überhaupt nicht schwer zu tun. In der getLine-Prozedur erstellen wir eine lokale Variable, in der wir den gelesenen Text akkumulieren (vorausgesetzt, dies ist kein Kommentar und endet mit ein paar "_" -Zeichen. Sobald eine signifikante Zeile mit einer anderen Endung festgelegt ist, geben wir den akkumulierten Wert als Wert zurück. Hier ist der Code:
(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))))))
Und die letzte Verbesserung. In vielen Programmiersprachen können Sie logische Operanden in arithmetischen Ausdrücken verwenden (die in diesem Fall auf Null oder Eins berechnet werden). Dies verleiht der Sprache zusätzliche Ausdruckskraft und entspricht im Übrigen durchaus dem Grundgeist. In unserem Mini-BASIC lautet der Versuch, diesen Ausdruck zu berechnen, beispielsweise:
z=(x>y)*5+(x<=y)*10
verursacht einen Laufzeitfehler. Und das ist verständlich: In Lisp wird der Ausdruck (> xy) zu Nil oder T berechnet. Aber Nil / T kann nicht mit 5 multipliziert werden ... Dieses Problem ist jedoch leicht zu beheben. Schreiben wir einige einfache Makros, die das Ergebnis von Vergleichsausdrücken durch 0/1 (anstelle von Nil / T) ersetzen:
(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))
Schauen Sie sich nun die Zeile in der Funktion ipn2pref an, die die Verarbeitung der Operation ausführt. Hier ist die Zeile:
(ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar)))
Hier (Auto f) ist der Name der Operation. Schreiben wir eine winzige Funktion, um Vergleichscodes zu ersetzen:
(defun chng-comp (op) (if (member op '(= == /= <> > < >= <=)) (implode (cons '$ (explode op))) op))
Die Funktion prüft, ob es sich bei ihrem Argument um eine Vergleichsoperation handelt, und hängt bei Bedarf das Zeichen "$" an den Anfang an. Rufen Sie es nun an der richtigen Stelle der ipn2pref-Funktion auf:
(ipn2pref (cdr f) (cons (cons (chng-comp (car f)) (reverse (subseq s 0 ar))) (subseq s ar)))
Was wird das Ergebnis sein? Vergleichsoperationen werden durch Aufrufe des entsprechenden Makros ersetzt, und alle anderen Operationen werden nicht geändert. Wenn Sie diese Funktion übersetzen:
proc test() local x,y x=1 y=2 result=(x>y)*5+(x<=y)*10 end_proc
und dann nennen wir es, wir bekommen das erwartete Ergebnis.
Das ist alles für heute.
Der Code für diesen Artikel befindet sich
hier.Fortsetzung folgt.