如何在用C编写的类似FORTH的语言解释器中实现LOOP
我在C中编写一个简单的基于堆栈的语言,并且想知道我应该如何实现某种循环结构和/或前瞻符号。 由于此页面的代码有点长(超过200行),我将它放在GitHub存储库中 。
编辑:主程序在文件stack.c
。
编辑:代码只是输入words
,类似于FORTH。 它使用scanf
并从左到右工作。 然后它使用一系列if
和strcmp
来决定做什么。 真的是这样。
Forth方法是在数据堆栈旁边添加一个单独的循环堆栈。 然后定义使用此循环堆栈的操作。 例如:
5 0 DO I . LOOP
会打印
0 1 2 3 4
这种方式的工作原理是:
-
DO
将索引(0)和控制(5)移动到循环堆栈。 -
I
将循环堆栈的顶部复制到数据堆栈。 -
LOOP
递增索引(循环堆栈的顶部)。 如果索引小于控件(低于循环堆栈顶部的索引),则它会将DO
的命令重新运行回LOOP
。 如果索引> =,则它从循环堆栈中弹出索引和控件,并且控制恢复正常。
将控制结构添加到基于堆栈的语言的显而易见的方法是添加“控制堆栈”。 我将描述Postscript机制,因为这就是我所知道的,但Forth有一些类似的行为(确切地说有微妙的差异)。
最简单的受控循环是repeat
循环。 从技术上讲,无限loop
更简单,但要使用它需要一个显式的exit
命令,并解释这会使事情复杂化。
repeat的语法是:
int proc **repeat** -
因此,当repeat
命令开始时,它期望在操作数堆栈的顶部找到一个过程(这是要执行的循环体),以及一个刚好在过程下面的整数(执行过程的次数)。
由于还有一个执行堆栈(我认为Forth称之为“返回”堆栈),命令可以通过在执行堆栈上按下它们来“执行”其他命令,从而安排其他命令在当前命令完成后立即执行,但在恢复当前命令的调用者之前。 这是一个很长的句子,但关键的想法就在那里。
作为具体示例,假设解释器正在从输入流执行。 堆栈看起来像这样:
operand: -empty- execute:
用户键入2 {(Hello World!\n) print} repeat
。
整数2被推入堆栈。
operand: 2 execute:
引用的(*)过程体被推入堆栈。
operand: 2 {(Hello World!\n) print} execute:
执行repeat
命令。
operand: 2 {(Hello World!\n) print} execute: repeat Repeat: expects operands: int proc if int<0 Error if int==0 return //do nothing push `repeat` itself on exec stack push quoted proc on exec stack push int-1 on exec stack push "executable" proc on exec stack
执行重复过程(一次)会像这样离开堆栈:
operand: -empty- execute: repeat {(Hello World!\n) print} 1 **{(Hello World!\n) print}**
解释器在exec堆栈的顶部执行过程,打印“Hello World!”,然后找到一个整数,它将其推送到op堆栈。
operand: 1 execute: repeat {(Hello World!\n) print}
解释器通过按下op栈来执行引用的过程。
operand: 1 {(Hello World!\n) print} execute: repeat
我们回到了开始! 为下一次迭代做好准备(如果整数降至零,则终止)。
希望这可以帮助。
编辑;
在实际查看你的代码之后,我有一个不同的建议,也许是我上面描述的类似的跳板。 我认为你应该暂时忘记代码并专注于数据结构。 你有一个很好的变量表,但是所有命令都是通过代码分散的嵌入式文字!
如果使表保持变量记录类型,则可以对两者使用相同的查找机制(甚至可以移动到散列或三元搜索树(我当前最喜欢的))。 我记得这样的事情:
struct object { int type; union { int i; void (*command)(context *); } u; }; struct dict { int sz; struct rec { char *key; object val; } data[]; //think resizable! };
这样每个命令都有自己的function。 奖金是:小function。 您应该尝试使您的function足够小,以便您可以同时在屏幕上看到整个事物。 一次扫描整个事物可以让你的右脑做一些检测问题区域的工作。
然后你需要另一种类型来保存命令序列。 数组或列表应该有效。 当您可以定义序列时,您可以更轻松地重复序列。
这里的奖励是你只有一个远离图灵完成的构造。 序列,迭代,决策(或选择):这就是编写任何可计算函数所需的全部内容!
*。 正如评论员所发现的那样,Postscript实际上并没有我在描述中使用的引用概念。 在这里,我借用了Lisp术语中的概念。 Postscript有一个文字/可执行标志,可以设置cvlit
或测试xcheck
。 将执行执行堆栈上的可执行数组。 因此对于“引用的”过程体,它实际上是一个文字过程体(即一个数组)。 因此, repeat
必须还要在下一次迭代之前执行对cvx
的调用。 因此, repeat
的postscript实现的更正确的伪代码是:
Repeat: expects operands: int proc if int<0 Error if int==0 return //do nothing push `repeat` itself on exec stack push 'cvx' on the exec stack push cvlit(proc) on exec stack push int-1 on exec stack push "executable" proc on exec stack
这将执行必要的标志操作以将过程作为执行堆栈上的数据传递。
我看到这种控制结构实现的另一种方式是使用两个函数, repeat
相同的入口点,以及一个理论上不需要名称的内部延续运算符。 我认为ghostscript称之为@ repeat-continue @。 使用单独的continue函数,您不必非常小心堆栈中的东西顺序,并且您不需要旋转文字标记。 相反,您可以在exec堆栈上的递归调用下存储一些持久数据; 但你也要清理它。
所以另一个repeat
是:
int proc **repeat** - if int<0 Error if int==0 return //do nothing push null on exec stack <--- this marks our "frame" push int-1 on exec stack push proc on exec stack push '@repeat-continue' on exec stack push executable proc on exec stack
延续也有一个更简单的工作。
@repeat-continue peek proc from exec stack peek int from exec stack if int==0 clear-to-null and return push '@repeat-continue' on exec stack push executable proc on exec stack
最后, exit
运算符与循环密切相关,它将exec堆栈清除到循环运算符的“框架”。 对于双function样式,这是一个null
对象或类似对象。 对于单function样式,这是循环运算符本身。
你的语言根本不像Forth,所以不要复制Forth(仅限编译 – 对你的语言来说毫无意义的区别)循环结构。
查看代码,添加until
条件重启评估字。 也就是说,将它作为正常单词添加到您的range
并jump
,让它弹出堆栈,如果堆栈的顶部为真,则将stack.c的cmd
设置回到开头。
0 dup . 1 + dup 5 > until .
在您的语言中,将产生输出0 1 2 3 4 5 6
,跨三次评估,并多次重新评估第二行。 这假设您在整个评估期间保留状态,并且该评估是面向行的。 您可以挖掘LSE64以获得更多这样的想法。
这是一篇博文,其中DO / LOOP,BEGIN / UNTIL,WHILE / REPEAT等在我的小型TransForth项目中得到了肯定: http : //blogs.msdn.com/b/ashleyf/archive/2011/02/06/ loopty-DO-I-loop.aspx
我已经改变了主意,完全依赖于没有这种循环词的递归。
希望有所帮助,玩得开心!