與傳統語言相比,Forth的編譯器過于簡單。
傳統的編譯器通常設計成大型的程序,用來將可預見的合法的語法組織轉換為機器語言。
然而Forth的編譯器僅僅使用一個包含幾行的定義實現。高級的語法結構如條件語句和循環語句則由定義的高級words再次定義實現的
拋開對Forth過于簡單的看法。你會發現Forth在擴展編譯器方面的獨特功能。通過定義新的word,Forth非常容易對底層的編譯器進行擴展。
這種通過擴展編譯器的方式,可以實現具有強大的語言功能
1 區分編譯時與運行時
在深入理解Forth的編譯器時,我們需要認真區分Forth中的編譯時與運行時
通常在Forth編程中,一個word的運行過程(executed)稱為運行時(run time)。一個Forth的定義過程(compiled)稱為編譯時(compile time)。然而Forth中有些特定的word既包含運行時(run-time)也包含編譯時(compile-time)行為
Forth中兩類特定word具有編譯和運行時行為,下面的討論過程中,我們將這類word看做定義word(defining words)和編譯word(compiling words)
一個定義word(defining word)在運行時(executed)用來編譯生成一個新的定義。每個定義word(defining word)說明了它所定義一類的word所具有的運行時(run-time)和編譯時(compile-time)行為。
其中一個定義word(definiting word)是常量定義wordCONSTANT
。
在Forth終端中輸入80 CONSTANT MARGIN
,就會進入CONSTANT
的編譯時(compile-time)行為,
CONSTANT
的編譯時行為將在字典(dictionary)建立一個常量類型的入口,并命名為MARGIN。然后將80存儲到該常量的參數字段,
在Forth終端輸入MARGIN
,就會進入CONSTANT
的運行時(run-time)行為,
CONSTANT
的運行時行為會將MARGIN
對應的常量值存儲到數字棧(stack)中。
另一類具有雙重行為的word我們稱為編譯word(compiling word)。編譯word通常用來分號定義中,并且在這個過程的定義中實現某些特定功能
其中一種編譯word是."
。這個編譯word在定義的編譯(compile-time)過程中將會將一段文本字符串的長度與其內容寫入到該定義word的入口。在運行(run-time)過程中將會打印輸出這段文本內容。
其他的編譯word如IF
和LOOP
頁同時包含不同的運行時和編譯時行為。
2 定義word(defining word)的實現
目前為止我們接觸到的定義word(defining word)如下
VARIABLE ; 變量定義
2VARIABLE ; 變量定義
CONSTANT ; 常量定義
2CONSTANT ; 常量定義
: ; 過程定義
CREATE ; 內存分配定義
這些定義word都是用來定義具有相似的編譯時和運行時行為的類words
這些定義word通常為他們所定義一類word實現了特定的編譯時(compile-time)和運行時(run-time)行為
其中CREATE
是這類定義word(defining words)中基礎的定義word。
在編譯時(compile-time),CREATE
會從輸入流(inpute stream)通過空格截取一個名稱(EXAMPLE),然后在字典中創建一個變量區域使用這個名稱作為開頭。
在運行時(run-time),CREATE
會將這個名稱(EXAMPLE)對應的字典的body的地址存儲到數字棧(stack)上。
基于CREATE
可以實現定義wordVARIABLE
。
: VARIABLE CREATE 0 , ;
因此輸入VARIABLE ORANGES
時
VARIABLE
會執行CREATE
以ORANGES為名稱在字典中創建變量入口。然后將變量的body地址存儲到數字棧(stack)
然后0 ,
中的,
會將0存儲到變量的body中。
由于VARIABLE
定義的變量由CREATE
實現,因此ORANGES具有與CREATE定義的word相同的運行時(run-time)行為,VARIABLE
調用時將會將地址存放到數字棧(stack)上。
通常在定義word(deining word)中我們使用DOES>
區別word的運行時(run-time)行為
: DEFINING_WORD
CREATE (compile-time operations)
DOES> (run-time operations)
為了證實這點,我們可以將CONSTANT
定義word實現為如下結構
: CONSTANT
CREATE , (CREATE 創建一個字典入口,存儲數字棧的值到常量參數字段)
DOES> @; (DOES 將word的body地址存儲到stack中,然后調用@,獲取數字棧上地址的內容)
那么在Forth中輸入76 CONSTANT TROMEBONES
將會實現如上定義過程
而DOES>
前綴修改模式,其后的內容定義這個模式的操作內容
DOES>
用在創建定義word(defining word)標記編譯時行為結束和運行時行為開始。運行時行為常常是Forth中高層的操作。這個body地址將會存儲到stack中
3 定義word實現實例
接下來舉例說明定義word的實現
在字符串輸入中,我們需要將輸入內容的長度和輸入內存存儲到變量中。
我們可以定義相應的word簡化這種操作
這里我們定義wordCHARACTERS
如果我們輸入20 CHARACTERS ME
那么我們將創建一個名為ME的可以用來存儲20個字符的字符串數組
如果我們再次輸入ME
,我們可以獲取這個字符串數組的地址和字符串中字符的長度存儲到數字棧(stack)在。
CHARACTER
的實現如下
: CHARCTERS
CREATE (定義時創建字典ME入口)
DUP , ALLOT (首先(DUP ,)復制20將其存儲到body首個位置,然后(ALLOT)申請20個用來存儲字符的內存)
DOES> (運行時獲取body地址到stack)
DUP (復制數字棧上的body地址)
CELL+ (移動到字符串長度下一個內存地址,也就是字符串內容首地址到數字棧,)
SWAP (然后交換字符串地址棧與長度地址,)
@ (獲取字符串長度到數字棧頂部)
; (運行完后數字棧的內容為 (addr count --))
這里我們擴展編譯器實現了一個CHARACTERS
定義word。這個定義word將會創建特定的數據結構。不僅僅可以簡化我們的輸入輸出,還可以在需要的時候用來修改字符串的長度,
接下來我們實現一個有用的字符串數組定義wordSTRING
: STRING CREATE ALLOT DOES> + ;
輸入30 STRING VALUE
將會創建一個名為VALUE的30個字節長的數組。
當我們輸入6 VALUE C@
我們可以訪問特定位置的內容
我們還可以實現其他的數字數組。比如初始化為0的數字數組,
: ERASED HERE OVER ERASAE ALLOT ;
: 0STRING CREATE ERASAED DOES> + ;
首先定義ERASED
然后在0STRING
中調用這個定義word
可以通過修改定義word,實現修改其他由這個word實現的功能。
另外我們可以實現將內存中的字符串存儲到硬盤中,只需要重新修改STRING的運行時行為(run-time)。這個新的STRING將會通過計算機硬盤中需要包含記錄的長度 然后將去讀取到輸入緩存中,最后然后這個輸入緩存中的地址
可以使用定義 word創建任何類型的數據結構,有時候需要創建多維數組,下面給出創建二維數組的定義
: ARRAY (#row #cols --)
CREATE DUP , * ALLOT
DOES (member : row col -- addr)
ROW OVER@ * + + CELL+ ;
如果輸入4 4 ARRAY BOARD
將會創建4x4的數組
為了獲取數組的內容 輸入2 1 BOARD C@
將會獲取2行1列內容
其中運行時(run-time)行為如下
Operation Contents of stack
... row col pfa
ROT col pfa row
OVER @ col pfa row #cols
* col pfa row-index
++ address
CELL+ corrected address
最后一個例子是一個可視化定義wordShapes
DECIMAL
: star [CHAR] * EMIT ;
: .row CR8 0 DO
DUP 128 AND IF star
ELSE SPACE
THEN
1 LSHIFT
LOOP DROP;
: SHAPE CREATE 8 0 DO C, LOOP
DOES> DUP 7 + DO I C@ .row -1 +LOOP CR;
4 分號編譯器的機制
上面的定義word(defining word)通常用來實現特定的數據結構的保存與獲取,下面的編譯word(compiling word)通常用在分號定義的編譯器。
最具有代表性的編譯word是實現邏輯控制的word,如IF
THEN
DO
LOOP
等。因為這些word對于Forth系統很重要,因此我們一般不會修改這些word。為了理解這些編譯word,我們會在運行過程檢測這些words的實現機制,然后實現各種編譯word(compiling-word)
正如在介紹:
時所說的,進入:
后搜索各個word的定義地址,然后編譯各個word的定義地址到相應的word的入口。
然后在分號編譯過程中并不會將編譯word的地址存儲到定義字典中,而是運行這種編譯word
那么分號編譯過程中如何區分這兩種word? 可以通過檢查word定義的優先級precedence bit
實現。如果這個優先級位是off。那么這個word的地址將會存儲到定義字典中,如果這個優先級位是on,那么可以理解執行這個word。這種立即執行的word稱為立即word(immediate)。
也就是說:
;
等word是立即word
可以使用關鍵詞IMMEDIATE
創建一個立即word。
: name definition ; IMMEDIATE(設置 precedence bit 為on)
那么,這個word將會在定義過程被運行
下面是個例子
: SAY-HELLO ." Hello" ; IMMEDIATE
我們可以簡單的運行這個word,正如普通word
SAY-HELLO return-key
輸出Hello ok
然而神奇的時,如果我們將這個word存放到另一個定義中,如
: GREET SAY-HELLO ." I Speak forth " ;
時
SAY-HELLO
的地址將不會存儲到GREET
的定義中,而是直接輸出
Hello ok
.
這個可以通過輸入GREET
的輸出I Speak forth
得證。
由此可知立即word不會被存儲到定義word的字典中
這里需要說說Forth使用者對于這種行為的習慣稱呼。
在上述的GREET例子中,我們認為SAY-HEELO有一個編譯時行為而沒有運行時行為,然而對于單獨的SAY-HELLO卻包含著運行時行為。
通常我們將GREET稱為SAY-HELLO的編譯者,立即word,SAY-HEELO對于它的編譯者GREET沒有運行時行為
另一個立即word的例子是BEGIN
: BEGIN HERE ; IMMEDIATE
。由此可知BEGIN
在編譯時將HERE的地址存儲到數字棧stack
。然后在隨后的UNTIL
或者REPEAT
的立即word將會得到重復時需要返回的地址,也就是BEGIN
存儲到數字棧的地址。
BEGIN
并不會存儲任何內容到對應的word。只是簡單的將HERE存儲到stack中供REPEAT使用。
然后大多數編譯word包含一個運行時行為,對于這類編譯word,通常需要將其運行行為入口地址存儲到編譯word中。
一個具有代表性的例子是DO
。
與BEGIN
相同的是DO
在編譯時需要提供HERE
供LOOP
或者+LOOP
使用來返回到重復運行的地址。
不過與BEGIN
不同的是DO
也包含一個運行時行為,會將limit和index存儲到return stack中
DO
的運行時行為使用Forth在低級word定義,
: DO POSTONE 2>R HERE ; IMMEDIATE
其中的POSTONE
會查找接下來的定義中的word(2>R)。然后將其存儲到編譯定義中。因此在運行時2>R
將會被運行。也就是用來存儲limit和index到return stack中。
可以將POSTONE看做IMMEDIATE的臨時取消動作,
另一個例子是;
。在編譯過程中,分號的操作內容如下
; POSTPONE EXIT REVEAL POSTONE [ ; IMMEDIATE
首先將EXIT的地址存儲到;
中,作為運行時的行為,
然后使用REVEAL
將當前編譯的;
暴漏出來可以被用于其后的定義中
REVEAL
會將正在被編譯生成的word可以在編譯過程中搜索到。
POSTPONE
會將立即word強制編譯到word中而不是運行它
其運行機制是解析其后的輸入字符流中的word,判斷是否是立即word然后執行不同的行為。如果這個word不是立即word。將會將這個word的地址編譯存儲到定義字典中,如果這個word是立即word,那么會強制將立即word的地址編譯到當前定義字典中。然而在退出的定義中,一個立即word會被運行。
下面是IMMEDIATE和POSTPONE的機制
IMMEDIATE 標注定義的word為立即word
POSTPONE 在編譯word中,直接編譯接下來的word地址到定義中,
5 更多的編譯控制words
還有兩個需要了解的編譯控制word。[
]
可以用在分號定義中停止編譯和重新開啟編譯。在它們之間包含的word將會被看做立即word運行
: SAY-HELLO ." Hello " ;
: GREET [ SAY-HELLO ] ." I speak forth " ; (輸出 Hello ok)
GREET 輸出(I speak Forth ok)
SAY-HELLO并不是一個立即word,但是[]
可以修改其編譯控制,當做立即word運行
這種機制的代表例子是word LITERAL
數字在分號定義中會被看做字面量(literal),比如數字4
: FOUR-MORE 4 +
;會將數字4看做字面量
字面量在過程定義(:
)中需要兩個cells保存,如下
首先保存LITERAL的word將會在運行將數字4存儲到數字棧stack中
可以將這種行為成為字面量運行時代碼。或者簡單看做字面量行為
因此過程定義:
首先會將字面量行為word存儲,然后存儲數字本身
LITERAL
會將字面量代碼和數字本身存儲到定義中
: FOUR-MORE [ 4 ] LITERAL + ;
首先會將數字4當做立即word運行,存儲到數字棧(stack)。然后保存運行時代碼與數字4到定義中.會得到與上面相同的FOUR-MORE的定義
另一個LITERAL的使用如下
VARIABLE LIMITS 4 CELLS ALLOT
可以創建一個LIMIT
: LIMIT (index -- addr) CELLS LIMITIS + ;
HERE 5 CELLS ALLOT BASAE !
首先我們將HERE的地址存儲到BASE中。然后定義如下的LIMIT
: LIMIT (index -- addr) CELLS [BASE @] LITERAL +
;
可以將這個地址作為字面量存儲到LIMIT中,然后恢復BASE
DECIMAL
目前我們已經清楚了LITERAL。我們給出一個關于[,]
更好的例子
假設在過程定義:
中,我們需要打印數組 我們上面定義的BOARD的row 2 column 3。為了獲取這個字節的地址,我們可以使用下面的句子
BOARD 2 8 (#cols) * 3 + CELL+ +
為了將這種語句特例化
可以改寫為BOARD [2 8 ( #cols) * 3 + CELL+] LITERAL +
下面是一個例子可以用在應用中,這些定義可以讓我們一探word的內部實現機制
`: DUMP-THIS [HERE] LITERAL 32 DUMP . " DUMP-THIS"
運行DUMP-THIS時,將會打印存儲到DUMP-THIS中的地址。
而LITERAL
的定義如下
: LITERAL POSTPONE (LITERAL) , ; IMMEDIATE
首先編譯運行時代碼地址到運行過程,然后編譯字面值
6 最終的解釋
接下來會給出文本解釋和分號編譯的概括性解釋
Forth系統中的INTERPRET的定義如下
: INTERPRET (--)
BEGIN
BL FIND
IF EXECUTE ?STACK ABORT" Stack empty"
ELSE NUMBER
THEN
AGAIN;
在一個循環中,試著在輸入字符流中根據BL切分word,
在字典中查找word,如果定義了就運行這個word,然后檢查是否棧移除。如果查找word失敗,然后將其看做數字,存儲到數字棧stack中
當這個word解析完后重新返回解析下個word
解釋器就是一個如此簡單而強力的結構,編譯器:
的定義如下
: ]
BEGIN
BL FIND DUP
IF -1 = IF EXECUTE ?STACK ABORT" Stack empty"
ELSE ,
THEN
ELSE DROP (NUMBER) POSTPONE LITERAL
THEN
這里將編譯器定義為]
。而:
調用]
在一個循環中,試著在輸入字符流中根據BL切分word,
然后在字典中查找,如果這個word定義而且是立即word就立即運行,
如果不是立即word,則將FIND查找到的地址保存到定義中
如果使用數字的則將數字看做字面量存儲
然后重復這個循環。
與解釋器相比,編譯器]
可以看做一個具有運行或者編譯的word的解釋器擴展。這正是編譯器可以簡單擴展的原因
因此有兩種方式可以用來擴展Forth的編譯器,總結如下
1 通過創建新的定義word(defining words),來擴展數據編譯器
2 通過創建新的編譯word(compiling words),來擴展過程編譯器