上一篇文章错误,错误,错误...
好的程序应该受到保护,免受用户错误的影响。 这是绝对可以肯定的。 错误需要得到处理,甚至要得到更好的警告(预防总比治疗好!)。 特技飞行-与用户建立对话,使后者完全不会犯错误。
例如,如果用户需要在输入字段中输入正整数,那么您当然可以分析答案,并且如果找到非数字字符,则发出警告并要求用户重复输入。 但是,最好禁止使用非数字字符!
不幸的是,这种技术不能总是被应用。 特别是,输入翻译器输入的各种设计太大,以至于无法通过设置输入掩码简单地“切断错误的输入”。
一个人有犯错误的特权,在输入不正确的语言结构的情况下,翻译人员应做出明确的诊断,并在可能的情况下继续分析源文本以识别所有错误。 如果翻译员“一次一次”发现错误,用户可能会不太喜欢它。 而且,绝对不能接受程序出现系统错误消息而“崩溃”的情况。
在本文中,我们将严格地检查以前开发的代码,并尝试防止(处理)可能的错误。
让我们从第一个启动功能开始。 她在做什么 她采用输入文件的名称,将其打开,然后逐行处理。 对于此类程序,用户交互场景已“解决”-可以认为是规范的:
- 如果未指定文件名,则调用标准的“打开”对话框。
- 如果用户单击“打开”对话框中的“拒绝”按钮-关闭;
- 检查具有给定/输入名称的文件是否存在。 如果不存在,则发出一条消息并退出;
- 如果指定的文件存在,请对其进行处理。
我们的启动过程版本不满足这种情况。 实际上,请看下面的代码:
(defun start (&optional (fname "")) (setq *numline* 0) (setq *flagerr* nil) (setq *oplist* …)
用户的否定响应不会被分析,因此如果按下“拒绝”按钮,程序将“崩溃”。 文件的存在也不进行分析。 不幸的是,这种缺陷不仅限于缺点。 显然,如果mini-basic过程是输入文件中的最后一个程序,则在生成的函数加载到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*))
如果指定了文件名并且文件存在,则执行处理。 否则,将显示以下消息之一:“文件不存在”或“文件名省略”。
在主循环的主体中依次执行以下操作:
- 功能action-proc已实现。 其工作结果存储在局部变量curr-proc中;
- 如果* flagerr *标志被引发,则循环中断;
- 如果action-proc函数返回非空结果,则将生成的函数加载到Lisp环境中;否则,将其加载到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 ****
我们假设我们已经处理了启动功能。 但是“漏洞的工作”才刚刚开始。 让我们看一下已经实现的那部分语言的语法。
人们最常犯的最常见语法错误是不正确的括号结构(不平衡或括号顺序错误)。 回想一下一个小型基础程序的源代码行发生了什么情况。 解析该字符串(将其分解为令牌),然后将令牌列表转换为内部列表形式。 在标记列表中,括号是单独的标记,我们不检查其余额。 这可以作为单独的函数完成,但是令牌列表将传输到输入函数的输入,输入函数会将行列表转换为Lisp列表。 如果将错误的字符串表达式传递给输入函数输入,则该函数将返回错误。
让我们处理这个错误。
在HomeLisp中,构造用于处理错误(尝试使用Expression-1(Expression-1除外))。 其工作方式如下:
- 试图计算Expression-1。 如果尝试成功,则将计算结果作为整个try表单的结果返回;否则,将返回计算结果。
- 如果发生错误,则计算Expression-2。 同时,没有参数(错误消息)的系统功能可用,该函数返回错误消息的文本。
基于上述内容,可以按以下方式发送到列表的转移:
(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) ))))
如果发生转换错误,将发出系统消息,结果将返回一个元素的列表-原始代码行。 此外,此列表(作为下一条语句)将落入action-proc过程中。 并且,当然,它不会被识别。 这将生成另一个错误消息,并且编译器将继续工作。 我们将准备以下源代码,并尝试对其进行翻译:
* * * 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运算符。 怎么了 一元减! 将公式从中缀形式转换为前缀形式,我们不知何故认为减号是“两面的”-存在二进制减号(运算符),并且存在一元减号(数字符号)。 我们的解析器不知道这种区别-它认为所有缺点都是二进制的...现在该怎么办? 为了不破坏已经工作的代码,让我们将所有一元缺点变成二进制。 怎么了 但是很简单。 很明显,一元减号仅在以下结构中“存在”:
“(-某事”
“>-某事”
“ <某事”
“ =某事”
好吧,在公式的开始,他也可以见面。 因此,如果在分成代币之前,我们执行以下替换:
“(-Something” =>“(0-something”
“>-某物” =>“> 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转换为前缀一的阶段“捕获”了这些错误。 并且(以防万一),我们将捕获将公式从中缀转换为前缀的过程中可能出现的所有错误。 最好的地方是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语句处理程序进行“批判性研究”。 我们显然错过了两点。 要做的第一件事是在处理过程以计算其Arity(参数数)并相应地修改全局变量oplist *时不要忘记。 其次,我们生成的函数未返回正确的值! 更准确地说,由于我们的翻译器生成的函数的结果,将返回返回之前计算出的最后一个表单的值。 为了保证期望值的返回,我建议从Pascal传递结果变量。 现在,如果有必要,返回期望值,用户在退出函数之前为该变量分配期望值就足够了,并且在生成函数主体时,我们需要在函数主体中插入最后一个表达式作为结果名称。 所有这些使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)))
我们现在将在这里停止(尽管我们仍然会遇到问题,并且必须完成代码;但这是程序员的大部分工作……)现在,我们将考虑对我们的语言进行两项改进,以适合现在进行。
小改进...
在上一篇文章中,我写道如果程序员用一种语言仅占用一行代码,这对于程序员是不方便的。 有必要提供在多行上编写大容量语句的功能。 让我们实现它。 这一点都不难做到。 在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)))
这里(car f)是操作的名称。 让我们编写一个微型函数来替换比较代码:
(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
然后调用它,我们得到了预期的结果。
今天就这些了。
本文的代码位于
此处。待续。