Artículo anteriorErrores, errores, errores ...
Un buen programa debe estar protegido de los errores del usuario. Esto es absolutamente cierto. Los errores deben ser manejados e incluso mejor advertidos (¡prevenir es siempre mejor que curar!). Acrobacias aéreas: construya un diálogo con el usuario para que este último simplemente no pueda cometer un error.
Por ejemplo, si el usuario necesita ingresar un número entero positivo en el campo de entrada, entonces, por supuesto, puede analizar la respuesta y, si encuentra caracteres no numéricos, dar una advertencia y pedirle al usuario que repita la entrada. ¡Pero es mucho mejor simplemente prohibir caracteres no numéricos!
Desafortunadamente, tal técnica no siempre se puede aplicar. En particular, la variedad de diseños que ingresan la entrada del traductor es demasiado grande para simplemente "cortar los incorrectos" configurando la máscara de entrada.
Una persona tiene el privilegio de cometer errores, y el traductor debe, en el caso de ingresar construcciones de lenguaje incorrectas, dar un diagnóstico claro y, si es posible, continuar analizando el texto fuente para identificar todos los errores. Probablemente al usuario no le gustará realmente si el traductor detecta los errores "uno a la vez". Y es absolutamente inaceptable reconocer una situación en la que un programa "falla" con un mensaje de error del sistema.
En este artículo, revisaremos críticamente el código previamente desarrollado e intentaremos evitar (procesar) posibles errores.
Comencemos con la primera función de inicio. Que esta haciendo ella Toma el nombre del archivo de entrada, lo abre y procesa línea por línea. Para tales programas, el escenario de interacción del usuario ya se ha "establecido"; puede considerarse canónico:
- Si no se especifica el nombre del archivo, llame al cuadro de diálogo estándar "Abrir";
- Si el usuario hizo clic en el botón "rechazar" en el cuadro de diálogo "Abrir", cierre;
- Verifique si el archivo con el nombre dado / ingresado existe. Si no existe, emita un mensaje y salga;
- Si el archivo especificado existe, procesarlo.
Nuestra versión del procedimiento de inicio no satisface este escenario. De hecho, mira el siguiente código:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …)
La respuesta negativa del usuario no se analiza, por lo que si se presiona el botón "rechazar", el programa se "bloqueará". La existencia del archivo tampoco se analiza. Lamentablemente, este defecto no se limita a las deficiencias. Obviamente, si el procedimiento mini-básico es el último en el archivo de entrada, el análisis del final del archivo hará que el ciclo se rompa antes de que la función generada se cargue en el entorno Lisp.
Corrija estos defectos:
(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 se especifica el nombre del archivo y el archivo existe, se realiza el procesamiento. De lo contrario, se imprime uno de los mensajes: "El archivo no existe" o "Nombre de archivo omitido".
Las siguientes acciones se realizan secuencialmente en el cuerpo del bucle principal:
- La función action-proc se cumple. El resultado de su trabajo se almacena en la variable local curr-proc;
- Si se levanta la bandera * flagerr *, el bucle se rompe;
- Si la función action-proc devolvió un resultado no vacío, la función generada se carga en el entorno Lisp;
- Si se alcanza el final del archivo, el ciclo también se rompe.
El código parecía ser mejor ... Pero otra falla grave permaneció sin resolver: después de que se complete el procesamiento del procedimiento que contiene uno o más errores, el bucle principal se interrumpirá y el programa finalizará sin mirar la parte del año original ubicada detrás del procedimiento con errores. Esto es malo: me gustaría que el traductor produzca todos los errores que se pueden detectar en cada inicio.
Para corregir este defecto, introduzcamos la variable global "contador de errores", mientras procesamos el procedimiento con errores, aumentaremos este contador. Y el indicador de error se restablecerá después de procesar cada procedimiento:
(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*))
Ahora, la función de inicio funcionará aceptable. Asegurémonos de esto. Cree el siguiente archivo fuente:
* * * 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
Y "déjalo pasar" a través de nuestro traductor. Obtenemos:
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 ****
Asumimos que nos enfrentamos con la función de inicio. Pero el "trabajo sobre los errores" acaba de comenzar. Veamos la sintaxis de esa parte del lenguaje que ya hemos implementado.
Probablemente, el error de sintaxis más común que las personas hacen con mayor frecuencia es una estructura de paréntesis incorrecta (paréntesis desequilibrados o en el orden incorrecto). Recuerde lo que le sucede a una línea de código fuente para un programa mini-básico después de leerlo. La cadena se analiza (se divide en tokens) y luego la lista de tokens se traduce en un formulario de lista interna. En la lista de tokens, los paréntesis son tokens separados y no verificamos su saldo. Esto podría hacerse como una función separada, pero la lista de tokens se transmite a la entrada de la función de entrada, que traduce la lista de líneas en la lista Lisp. Si se pasa una expresión de cadena incorrecta a la entrada de la función de entrada, la función devolverá un error.
Manejemos este error.
En HomeLisp, se utiliza una construcción para manejar errores (pruebe Expression-1 excepto Expression-1). Funciona de la siguiente manera:
- Se intenta calcular la Expresión-1. Si el intento es exitoso, el resultado del cálculo se devuelve como el resultado del formulario de prueba completo;
- Si se produce un error, se calcula la Expresión-2. Al mismo tiempo, está disponible una función del sistema sin parámetros (mensaje de error), que devuelve el texto del mensaje de error.
Con base en lo anterior, la transferencia al formulario de lista se puede emitir de la siguiente manera:
(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 caso de un error de conversión, se emitirá un mensaje del sistema y, como resultado, se devolverá una lista de un elemento: la línea de código original. Además, esta lista caerá (como la siguiente declaración) en el procedimiento action-proc. Y, por supuesto, no será reconocido. Esto generará otro mensaje de error y el compilador continuará funcionando. Prepararemos el siguiente código fuente e intentaremos traducirlo:
* * * 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
Obtenemos el resultado esperado:
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 ****
Ahora echemos un vistazo crítico al código que convierte las expresiones aritméticas en una notación de prefijo. Este código no contiene ningún medio para corregir los errores del usuario. Desafortunadamente, estos errores pueden ser bastante. Arreglemos este error. Para comenzar, intentemos traducir un código completamente inocente (en apariencia):
proc test() local x,y x=6 y=-x print y end_proc
¡La transmisión terminará con la "caída" del traductor! Una caída causará el operador y = -x. Cual es el problema En un unario menos! Al convertir una fórmula de una forma de infijo a una de prefijo, de alguna manera no pensamos que menos "dos caras" - hay un binario menos (un signo de operación), y hay un menos unario (un signo de un número). Nuestro analizador no conoce esta diferencia: considera que todos los inconvenientes son binarios ... ¿Qué hacer ahora? Para no destruir el código que ya funciona, vamos a convertir todos los inconvenientes unarios en binarios. Como? Pero muy simple. Es bastante obvio que el unario menos "vive" solo en tales construcciones:
"(-Algo"
"> -Algo"
"<-Algo"
"= Algo"
Bueno, al comienzo de la fórmula, también puede encontrarse. Por lo tanto, si, antes de dividir en tokens, realizamos los siguientes reemplazos:
"(-Algo" => "(0-algo"
"> -Algo" => "> 0-algo"
"<-Algo" => "<0-algo"
“= Algo” => “= 0 algo”
y si la fórmula comienza con un signo menos, asignamos cero al comienzo de la fórmula, entonces todas las desventajas se volverán binarias y el error se eliminará radicalmente. Llamemos a la función que realizará la conversión sobre el nombre prepro. Así es como podría verse:
(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))
No se requieren comentarios especiales aquí. Pero nuestro analizador simple tiene otro problema que no es del todo evidente a primera vista: dos signos de operaciones. Cuando se trabaja con fórmulas, los signos ">" y "=" parados uno al lado del otro significan una operación "> =" (¡y debe ser una ficha!). El analizador no quiere saber esto, hará que cada uno de los signos sea una ficha separada. Puede hacer frente a este problema mirando la lista de tokens recibidos, y si los caracteres correspondientes están uno al lado del otro, combinándolos. Nombramos la función que realizará la unión con el nombre "postpro". Aquí está el código para una posible implementación:
(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)))))))))
Además, como vemos, nada especial. Pero ahora la función final de traducir el operador al formulario de lista interna se verá así:
(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) ))))
Ahora echemos un vistazo crítico a la función inf2ipn. ¿Qué errores del usuario pueden "culparlo"? Ya hemos cortado el desequilibrio de los corchetes de arriba. ¿Qué podría ser más? Dos signos de operación o dos operandos, de pie en una fila. Uno podría analizar esto en el código inf2ipn (y aquellos que lo deseen pueden hacerlo por su cuenta). "Capturamos" estos errores en la etapa de convertir la fórmula del SCR al prefijo uno. Y vamos (por si acaso) detectaremos todos los errores que puedan surgir en el proceso de convertir la fórmula de infijo a prefijo. El mejor lugar para esto es la función de envoltura i2p. Ahora podría verse así:
(defun i2p (f) (try (ipn2pref (inf2ipn f)) except (progn (printsline "**** ") (printsline (strCat "**** " (errormessage))) (setq *flagerr* t) nil)))
Y ahora evitaremos la aparición en las fórmulas de dos signos de operación o dos operandos seguidos. El artículo anterior describió un algoritmo para traducir una fórmula de un SCR a una forma de prefijo. Una señal de la finalización correcta de este algoritmo es que en el último paso la pila debe contener un solo valor. Si esto no es así, entonces se ha cometido un error. Y surge otra situación errónea en el caso cuando la función se llama con el número incorrecto (más o menos) de parámetros. Estas situaciones deben ser "atrapadas":
(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))))))
Ahora echemos un "vistazo crítico" al manejador de declaraciones de proceso. Claramente perdimos dos puntos. Lo primero que debe hacer es no olvidar cuando procesa el procedimiento para calcular su aridad (el número de argumentos) y modificar la variable global * oplist * en consecuencia. ¡Y el segundo es que las funciones que generamos no devuelven el valor correcto! Más precisamente, como resultado de las funciones generadas por nuestro traductor, se devolverá el valor de la última forma calculada antes de regresar. Para garantizar el retorno del valor deseado, propongo transferir la variable de resultado de Pascal. Ahora, si es necesario, devuelva el valor deseado, es suficiente para que el usuario asigne el valor deseado a esta variable antes de salir de la función, y al generar el cuerpo de la función, necesitamos insertar el resultado del nombre en el cuerpo de la función con la última expresión. Todo esto lleva la función action-proc a:
(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)))
Nos detendremos aquí por ahora (aunque todavía encontraremos problemas, y el código tendrá que ser finalizado; pero tal es la tarea del programador ...) Y ahora consideraremos dos mejoras en nuestro lenguaje que son apropiadas para hacer en este momento.
Pequeñas mejoras ...
En un artículo anterior, escribí que es inconveniente para un programador si en un idioma un operador ocupa exactamente una línea. Es necesario proporcionar la capacidad de escribir declaraciones voluminosas en varias líneas. Implementemos esto. Esto no es del todo difícil de hacer. En el procedimiento getLine, crearemos una variable local en la que acumularemos el texto leído (siempre que no sea un comentario y termine con un par de caracteres "_". Tan pronto como se arregle una línea significativa con un final diferente, devolveremos el valor acumulado como un valor. Aquí está el código:
(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))))))
Y la última mejora. En muchos lenguajes de programación, puede usar operandos lógicos en expresiones aritméticas (que en este caso se calculan a cero o uno). Esto le da al lenguaje una expresividad adicional y, por cierto, es bastante consistente con el espíritu básico. En nuestro mini-BASIC, un intento de calcular esta expresión, por ejemplo, es:
z=(x>y)*5+(x<=y)*10
provocará un error de tiempo de ejecución. Y esto es comprensible: en Lisp, la expresión (> xy) se calcula como Nil o T. Pero Nil / T no se puede multiplicar por 5 ... Sin embargo, este problema es fácil de ayudar. Escribamos algunas macros simples que reemplacen el resultado de las expresiones de comparación con 0/1 (en lugar 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))
Ahora, eche un vistazo a la línea en la función ipn2pref que realiza el procesamiento de la operación. Aquí está la línea:
(ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar)))
Aquí (carro f) es el nombre de la operación. Escribamos una pequeña función para reemplazar los códigos de comparación:
(defun chng-comp (op) (if (member op '(= == /= <> > < >= <=)) (implode (cons '$ (explode op))) op))
La función verifica si su argumento es una operación de comparación y, si es necesario, agrega el carácter "$" al principio. Ahora llámelo en el lugar correcto de la función ipn2pref:
(ipn2pref (cdr f) (cons (cons (chng-comp (car f)) (reverse (subseq s 0 ar))) (subseq s ar)))
¿Cuál será el resultado? Las operaciones de comparación serán reemplazadas por llamadas a la macro correspondiente, y todas las demás operaciones no cambiarán. Si traduces esta función:
proc test() local x,y x=1 y=2 result=(x>y)*5+(x<=y)*10 end_proc
y luego lo llamamos, obtenemos el resultado esperado.
Eso es todo por hoy.
El código para este artículo se encuentra
aquí.Continuará