Nous écrivons un traducteur simple en Lisp - III

Article précédent

Erreurs, erreurs, erreurs ...


Un bon programme doit être protégé contre les erreurs des utilisateurs. C'est absolument certain. Les erreurs doivent être traitées, et encore mieux prévenues (prévenir vaut toujours mieux que guérir!). Voltige - construisez donc un dialogue avec l'utilisateur afin que ce dernier ne puisse tout simplement pas se tromper.

Par exemple, si l'utilisateur doit entrer un entier positif dans le champ de saisie, alors, bien sûr, vous pouvez analyser la réponse, et si vous trouvez des caractères non numériques, donner un avertissement et demander à l'utilisateur de répéter la saisie. Mais il vaut mieux interdire tout simplement la saisie de caractères non numériques!

Malheureusement, une telle technique ne peut pas toujours être appliquée. En particulier, la variété des modèles venant à l'entrée du traducteur est trop grande pour simplement «couper les mauvais» en définissant le masque de saisie.

Une personne a le privilège de faire des erreurs et le traducteur doit, en cas de saisie de constructions linguistiques incorrectes, donner un diagnostic clair et, si possible, continuer d'analyser le texte source pour identifier toutes les erreurs. L'utilisateur ne l'aimera probablement pas vraiment si le traducteur détecte les erreurs «une à la fois». Et il est absolument inacceptable de reconnaître une situation dans laquelle le programme se bloque avec un message d'erreur système.
Dans cet article, nous allons passer en revue le code développé précédemment de manière critique et essayer d'empêcher (traiter) les erreurs possibles.

Commençons par la première fonction de démarrage. Que fait-elle? Elle prend le nom du fichier d'entrée, l'ouvre et traite ligne par ligne. Pour de tels programmes, le scénario d'interaction avec l'utilisateur est déjà «réglé» - il peut être considéré comme canonique:

  • Si le nom de fichier n'est pas spécifié, appelez la boîte de dialogue standard «Ouvrir»;
  • Si l'utilisateur a cliqué sur le bouton «refuser» dans la boîte de dialogue «Ouvrir» - arrêtez;
  • Vérifiez si le fichier avec le nom donné / entré existe. S'il n'existe pas, émettez un message et quittez;
  • Si le fichier spécifié existe, traitez-le.

Notre version de la procédure de démarrage ne satisfait pas ce scénario. En fait, regardez le code ci-dessous:

(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …) ;;      (when (zerop (strLen fname)) (setq fname (sysGetOpenName (sysHome) "-|*.mbs"))) (let ((fi (gensym 'fi))) (filOpen fi fname _INPUT) (loop (let ((curr-proc (action-proc fi))) (when *flagerr* (return t)) (when (filEOF fi) (return t)) (eval curr-proc))) (filClose fi)) (when *flagerr* (printsline "****   "))) 

La réponse négative de l'utilisateur n'est pas analysée, donc si le bouton "rejeter" est enfoncé, le programme "plantera". L'existence du fichier n'est pas non plus analysée. Malheureusement, cette faille ne se limite pas aux lacunes. Évidemment, si la procédure mini-basique est la dernière du fichier d'entrée, l'analyse de la fin du fichier provoquera la rupture du cycle avant que la fonction générée ne soit chargée dans l'environnement Lisp.

Corrigez ces défauts:

 (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*)) 

Si le nom de fichier est spécifié et que le fichier existe, le traitement est effectué. Sinon, l'un des messages est imprimé: «Le fichier n'existe pas» ou «Nom de fichier omis».
Les actions suivantes sont exécutées séquentiellement dans le corps de la boucle principale:

  • La fonction action-proc est remplie. Le résultat de son travail est stocké dans la variable locale curr-proc;
  • Si le drapeau * flagerr * est levé, la boucle se rompt;
  • Si la fonction action-proc a renvoyé un résultat non vide, la fonction générée est chargée dans l'environnement Lisp;
  • Si la fin du fichier est atteinte, la boucle se rompt également.

Le code semblait être meilleur ... Mais une autre faille sérieuse est restée non résolue - une fois le traitement de la procédure contenant une ou plusieurs erreurs terminé, la boucle principale sera rompue et le programme se terminera sans regarder la partie de l'année d'origine située derrière la procédure avec des erreurs. C'est mauvais - je voudrais que le traducteur produise toutes les erreurs qui peuvent être détectées à chaque démarrage.

Pour corriger ce défaut, introduisons la variable globale «compteur d'erreurs», tout en traitant la procédure avec des erreurs, nous allons augmenter ce compteur. Et l'indicateur d'erreur sera réinitialisé après le traitement de chaque procédure:

 (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*)) 

Maintenant, la fonction de démarrage fonctionnera de manière acceptable. Assurons-nous de cela. Créez le fichier source suivant:

 * *    * 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 

Et «laissez passer» grâce à notre traducteur. Nous obtenons:

 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 ****    

Nous supposons que nous avons géré la fonction de démarrage. Mais le «travail sur les bugs» ne fait que commencer. Regardons la syntaxe de cette partie du langage que nous avons déjà implémentée.

L'erreur de syntaxe la plus courante que les gens font le plus souvent est probablement une structure de crochets incorrecte (déséquilibrée ou dans le mauvais ordre entre parenthèses). Rappelez-vous ce qui arrive à une ligne de code source pour un mini-programme de base après sa lecture. La chaîne est analysée (divisée en jetons), puis la liste des jetons est traduite sous forme de liste interne. Dans la liste des jetons, les parenthèses sont des jetons distincts et nous ne vérifions pas leur solde. Cela pourrait être fait comme une fonction distincte, mais la liste des jetons est transmise à l'entrée de la fonction d'entrée, qui traduit la liste des lignes dans la liste Lisp. Si une expression de chaîne incorrecte est transmise à l'entrée de la fonction d'entrée, la fonction renvoie une erreur.

Gérons cette erreur.

Dans HomeLisp, une construction est utilisée pour gérer les erreurs (essayez Expression-1 sauf Expression-1). Cela fonctionne comme suit:

  • Une tentative est faite pour calculer l'expression-1. Si la tentative réussit, le résultat du calcul est renvoyé comme résultat de l'ensemble du formulaire d'essai;
  • Si une erreur se produit, alors l'expression-2 est calculée. Dans le même temps, une fonction système sans paramètres (message d'erreur) est disponible, qui renvoie le texte du message d'erreur.

Sur la base de ce qui précède, le transfert vers le formulaire de liste peut être effectué comme suit:

 (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) )))) 

En cas d'erreur de conversion, un message système sera émis et, par conséquent, une liste d'un élément sera renvoyée - la ligne de code d'origine. De plus, cette liste tombera (comme la prochaine déclaration) dans la procédure action-proc. Et, bien sûr, il ne sera pas reconnu. Cela générera un autre message d'erreur et le compilateur continuera de fonctionner. Nous allons préparer le code source suivant et essayer de le traduire:

 * *    * 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 

On obtient le résultat attendu:

 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 ****    

Jetons maintenant un regard critique sur le code qui convertit les expressions arithmétiques en notation de préfixe. Ce code ne contient aucun moyen de corriger les erreurs utilisateur. Malheureusement, ces erreurs peuvent être considérables. Corrigeons cette erreur. Pour commencer, essayons de traduire un code complètement innocent (en apparence):

 proc test() local x,y x=6 y=-x print y end_proc 

L'émission se terminera par la «chute» du traducteur! Une chute entraînera l'opérateur y = -x. Quelle est la question? Dans un moins unaire! En convertissant une formule d'une forme infixe en une forme préfixée, nous ne pensions pas en quelque sorte que moins «à deux faces» - il y a un moins binaire (un signe d'opération), et il y a un moins unaire (signe d'un nombre). Notre analyseur ne connaît pas cette différence - il considère que tous les inconvénients sont binaires ... Que faire maintenant? Afin de ne pas détruire le code déjà fonctionnel, transformons tous les inconvénients unaires en binaires. Comment? Mais très simple. Il est bien évident que le moins unaire ne «vit» que dans de telles constructions:

"(-Quelque chose"
"> -Quelque chose"
"<-Quelque chose"
"= Quelque chose"
Eh bien, au tout début de la formule, il peut aussi se rencontrer. Par conséquent, si, avant de pénétrer en jetons, nous effectuons les remplacements suivants:

"(-Quelque chose" => "(0-quelque chose"
"> -Quelque chose" => "> 0-quelque chose"
"<-Quelque chose" => "<0-quelque chose"
"= Quelque chose" => "= 0 quelque chose"

et si la formule commence par un moins, nous attribuons zéro au début de la formule, alors tous les moins deviendront binaires et l'erreur sera éliminée radicalement. Appelons la fonction qui effectuera la conversion au-dessus du nom prepro. Voici à quoi cela pourrait ressembler:

 (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)) 

Aucun commentaire spécial n'est requis ici. Mais notre simple analyseur a un autre problème qui n'est pas tout à fait évident à première vue - les doubles signes des opérations. Lorsque vous travaillez avec des formules, les signes «>» et «=» côte à côte signifient une opération «> =» (et doivent être un seul jeton!). L'analyseur ne veut pas savoir cela - il fera de chacun des signes un jeton distinct. Vous pouvez faire face à ce problème en consultant la liste des jetons reçus, et si les caractères correspondants sont côte à côte, en les combinant. Nous nommons la fonction qui effectuera l'union sous le nom de «postpro». Voici le code d'une éventuelle implémentation:

 (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))))))))) 

De plus, comme nous le voyons, rien de spécial. Mais maintenant, la fonction finale de la traduction de l'opérateur dans le formulaire de liste interne ressemblera à ceci:

 (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) )))) 

Jetons maintenant un regard critique sur la fonction inf2ipn. Quelles erreurs des utilisateurs peuvent le «blâmer»? Nous avons déjà supprimé le déséquilibre des crochets ci-dessus. Quoi de plus? Deux signes d'opération ou deux opérandes, debout dans une rangée. On pourrait analyser cela dans le code inf2ipn (et ceux qui le souhaitent peuvent le faire par eux-mêmes). Mais nous «interceptons» ces erreurs au stade de la conversion de la formule du SCR en préfixe. Et nous allons (juste au cas où) nous allons attraper toutes les erreurs qui peuvent survenir dans le processus de conversion de la formule d'infixe en préfixe. Le meilleur endroit pour cela est la fonction wrapper i2p. Maintenant, cela pourrait ressembler à ceci:

 (defun i2p (f) (try (ipn2pref (inf2ipn f)) except (progn (printsline "****    ") (printsline (strCat "**** " (errormessage))) (setq *flagerr* t) nil))) 

Et maintenant, nous allons empêcher l'apparition dans les formules de deux signes d'opération ou de deux opérandes d'affilée. L'article précédent décrivait un algorithme pour traduire une formule d'un SCR en un préfixe. Un signe de l'achèvement correct de cet algorithme est qu'à la dernière étape, la pile doit contenir une seule valeur. Si ce n'est pas le cas, alors une erreur a été commise. Et une autre situation erronée se produit dans le cas où la fonction est appelée avec le mauvais nombre (plus ou moins) de paramètres. Ces situations doivent être «prises»:

 (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)))))) 

Jetons maintenant un «regard critique» sur le gestionnaire d'instructions proc. Nous avons clairement raté deux points. La première chose à faire est de ne pas oublier lors du traitement de la procédure de calculer son arité (le nombre d'arguments) et de modifier la variable globale * oplist * en conséquence. Et la seconde est que les fonctions que nous générons ne renvoient pas la bonne valeur! Plus précisément, du fait des fonctions générées par notre traducteur, la valeur du dernier formulaire calculé avant le retour sera restituée. Pour garantir le retour de la valeur souhaitée, je propose de transférer la variable de résultat depuis Pascal. Maintenant, si nécessaire, retournez la valeur souhaitée, il suffit que l'utilisateur attribue la valeur souhaitée à cette variable avant de quitter la fonction, et lors de la génération du corps de la fonction, nous devons insérer le résultat du nom dans le corps de la fonction avec la dernière expression. Tout cela amène la fonction action-proc à:

 (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))) 

Nous nous arrêterons ici pour le moment (même si nous rencontrerons toujours des problèmes, et le code devra être finalisé; mais c'est le lot du programmeur ...) Et maintenant, nous allons considérer deux améliorations de notre langage qui sont appropriées à apporter dès maintenant.

Améliorations mineures ...


Dans un article précédent, j'ai écrit qu'il n'est pas pratique pour un programmeur si dans une langue un opérateur occupe exactement une ligne. Il est nécessaire de fournir la possibilité d'écrire des instructions volumineuses sur plusieurs lignes. Implémentons cela. Ce n'est pas du tout difficile à faire. Dans la procédure getLine, nous allons créer une variable locale dans laquelle nous accumulerons le texte lu (à condition que ce ne soit pas un commentaire et se termine par quelques caractères «_». Dès qu'une ligne significative avec une fin différente est fixée, nous renvoyons la valeur accumulée en tant que valeur. Voici le 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)))))) 

Et la dernière amélioration. Dans de nombreux langages de programmation, vous pouvez utiliser des opérandes logiques dans des expressions arithmétiques (qui dans ce cas sont calculées à zéro ou à un). Cela donne au langage une expressivité supplémentaire et, soit dit en passant, est tout à fait conforme à l'esprit de base. Dans notre mini-BASIC, une tentative de calcul de cette expression, par exemple, est:

 z=(x>y)*5+(x<=y)*10 

provoquera une erreur d'exécution. Et cela est compréhensible: en Lisp, l'expression (> xy) est calculée à Nil ou T. Mais Nil / T ne peut pas être multiplié par 5 ... Cependant, ce problème est facile à aider. Écrivons quelques macros simples qui remplacent le résultat des expressions de comparaison par 0/1 (au lieu de 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)) 

Maintenant, jetez un œil à la ligne de la fonction ipn2pref qui effectue le traitement de l'opération. Voici la ligne:

 (ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar))) 

Ici (voiture f) est le nom de l'opération. Écrivons une petite fonction pour remplacer les codes de comparaison:

 (defun chng-comp (op) (if (member op '(= == /= <> > < >= <=)) (implode (cons '$ (explode op))) op)) 

La fonction vérifie si son argument est une opération de comparaison et, si nécessaire, ajoute le caractère «$» au début. Maintenant, appelez-le au bon endroit de la fonction ipn2pref:

 (ipn2pref (cdr f) (cons (cons (chng-comp (car f)) (reverse (subseq s 0 ar))) (subseq s ar))) 

Quel sera le résultat? Les opérations de comparaison seront remplacées par des appels à la macro correspondante et toutes les autres opérations ne changeront pas. Si vous traduisez cette fonction:

 proc test() local x,y x=1 y=2 result=(x>y)*5+(x<=y)*10 end_proc 

puis appelez-le, nous obtenons le résultat attendu.

C'est tout pour aujourd'hui.

Le code de cet article se trouve ici.
À suivre.

Source: https://habr.com/ru/post/fr423663/


All Articles