Forth 初学者指导 (使用Win32Forth)
原著 J.V. Noble
编译 Forth 中文网, ForthChina.com
原文 Win32Forth 系统联机在线文档
CREATE ... DOES> Forth 的珍珠
Michael Ham 把 CREATE...DOES> 称为“Forth 的珍珠 " 。 CREATE 是一个编译器组件,它的功能是用给定的名字(输入流中的下一个字)创建一个新的字典项,不做其它的事情。 DOES> 把指定的运行时间行为赋予 CREATE 新创建的字。
定义 “定义” 字
CREATE 最重要的用途就是扩展功能强大的 Forth 字类(称为定义字)。冒号定义是这种“定义字”, VARIABLE 和 CONSTANT 也是“定义字”。
VARIABLE 的定义用高级语言简单地表示就是
: VARIABLE CREATE 1 CELLS ALLOT ;
我们已经看到了如何把 VARIABLE 用在程序中(在有些 Forth 中使用的是一个类似的方法
: VARIABLE CREATE 0 , ;
这里变量被初始化成 0)
Forth 允许我们定义初始化指定值的字:我们可以把 17 定义成一个字:使用 CREATE 和“ , ” ( “逗号” ) :
17 CREATE SEVENTEEN , <cr> ok
现在测试它
SEVENTEEN @ . <cr> 17 ok .
说明:
> 字 , ( “逗号” ) 把 TOS 放到字典中的下一个单元中,并通过所用的字节数来增量字典的指针。
> 还有一个字 " C, " ("see-comma") — 它把字符放到字典中占一个字符的长度,字典指针增量为 1 (如果这个字符是 ASCII ,则是 1 个字节,如果是 Unicode ,则需要 2 个字节)
运行时间和编译时间行为
在前面的例子中,当我们 CREATE 变量的时候,可以把一个变量初始化成 17 ,但是当我们需要它的时候,还必须通过 SEVENTEEN @ 把它读取到堆栈上。这好象还不是我们内心想要的,我们希望仅仅使用名字 SEVENTEEN 就能够在 TOS 中得到 17 。字 DOES> 给了我们这样的工具:
字 DOES> 的功能是指定一个定义字的“子”字的运行时间行为。考虑定义字 CONSTANT 使用高级 Forth 字定义(当然由于速度的原因, 实际的Forth系统中 CONSTANT 通常是用机器码定义的):
: CONSTANT CREATE , DOES> @ ;
我们使用
53 CONSTANT PRIME <cr> ok
现在测试:
PRIME . <cr> 53 ok .
这里发生了什么?
CREATE ( 隐藏在 CONSTANT 中 ) 创建一个命名为 PRIME 项目(输入流中 CONSTANT 之后的第一个字)。接着 " , " 把字典中的下一个单元的内容放到栈顶(数值 53 )。
之后 DOES> ( 在 CONSTANT 里 ) 加入从它开始到 " ; " (结束定义)为止的全部字作为它的行为 – 在这里是 " @ "— 到被 CONSTANT 定义的子字中。
维度数据
这里是一个例子,它显示了定义字的能力和编译时间与运行时间的区别。
物理问题往往涉及维度数量,通常用质量(M)、长度(L)和时间(T)或者它们的乘积表示。有时还使用多于一个的物理单位来描述同一个属性。
例如,美国、英国的警察在报告事故的时候使用英寸、英尺、码,欧洲大陆的警察使用厘米和米。为了避免编写事故分析报告软件的不同版本,我们编写一个进行单位转换的程序更为简单。这就是 Forth 的简单性。
最简单的方法是在内部把长度统一用毫米表示,转换的方法如下:
: INCHES 254 10 */ ;
: FEET [ 254 12 * ] LITERAL 10 */ ;
: YARDS [ 254 36 * ] LITERAL 10 */ ;
: CENTIMETERS 10 * ;
: METERS 1000 * ;
注意:这个例子基于整数运算。字 */ 意味着“把堆栈上第三个数与 NOS 相乘,保持双精确度,再用 TOS 去除。字 */ 堆栈的说明为 ( a b c -- a*b/c).
用法应该是
10 FEET . <cr> 3048 ok
字 " [ " 在编译模式下把编译模式切换到解释模式(如果系统在解释模式下则不做任何事情)。字 " ] " 把解释模式切换到编译模式。
假设我们不考虑错误检测,冒号编译器 " : " 的 " 定义 " 就是:
: : CREATE ] DOES> doLIST ;
" ; " 的定义是
: ; next [ ; IMMEDIATE
这个开关的另一个用途是在编译时间而不是执行时间进行算术运算,使程序更清晰更容易修改,比如我们在上面的定义中
[ 254 12 * ] LITERAL
和
[ 254 36 * ] LITERAL
前面处理单位的方法要求许多不必要的定义,它们产生不必要的代码。更紧密的方法是使用一个定义字 UNITS :
: D, ( hi lo --) SWAP , , ;
: D@ ( adr -- hi lo) DUP @ SWAP CELL+ @ ;
: UNITS CREATE D, DOES> D@ */ ;
我们可以构造一个表
254 10 UNITS INCHES
254 12 * 10 UNITS FEET
254 36 * 10 UNITS YARDS
10 1 UNITS CENTIMETERS
1000 1 UNITS METERS
\ Usage:
10 FEET . <cr> 3048 ok
3 METERS . <cr> 3000 ok
\ .......................
\ etc.
这是一个改进,但是 Forth 还允许进行简单的扩展以使转换返回到输入单位并用于输出:
VARIABLE <AS> 0 <AS> !
: AS TRUE <AS> ! ;
: ~AS FALSE <AS> ! ;
: UNITS CREATE D, DOES> D@ <AS> @
IF SWAP THEN
*/ ~AS ;
\ UNIT DEFINITIONS REMAIN THE SAME.
\ Usage:
10 FEET. <cr> 3048 ok
3048 AS FEET . <cr> 10 ok
编译器的高级用法
假设我们有一些按钮,编号为 0-3 ,字 WHAT 用于读它们。也就是说 WHAT 等待从键盘来的输入,当键盘 #3 被按下的时候, WHAT 把 3 留在堆栈上。
我们应该定义一个字 BUTTON 来执行第 n 个键按下时应该产生的动作,所以我们可以说:
WHAT BUTTON
在传统的语言中 BUTTON 大概是这样的:
: BUTTON DUP 0 = IF RING DROP EXIT THEN
DUP 1 = IF OPEN DROP EXIT THEN
DUP 2 = IF LAUGH DROP EXIT THEN
DUP 3 = IF CRY DROP EXIT THEN
ABORT" WRONG BUTTON!" ;
我们平均需要穿过两层判断。
Forth 有可能使用更精巧的方法:“跳转表“。通过这种机制, Forth 把执行标记(通常是一个地址,但也不是必须)传送给字 EXECUTE . 如果我们有一个执行标记的表,则只需要通过索引(表的偏移量)去查找它,取出来放到堆栈上,然后执行 EXECUTE .
一种编码方法是
CREATE BUTTONS ' RING , ' OPEN , ' LAUGH , ' CRY ,
: BUTTON ( nth --) 0 MAX 3 MIN
CELLS BUTTONS + @ EXECUTE ;
注意 0 MAX 3 MIN 保证索引范围不发生溢出。尽管 Forth 的哲学是避免由于不必要的错误检测(因为字是在定义时进行检测的)而降低执行速度,但是在编程序的时候进行用户接口方面的错误处理还是很有意义的,它通常很容易防止错误,比发生了之后再恢复要简单得多。
行为表方法是如何工作的呢?
CREATE BUTTONS 在字典中建立一个项目 BUTTONS .
字 ' (”tick”) 找到后面字的执行标记 (XT) ,字 , (” 逗号 ”) 把它存储在新字 BUTTONS 的数据域中。重复直到我们要求的子程序的 XT 全部存入表中
表 BUTTONS 现在含有 BUTTON 和与对应的不同行为的 XT
CELLS 然后进行乘法,得到偏移量
BUTTONS+ 接着加上 BUTTONS 的基地址以得到 XT 存储器位置的绝对地址
@ 为 EXECUTE 读取 XT
EXECUTE 执行对应按键的功能。
简单!
如果程序需要不止一个行为表,上面的方法也可以满足。不过,更复杂的程序需要很多这样的表。在这种情况下,我们可以建立一个系统来定义行为表,包括防止错误的代码和进行适当行为选择的代码。一个办法是这样的:
: ;CASE ; \ do-nothing word
: CASE:
CREATE HERE -1 >R 0 , \ place for length
BEGIN BL WORD FIND \ get next subroutine
0= IF CR COUNT TYPE ." not found" ABORT THEN
R> 1+ >R
DUP , ['] ;CASE =
UNTIL R> 1- SWAP ! \ store length
DOES> DUP @ ROT ( -- base_adr len n)
MIN 0 MAX \ truncate index
CELLS + CELL+ @ EXECUTE ;
注意有两种方式的错误检查。在编译时间, CASE: 在我们要求它指向一个没有定义的子程序时,退出新字的编译:
case: test1 DUP SWAP X ;case
X not found
我们可以计算在表中有多少个子程序(包括不做任何事情的 ;case ) 这样我们就可以强制把索引范围设定在 [0,n] 范围内。
CASE: TEST * / + - ;CASE ok
15 3 0 TEST . 45 ok
15 3 1 TEST . 5 ok
15 3 2 TEST . 18 ok
15 3 3 TEST . 12 ok
15 3 4 TEST . . 3 15 ok
只是为了方便变换,这里还有另一种方法:
: jtab: ( Nmax --) \ starts compilation
CREATE \ make a new dictionary entry
1- , \ store Nmax-1 in its body
; \ for bounds clipping
: get_xt ( n base_adr -- xt_addr)
DUP @ ( -- n base_adr Nmax-1)
ROT ( -- base_adr Nmax-1 n)
MIN 0 MAX \ bounds-clip for safety
1+ CELLS+ ( -- xt_addr = base + 1_cell + offset)
;
: | ' , ; \ get an xt and store it in next cell
: ;jtab DOES> ( n base_adr --) \ ends compilation
get_xt @ EXECUTE \ get token and execute it
; \ appends table lookup & execute code
\ Example:
: Snickers ." It's a Snickers Bar!" ; \ stub for test
\ more stubs
5 jtab: CandyMachine
| Snickers
| Payday
| M&Ms
| Hershey
| AlmondJoy
;jtab
3 CandyMachine It's a Hershey Bar! ok
1 CandyMachine It's a Payday! ok
7 CandyMachine It's an Almond Joy! ok
0 CandyMachine It's a Snickers Bar! ok
-1 CandyMachine It's a Snickers Bar! ok
|