返回入门教程首页 主页 | 开发工具 | 应用芯片 | 核心模块  
 
   
 

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

 

 

   

(C) ForthChina.com 版权所有 2004-2010
Email:forthchina@163.com