我们正在用Lisp-II写一个简单的翻译器

上一篇文章

我们实现赋值运算符


现在让我们教译者如何处理赋值运算符。 在这里,我们面临着一项经典的任务,即确保计算自从我们学年以来就熟悉的符号形式的代数公式。 如果要翻译,那么我们需要计算公式的值。 在我们的例子中,Lisp内核将进行计算(在运行时)。 我们只需要将公式从通常的表示法转换为Lisp。
我们熟悉的符号称为“中缀符号”(操作符号位于操作数之间)。 在Lisp中,运算符放置在运算数之前(这种表示法称为“前缀表示法”)。 因此,我们的任务是将中缀形式转换为前缀一。

您可以通过不同方式解决此问题...

我建议将公式转换为所谓的 “反波兰形式”(SCF)。 波兰语反符号(以波兰数学家卢卡谢维奇命名)是一种非阻塞形式的符号,其中运算符号位于操作数之后(“后缀符号”)。 从后缀到前缀形式的转换非常简单。 一个人可以“一举解决问题”-立即执行从中缀到前缀的转换。 但是这个决定会比较麻烦。 但是,那些希望的人可以尝试自己做。

我们将从事将公式转换为SCR的工作。 在输入处,我们以中缀符号表示了一个代数公式,以Lisp多级列表的形式表示。 例如,这一个:

(12 + x / ( y ^ 2 + z ^ 4))

在SCR中,此表达式将具有以下形式(乍一看-奇怪):

(12 xy 2 ^ z 4 ^ + / +)

要以SCR的形式计算公式的值,您需要一个堆栈(一种数据结构,该结构按照“后进先出”的原则存储和传递数据)。 计算非常简单。 该列表被读取一次。 对于每个元素,将执行以下操作:

  • (变量值的数量)被简单地压入堆栈;
  • 从堆栈顶部开始,在相应数量的操作数上执行该操作。

请注意,SCF中没有括号,并且操作按其写入的顺序执行(优先级(如中缀形式,此处不再赘述))。

我们要转换为SCR的表达式可能包含数字,变量,函数和运算符。 有一个问题-如何区分变量和函数? 解决此问题的一种自然方法是列出所有操作和函数,并检查此列表上所需的字符:如果在列表中找到该字符,则为操作,否则为变量。
此外,对于每个函数/操作,您都需要存储其Arity (参数数量)。 基本的操作列表如下所示:

 (setq *oplist* '((+ 2) (- 2) (* 2) (/ 2) (^ 2) (\ 2) (% 2) (= 2) (== 2) (/= 2) (> 2) (>= 2) (< 2) (<= 2) (and 2) (or 2) (not 1) (sin 1) (cos 1) (abs 1) (exp 1) (log 1) (sqrt 1))) 

应该注意的是,在翻译过程中,这个列表可能会增加。 事实是,微型基本函数返回值并可以参与表达式。 这些函数的名称及其名称必须添加到* oplist *列表中。 这可以在处理proc语句的分支中的action-proc过程中完成。 * oplist *变量可以在启动过程中创建(并在完成前销毁)。 并可以在action-proc代码中添加一个函数,如下所示:

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

现在,必须对功能中发生的每个操作设置一定的优先级。 优先级由以下功能设置:

 (defun prty (OP) (cond ((EQ OP 'and) 1) ((EQ OP 'or) 1) ((EQ OP '>) 2) ((EQ OP '>=) 2) ((EQ OP '<) 2) ((EQ OP '<=) 2) ((EQ OP '=) 2) ((EQ OP '==) 2) ((EQ OP '/=) 2) ((EQ OP '+) 3) ((EQ OP '-) 3) ((EQ OP '*) 4) ((EQ OP '/) 4) ((EQ OP '\) 4) ((EQ OP '%) 4) ((EQ OP '^) 5) ((member op (mapcar 'car *oplist*)) 6))) 

逻辑运算“ and”和“ or”的优先级最低。 然后是比较操作,然后是加法和减法,等等。 功能具有最高优先级。

现在我们描述在SCR中翻译表达式的算法。 inf2ipn函数接受一个必需参数(输入公式)和两个可选参数(操作堆栈和累加器列表)。 在电池列表中,结果被累加。 该函数扫描输入列表,并执行以下操作:

  • 如果它的下一个元素是数字或变量,则将其输入电池。
  • 如果下一个元素是列表,则将该函数递归应用于此列表,并将其结果添加到电池中。
  • 如果下一个元素是一个操作,则在操作堆栈为空的情况下,将下一个元素压入操作堆栈。 使用非空操作堆栈时,会将传入操作的优先级与操作堆栈的顶部进行比较。 如果较高优先级的操作到达,则将其推入操作堆栈。
  • 如果到达的优先级不大于堆栈顶部的操作,则将操作堆栈的顶部转移到累加器,并将新到达的操作输入操作堆栈。
  • 如果输入列表已用完,并且操作堆栈为空,则该函数返回一个反向电池(端子分支)。 否则,该函数将返回一个反向电池,该电池具有先前从堆栈中附加的操作列表。

区分操作和操作数的函数非常简单-归结为检查给定字符是否在全局* oplist *列表中:

 (defun is-op (o) (member o (mapcar 'car *oplist*))) 

SCR中的传递函数具有以下形式:

 (defun inf2ipn (f &optional (s nil) (r nil)) (cond ((null f) (if (null s) (reverse r) (inf2ipn nil (cdr s) (cons (car s) r)))) ((listp (car f)) (inf2ipn (cdr f) s (append (reverse (inf2ipn (car f))) r))) ((numberp (car f)) (inf2ipn (cdr f) s (cons (car f) r))) ((not (is-op (car f))) (inf2ipn (cdr f) s (cons (car f) r))) (t (cond ((null s) (inf2ipn (cdr f) (cons (car f) s) r)) ((> (prty (car f)) (prty (car s))) (inf2ipn (cdr f) (cons (car f) s) r)) (t (let ((a (car s))) (inf2ipn (cdr f) (cons (car f) (cdr s)) (cons ar)))))))) 

您可以通过直接调用此函数来验证其功能:

 (inf2ipn '(2 + 3 * 6)) ==> (2 3 6 * +) (inf2ipn '((2 + 3) * 6)) ==> (2 3 + 6 *) (inf2ipn '(3 + a * sin ( 5 + x))) ==> (3 A 5 X + SIN * +) 

从SCR获得前缀条目非常简单。 ipn2inf函数接受SCR中的表达式和drive参数。 该函数的工作方式如下:

  • 如果输入列表为空,则返回驱动器磁头。
  • 如果下一个元素是数字或变量,则此原子连接到驱动器;
  • 如果下一个元素是对数n的操作,则将由该操作的符号和长度为n的驱动器的反向段组成的列表添加到没有前n个元素的驱动器上。

这是代码中的样子:

 ;;    (defun arity (o) (iter (for a in *oplist*) (when (eq o (car a)) (return (cadr a))))) ;;      (defun ipn2pref (f &optional (s nil)) (cond ((null f) (car s)) ((numberp (car f)) (ipn2pref (cdr f) (cons (car f) s))) ((is-op (car f)) (let ((ar (arity (car f)))) (ipn2pref (cdr f) (cons (cons (car f) (reverse (subseq s 0 ar))) (subseq s ar))))) (t (ipn2pref (cdr f) (cons (car f) s))))) ;; -,      (defun i2p (f) (ipn2pref (inf2ipn f))) 

检查代码的运行状况:

 (i2p '(3 + a * sin ( 5 + x))) ==> (+ 3 (* A (SIN (+ 5 X)))) (i2p '((3 + a) * sin ( 5 ) + x)) ==> (* (+ 3 A) (+ (SIN 5) X)) (i2p '((3 + a) * sin ( 5 ^ 2 - x ) + x)) ==> (* (+ 3 A) (+ (SIN (- (^ 5 2) X)) X)) 

现在只需编写赋值运算符的处理程序并将其连接到过程处理程序即可。 分配处理程序可以如下实现:

 (defun action-set (meat) (let ((name-var (car meat)) (r-value (i2p (cddr meat)))) `(setq ,name-var ,r-value))) 

meat参数应该引用列表分配:

 ( = ) 

识别操作符是在action-proc函数中完成的,其形式为:

 (defun action-proc (fi) (let ((stmt nil) (proc-name nil) (proc-parm nil) (loc-var nil) (lv nil) (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))) ((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)) `(defun ,proc-name ,proc-parm (let ,lv ,@body)))) 

让我们检查代码的性能。 我们将代码加载到Lisp环境中,调用start函数并转换以下过程:

0001 proc main()
0002 local x,y,z
0003 x=3
0004 y=4
0005 z=x^2+y^2
0006 print z
0007 end_proc


让我们看看我们的翻译器生成了什么:

 (getd 'main) ==> (EXPR NIL (LET ((X 0) (Y 0) (Z 0)) (SETQ X 3) (SETQ Y 4) (SETQ Z (+ (^ X 2) (^ Y 2))) (PRINTLINE Z))) 

一切似乎都是正确的。 现在,让我们调用过程并获得预期的结果:

 (main) 25 ==> 25 

我们的翻译人员还将正确处理内置函数。 为了验证这一点,我们将执行例如以下代码:

0001 proc main()
0002 local x,y,z,pi
0003 pi=3.1415926535
0004 x=sin(pi/6)
0005 y=cos(pi/6)
0006 z=x^2+y^2
0007 print x
0018 print y
0019 print z
0010 end_proc


我们得到:

 (main) 0.499999999987039 0.866025403791922 1.0 ==> 1.0 

我们的翻译在我们眼前变得栩栩如生!

但是,我们非常高兴:为实现最终目标而努力,我们根本没有考虑用户(使用mini-basic的程序员)可能犯的错误。 以一种很好的方式,我们必须立即考虑错误,但是我们只是开始工作,没有走太远,并且在代码中引入错误处理和诊断还为时不晚。 此外,很明显,建议“稍作改进”(例如,我们的翻译要求操作员只占用一行,这很不方便)。

以下文章将专门讨论所有这些问题。
待续

可以在此处下载本文的代码

Source: https://habr.com/ru/post/zh-CN421445/


All Articles