如何在用C编写的类似FORTH的语言解释器中实现LOOP

我在C中编写一个简单的基于堆栈的语言,并且想知道我应该如何实现某种循环结构和/或前瞻符号。 由于此页面的代码有点长(超过200行),我将它放在GitHub存储库中 。

编辑:主程序在文件stack.c

编辑:代码只是输入words ,类似于FORTH。 它使用scanf并从左到右工作。 然后它使用一系列ifstrcmp来决定做什么。 真的是这样。

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条件重启评估字。 也就是说,将它作为正常单词添加到您的rangejump ,让它弹出堆栈,如果堆栈的顶部为真,则将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

我已经改变了主意,完全依赖于没有这种循环词的递归。

希望有所帮助,玩得开心!