Forth 语言简明教程 |
作者 Richard E. Haskell |
| 编译 赵宇 张文翠 |
第九课 编译字
9.1 编译和解释
编译字是立即字,这意味着如果在一个冒号定义中遇到它们时,将被立即执行而不是编译到列表段。立即字在名字字段中有一个优先位。(见 see Lesson 3, Section 3.12 ) .
F-PC 处于两个可能的状态之一:编译或者解释。在一个冒号定义的编译期间它处于编译状态,就是说在字“冒号:”执行之后和“分号;”执行之前。系统变量STATE有下列两个可能的值:
TRUE -- 如果编译
FALSE -- 如果解释
为了测试当前在什么状态,我们考虑下面两个定义:
: 1state? ( -- )
STATE @
IF
." Compiling"
ELSE
." Interpreting"
THEN
CR ;
: 1test ( -- )
1state? ;
你把这个程序装入然后打入
1state?
和
1test
在每种情况下都是打印出 "interpreting" ,为什么?
因为,当你打印 1state? 和 1test. 时你都是处于解释状态。
你怎么才能够打印出 "Compiling" 呢?这就需要 1state? 在 1test 编译时执行,也就是说我们必须把 1state? 设计成一个立即字。我们可以通过在 ; 分号之后加一个字 IMMEDIATE 来实现这个目的。让我们定义
: 2state? ( -- )
STATE @
IF
." Compiling"
ELSE
." Interpreting"
THEN
CR ; IMMEDIATE
现在打入下面的定义
: 2test 2state? ;
注意当你打入这个定义的时候,只要你一按下 <Enter> , Compiling 就会打印出来。也就是说, 2state? 被立即执行,并不等待你后面打入 2test. 现在打印
2test
注意没有任何东西打印在屏幕上,这是因为 2state? 没有被编译进字典,它只是立即执行。立即字并不被编译进字典,除非你强制这样做。你可以强制一个立即字被编译进字典而不再立即执行,这是通过字 [COMPILE] 实现的。
下面的字定义 3test 是字 2state? 被编译而不是被执行:
: 3test ( -- )
[COMPILE] 2state? ;
你觉得 3test 会打印什么?试一试。
也可以在冒号定义中使用字 [ 和 ] 来打开或者关闭编译。 [ 的定义是:
: [ ( -- )
STATE OFF ; IMMEDIATE
字 ] 打开编译模式并进入编译循环,编译循环包括:
DO
从输入流中得到下一个字,如果这是一个立即字,执行这个字;
否则编译它;
如果这个字不在字典中,把它转为一个数字并编译它 ;
UNTIL 输入流结束
作为最后一个例子,输入
: 4test [ 1state? ] ;
注意当你按下 <Enter> 后, "interpreting" 被打印出来,为什么?
9.2 字 COMPILE 和 [COMPILE]
我们已经看到: [COMPILE] 将把后面的立即字编译到列表段中。它的定义是:
: [COMPILE] ( -- )
' X, ; IMMEDIATE
字 "tick" (') 把下一个(立即)字的 CFA 放到堆栈上,字 X 编译堆栈上的整数到列表字典的下一个可有地址。注意 [COMPILE] 本身是一个立即数,在包含它的字编译期间可以被执行。
有时你希望在运行时编译一个字,字 COMPILE 将实现这个功能。例如, “semi-colon” 的定义基本上是这样的:
: ; ( -- )
COMPILE UNNEST \ compile the UNNEST routine
REVEAL \ make the colon word available
[COMPILE] [ \ go to interpreting mode
; IMMEDIATE \ do ; immediately
注意 ; 是一个立即字,在一个冒号定义中遇到它时被执行。它 COMPILE (编译)字 UNNEST 子程序的 CFA 到冒号定义字的列表字典,并通过字 REVEAL 使得这个冒号定义字在字典中可以搜索到,之后通过执行 [ 来切换在解释模式。尽管 [ 是一个立即字,但它在分号 ; 定义中被 [COMPILE] 编译。
COMPILE 的定义如下,当包含 COMPILE 的字执行时,它编译下面非立即字的 CFA 。
: COMPILE ( -- )
2R@ \ get ES:SI of next CFA in list seg
R> 2+ >R \ inc SI past next word in list seg
@L \ get CFA on next word in list seg
,X ; \ & compile it at run time
9.3 常数
考虑下面的冒号定义
: four+ ( n -- n+4 )
4 + ;
它编译的字典结构如下所示

字 (LIT) 是一个 CODE 字,它的定义如下 :
CODE (LIT) ( -- n )
LODSW ES: \ get next word at ES:SI, SI=SI+2
1PUSH \ push it on stack
END-CODE
于是字 (LIT) 将把数 4 压入堆栈,指令指针 ES:SI 将指向 + 的 CFA 。
如果你在堆栈上有一个数并希望把它作为一个常数编译到列表字典中,你可以使用 LITERAL ,定义如下:
: LITERAL ( n -- )
COMPILE (LIT) \ compile (LIT)
X, \ plus the value n
; IMMEDIATE \ immediately
字 LITERAL 一个很有用的功能是你可以在定义中计算常数。例如,有时我们写 2+3 比写 5 更直观,你可以这样定义 five+:
: five+ ( n -- n+5 )
[ 3 2 + ] LITERAL + ;
当然你要是这样写,最后的结果与一样:
: five+ 3 2 + + ;
不过, [ 3 2 + ] LITERAL 有一个优点就是常数 5 是在编译期间计算出来的,运行的时候只是执行 5 + 。而 3 2 + + 却需要编译一个常数 3 和一个常数 2 到字典中,而在运行时也需要执行两个加法操作。所以,使用 [ 3 2 + ] LITERAL 产生的代码执行得更快、更有效。
9.4 条件编译字
BRANCH ?BRANCH
有两个条件编译字 BRANCH 和 ?BRANCH 被用于定义 F-PC 中各种条件分支指令。字 BRANCH 是一个 CODE 字,它的定义如下:

BRANCH 被编译到列表段,它的后面是无条件分支目的地址的偏移量。
字 ?BRANCH 在栈顶标志为假时分支到它后面的目的地址。它的定义如下

BEGIN...WHILE...REPEAT
作为一个 BEGIN...WHILE...REPEAT 循环的例子,我们回忆一下字第 4 课中 "factorial" 的定义:
: factorial ( n -- n! )
1 2 ROT \ x i n
BEGIN \ x i n
2DUP <= \ x i n f
WHILE \ x i n
-ROT TUCK \ n i x i
* SWAP \ n x i
1+ ROT \ x i n
REPEAT \ x i n
2DROP ; \ x
这个定义将以下列方式存于列表字典中:

字 BEGIN 在栈顶留下 xhere1 的地址。字 WHILE 编译 ?BRANCH 之后放一个 0 在 xhere2. 这个值 0 将在以后被字 2DROP 的地址 xhere3 代替。而 WHILE 也把 xhere2 的值放在栈上并在 xhere1 之下。字 REPEAT 编译 BRANCH 并用 xhere1 的地址存储在它的之后,然后再把 xhere3 放到堆栈上并把它存入地址 seg:xhere2.
IF...ELSE...THEN
考虑如下的冒号定义
: test ( f -- f )
IF
TRUE
ELSE
FALSE
THEN ;
在列表字典中将按以下方式存储

字 IF 编译 ?BRANCH 后随一个 0 在地址 xhere1. 这个值 0 将在以后被字 FALSE 的地址 xhere3 代替。 IF 也在堆栈上留下 xhere1 的值。
字 ELSE 编译 BRANCH 后随一个 0 在地址 xhere2. 这个值 0 将在以后被字 UNNEST 地址 xhere4 代替。 ELSE 也在把地址留放到栈上之后在栈上留下 xhere2 值并把它存入地址 seg:xhere1.
字 THEN 把地址 xhere4 放到栈上然后把它存入地址 seg:xhere2.
BEGIN...AGAIN
作为一个使用 BEGIN...AGAIN 的例子,看第 8 课的弹出式菜单。它的典型形式是
: main ( -- )
minit
BEGIN
KEY do.key
AGAIN ;
在列表字典中按如下方式存储

字 BEGIN 在栈顶留下 xhere1 的偏移地址,字 AGAIN 编译 BRANCH 并把地址用 , 存入。
BEGIN...UNTIL
下面使用 BEGIN...UNTIL 的例子来自第 4 课:
: dowrite ( -- )
BEGIN
KEY
DUP EMIT
13 =
UNTIL ;
它将按以下方式存储在列表字典中:

字 BEGIN 在栈顶留下 xhere1 的地址。字 UNTIL 编译 ?BRANCH 并把 xhere1 写入,注意 BEGIN...AGAIN 和 BEGIN...UNTIL 的唯一差别是在 AGAIN 中用 UNTIL ?BRANCH 代替了 BRANCH.
DO...LOOP
一个 DO 循环将产生以下的列表字典:

字 DO 编译 (DO) 后随一个 0 在地址 xhere1 。这个值 0 后来将用 DO 循环之后的第一个字的地址 xhere2 代替。 DO 也在栈顶留下了 xhere1 的值。
字 LOOP 编译 (LOOP) 并用 , 写入到地址 xhere1+2. LOOP 然后把地址 xhere2 放到栈上并把它存入 seg:xhere1.
运行时间字
(DO) ( limit index -- )
建立如下的返回栈

运行时间字 (LOOP) 把返回栈顶的值加 1 并在溢出标志没有设置时跳转到 xhere1+2 ,如果溢出标志已经设置(当 index = limit 而栈顶越过了 8000H ) ,则 (LOOP) 从返回栈上弹出 3 个项目,并把指令指针 ES:SI 指向 xhere2.
把 xhere2 放在返回栈的第 3 项是为了 LEAVE 能够找到退出地址。把返回栈顶的 2 个值加上 8000H 可以使执行 (DO) 时 DO 循环能够正确处理 limit 大于 8000H 的情况。
例如,假设 limit 是 FFFFH , initial index 是,返回堆栈上的 initial value o 将是 -7FFFH ,当这个值加 1 之后,溢出标志将不置位直到栈顶等于 8000H, 也就是 FFFFH 个循环之后。
9.5 练习
用字 SEE 和 LDUMP 观察下面 3 个测试字的字典结构:
: a.test ( f -- )
IF
." True"
ELSE
." False"
THEN ;
: b.test ( -- )
5 0 DO
I .
LOOP ;
: c.test ( -- )
4
BEGIN
DUP .
1- DUP 0=
UNTIL
DROP ;
请你为每个字画出字典结构,指出名字和字典中所有字段的实际值,指出字 IF、 ELSE、THEN、DO、LOOP、BEGIN 和 UNTIL 的实际效果。也请解释字 ." 在 a.test 的工作方式,数 5、 0 和 4 在 b.test 和 c.test. 的工作情况。
|