我正在用 C 编写一种简单的基于堆栈的语言,并且想知道我应该如何实现某种循环结构和/或前瞻符号。由于此页面的代码有点长(超过 200 行),我将其放在GitHub 存储库中。
编辑:主程序在文件中stack.c
。
编辑:代码只接受输入words
,有点像 FORTH。它scanf
从左到右使用和工作。然后它使用一系列if
s 和strcmp
s 来决定要做什么。就是这样。
我正在用 C 编写一种简单的基于堆栈的语言,并且想知道我应该如何实现某种循环结构和/或前瞻符号。由于此页面的代码有点长(超过 200 行),我将其放在GitHub 存储库中。
编辑:主程序在文件中stack.c
。
编辑:代码只接受输入words
,有点像 FORTH。它scanf
从左到右使用和工作。然后它使用一系列if
s 和strcmp
s 来决定要做什么。就是这样。
Forth 方法是在数据堆栈旁边添加一个单独的循环堆栈。然后,您定义使用此循环堆栈的操作。例如:
5 0 DO I . LOOP
将打印
0 1 2 3 4
它的工作方式是:
DO
将索引 (0) 和控制 (5) 移动到循环堆栈。I
将循环堆栈的顶部复制到数据堆栈。LOOP
增加索引(循环堆栈的顶部)。如果索引小于控件(循环堆栈顶部下方的一个),则它重新运行从DO
返回到的命令LOOP
。如果索引>=,则从循环堆栈中弹出索引和控制,控制恢复正常。将控制结构添加到基于堆栈的语言的明显方法是添加“控制堆栈”。我将描述 Postscript 机制,因为这是我所知道的,但是 Forth 有一些类似的行为(当然,有细微的差别)。
最简单的受控循环是repeat
循环。从技术上讲,无限loop
更简单,但要使用它需要明确的exit
命令,并且解释会使事情复杂化。
重复的语法是:
int proc **repeat** -
因此,当repeat
命令开始时,它希望在操作数堆栈的顶部找到一个过程(这是要执行的循环体),并在该过程的正下方找到一个整数(执行该过程的次数)。
由于还有一个执行堆栈(我认为 Forth 将其称为“返回”堆栈),一个命令可以通过将其他命令压入执行堆栈来“执行”其他命令,从而调度其他命令在当前命令完成后立即执行,但在恢复当前命令的调用者之前。这是一个很长的句子,但关键的想法就在那里。
作为一个具体的例子,假设解释器是从输入流中执行的。堆栈看起来像这样:
operand: -empty-
execute: <stdin>
用户类型2 {(Hello World!\n) print} repeat
。
整数 2 被压入堆栈。
operand: 2
execute: <stdin>
引用的(*)过程主体被压入堆栈。
operand: 2 {(Hello World!\n) print}
execute: <stdin>
repeat
命令被执行。
operand: 2 {(Hello World!\n) print}
execute: <stdin> 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: <stdin> repeat {(Hello World!\n) print} 1 **{(Hello World!\n) print}**
解释器在 exec 堆栈顶部执行过程,打印“Hello World!”,然后找到一个整数,并将其压入操作堆栈。
operand: 1
execute: <stdin> repeat {(Hello World!\n) print}
解释器通过压入操作堆栈来执行引用的过程。
operand: 1 {(Hello World!\n) print}
execute: <stdin> 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!
};
这样每个命令都有自己的功能。好处是:小功能。您应该尝试使您的功能足够小,以便您可以同时在屏幕上看到整个内容。一次扫描整个事情可以让你的右脑做一些检测问题区域的工作。
然后你需要另一种类型来保存命令序列。数组或列表应该可以工作。当您可以定义序列时,您可以更轻松地重复序列。
这里的好处是你离图灵完备只有一个构造。序列、迭代、决策(或选择):这就是编写任何可计算函数所需的全部内容!
*。正如评论员所发现的,Postscript 实际上与我在描述中使用的引用概念并不相同。在这里,我从 Lisp 术语中借用了这个概念。Postscript 有一个可以设置或测试的文字/可执行标志。将执行执行堆栈上的可执行数组。所以对于“引用”的过程体,它实际上是一个文字过程体(即一个数组)。因此,还必须在下一次迭代之前推送要执行的调用。所以,一个更正确的 postscript 实现的伪代码是:cvx
cvlit
xcheck
repeat
cvx
repeat
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 堆栈清除到循环运算符的“帧”。对于 2-function 样式,这是一个null
对象或类似的。对于 1-function 风格,这是循环运算符本身。
你的语言根本不像 Forth,所以不要复制 Forth 的(仅编译 - 对你的语言来说没有意义的区别)循环结构。
查看您的代码,添加until
为条件重启评估词。也就是说,将它作为一个普通单词添加到你的range
and旁边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
然而,我后来改变了主意,完全依赖递归,没有这样的循环词。
希望有帮助,玩得开心!