rtforth 0.6.8

Forth implemented in Rust for realtime application
Documentation
# 自己定義運算指令

之前我們看到了 `+` 、 `-` 、 `f+` 、 `f-` 等指令,這些是系統內建的指令。現在讓我們定義自己的第一個指令。這個指令在安裝的章節已經定義過。輸入時請注意 Forth 使用空格來解析使用者的輸入,因此指令 `."` 之後要有空格,指令 `;` 之前也要有空格。

```
rf> : hello ." Hello World!" ;
 ok
```
測試一下新定義的指令。

```
rf> hello
Hello World! ok
```

這個指令是使用冒號 (`:`) 定義出來的。因此被稱為冒號定義 (colon definition)。冒號定義相當於其他程式語言的副程式或函式。

冒號 `:` 定義了一個新指令,在它之後的 hello 是這個新指令的名稱。名稱之後到 `;` 之前的部份是這個指令的行為。分號 `;` 結束了這個新的定義。

指令 `."` 編譯了它之後一直到 `"` 的字串,然後,當 hello 執行時,這個字串被印出來。

在前幾章我們學會如何使用 Forth 的整數、浮點數及比較邏輯運算指令來進行運算。在以下幾節,我們將使用自行定義的冒號定義來處理更複雜的運算。

### 本節指令集

為了描述 `:` 及 `."` 這類之後必須接其他資料的指令,我們在堆疊效果的括弧中以引號表達指令之後所需的資料。

| 指令 | 堆疊效果及指令說明                        | 口語唸法 |
|-----|----------------------------------------|--------|
| `:` | ( "name" -- )   定義一個名為 name 的指令後,進入編譯模式 | colon |
| `;` | ( -- )   結束一個冒號定義,會编譯一個結束指令 EXIT 並且結束編譯模式 | semicolon |
| `."` | ( "text" -- )   編譯其後直到下一個 " 的文字,並於未來指令執行時印出此文字 | dot-quote |

-------------
## 資料堆疊指令

Forth 使用堆疊存放資料。但是用堆疊計算時,常會出現困境,使得我們明明看到堆疊上有我們想要的資料,卻苦於無法使用。解決的方法是堆疊指令。以下我們透過幾個小問題,讓我們理解這個需求。

### 複製堆疊頂端的數

我們先從一個小問題開始:設計指令計算整數的平方。

從前幾章我們學到可以這樣計算 3 的平方:`3 3 *`。現在我們希望能設計一個名為 `square` 的指令,執行 `3 square` 時,就會計算出 3 的平方。因為使用者只放了一個 3 到堆疊上,所以我們必須先將堆疊上的那個 3 複製一次,使得堆疊上有兩個 3 ,才能執行乘法。Forth 提供了一個複製堆疊上整數的指令 `dup`。所以我們可以定義這平方指令如下:
```
rf> : square   dup * ;
 ok
rf> 3 square  .
9  ok
rf> 4 square  .
16  ok
```

練習:請設計指令 `cube` 計算堆疊上整數的立方。

### 交換堆疊上的兩個數

假設堆疊上有兩個數字 ( x y ),我們設計一個指令計算 x<sup>2</sup> - y<sup>2</sup>。我們採用以下思路:
```
( x y )     square
( x y*y)    <- 糟糕,x 不在堆疊頂端,如何將它平方?
```
我們需要能將堆疊頂端的兩個數字交換的指令,`swap` 就是這個指令。
```
( x y*y )   swap
( y*y x )   square
( y*y x*x ) <- 糟糕,我們要算的是 x*x-y*y。但是在這兒用減法會得到 y*y-x*x。
```
沒關係,我們可以先減再求負數,如下:
```
( y*y x*x ) -
( y*y-x*x)  negate
( x*x-y*y)
```
或是先交換再減,如下:
```
( y*y x*x ) swap
( x*x y*y ) -
( x*x-y*y )
```
我們使用第一個版本:
```
rf> : x^2-y^2 ( x y -- x^2-y^2 )   square  swap  square  - negate ;
 ok
rf> 3 2 x^2-y^2  .
5  ok
```

注意這兒我們使用 x^2-y^2 做為這個指令的名字。Forth 指令名字不像其他語言的函式名只能是英文字母和數字。重點是指令和指令之前必須以空格隔開。

練習:假設堆疊上有兩個數字 ( x y ) 。設計一個指令計算 x<sup>2</sup> - y 。

### 其他資料堆疊指令

除了 `dup` 與 `swap` ,Forth 還提供以下資料堆疊指令:

* 檢視堆疊上的資料:指令 `.s` 顯示資料堆疊和浮點堆疊上的數值,但不會改變堆疊的內容。
* 拋棄堆疊上的資料。指令 `drop` 拋棄資料堆疊最上方的整數。指令 `nip` 則拋棄最上方之下的那個整數。
* 複製堆疊上的資料。之前已說過 `dup` 複製了資料堆疊最上方的整數。指令 `over` 則複製最上方之下的那個整數。
* 旋轉堆疊上的資料。當堆疊上有三個整數 ( n1 n2 n3 ) 時,指令 `rot` 會旋轉它們的次序使得原本在堆疊從上數來第三位的 n1 會出現在最上方。指令 `-rot` 則以相反的方向旋轉,使得最上方的 n3 出現在從上數來第三位的位置。
* 雙整數堆疊指令。另有 `2dup` `2drop` `2swap` `2over` 類似 `dup` `drop` `swap` `over` ,但以兩個整數為一組進行處理。

請參考本節指令集中的堆疊效果以瞭解以上說明。

雖然堆疊是 CPU 內極為重要的資料結構,但是大多數的程式語言並不讓人直接存取堆疊。因此對有其他程式語言經驗的人,這是接受 Forth 最困難的,也最需要練習的部份。Forth 將這部份開放給它的使用者,帶來很特別的好處:極度簡化 Forth 的文本直譯器。這使得 Forth 成為少數能由非資工背景的工程師完全掌握,甚至自行開發的語言。

例子:堆疊上原本有三個數字 ( 1 2 3 ),請運用堆疊指令使其次序變成  ( 3 2 1 ),並以 .s 檢查結果。
```
rf> 1 2 3 .s
1 2 3  ok
rf> -rot .s
3 1 2  ok
rf> swap .s
3 2 1  ok
```

練習:請做以下練習,在表格中填入能達成堆疊效果的 Forth 堆疊指令。並以 `.s` 檢查結果。若有必要可以輸入任何 Forth 不認得的指令,比如 `xx` ,以清除堆疊。因為 Forth 在發生錯誤時會清除所有堆疊。

| 堆疊效果 | 所需 Forth 指令 |
|---------|---------------|
| ( 1 2 3 -- 3 2 1 ) | -rot swap |
| ( 1 2 3 -- 1 2 ) | |
| ( 1 2 3 -- 1 3 ) | |
| ( 1 2 3 -- 2 3 ) | |
| ( 1 2 3 -- 3 1 ) | |
| ( 1 2 3 -- 2 2 3 ) | |
| ( 1 2 3 -- 1 2 3 1 ) | |
| ( 1 2 3 -- 1 2 3 1 2 ) | |
| ( 1 2 3 -- 1 2 3 1 2 3 ) | |
| ( 1 2 3 -- 2 1 3 1 ) | |
| ( 1 2 3 4 -- 1 2 3 4 2 1 ) | |
| ( 1 2 3 4 -- 4 3 2 1 ) | |

### 本節指令集

| 指令 | 堆疊效果及指令說明                        | 口語唸法 |
|-----|----------------------------------------|--------|
| `.s` | ( ... -- ... ) &emsp; 顯示但不改變資料堆疊和浮點堆疊的內容 | dot-s |
| `dup` | ( n -- n n ) &emsp; 複製資料堆疊頂端的數 | dup |
| `drop` | ( n -- ) &emsp; 拋棄資料堆疊頂端的數 | drop |
| `nip` | ( n1 n2 -- n2 ) &emsp; 拋棄資料堆疊頂端數來第二個數 | nip |
| `swap` | ( n1 n2 -- n2 n1 ) &emsp; 交換資料堆疊頂端的兩個數 | swap |
| `over` | ( n1 n2 -- n1 n2 n1 ) &emsp; 複製資料堆疊從頂端數來的第二個數 | over |
| `rot` | ( n1 n2 n3 -- n2 n3 n1 ) &emsp; 旋轉資料堆疊頂端的三個數,使得第三個數被移到頂端 | rote |
| `-rot` | ( n1 n2 n3 -- n3 n1 n2 ) &emsp; 旋轉資料堆疊頂端的三個數,使得最頂的數被移到頂端算來第三個位置 | minus-rote |
| `2dup` | ( n1 n2 -- n1 n2 n1 n2 ) &emsp; 複製資料堆疊頂端的兩個數 | two-dup |
| `2drop` | ( n1 n2 -- ) &emsp; 拋棄資料堆疊頂端的兩個數 | two-drop |
| `2swap` | ( n1 n2 n3 n4 -- n3 n4 n1 n2 ) &emsp; 交換資料堆疊頂端的兩對整數 | two-swap |
| `2over` | ( n1 n2 n3 n4 -- n1 n2 n3 n4 n1 n2 ) &emsp; 複製資料堆疊從頂端數來的第三、第四個數| two-over |

-------------
## 浮點堆疊指令

Forth 也同樣提供浮點堆疊指令,但沒有資料堆疊那麼完整。請參考本節指令集。

例子: 求 x<sup>5</sup> + 2 x<sup>4</sup> + 3 x<sup>3</sup> + 4 x<sup>2</sup> + 5 x + 6 。

若直接計算這個五次方程式,需要執行 4 + 4 + 3 + 2 + 1 = 14 個乘法,以及 5 個加法。使用 Horner's method 可以簡少計算量:

x<sup>5</sup> + 2 x<sup>4</sup> + 3 x<sup>3</sup> + 4 x<sup>2</sup> + 5 x + 6

= (x<sup>4</sup> + 2 x<sup>3</sup> + 3 x<sup>2</sup> + 4 x + 5) x + 6

= ((x<sup>3</sup> + 2 x<sup>2</sup> + 3 x + 4) x  + 5) x + 6

= (((x<sup>2</sup> + 2 x + 3) x + 4) x  + 5) x + 6

= ((((x + 2) x + 3) x + 4) x  + 5) x + 6

最後的式子 ((((x + 2) x + 3) x + 4) x  + 5) x + 6 只需要 5 個加法及 4 個乘法,在計算上更有效率。以下設計 Forth 指令計算這個式子:

```
: poly5 ( F: x -- x^5+2x^4+3x^3+4x^4+5x^5+6 )
   fdup         \ ( F: x x )
   2e f+        \ ( F: x x+2 )
   fover f*     \ ( F: x (x+2)x )
   3e f+        \ ( F: x ((x+2)x+3) )
   fover f*     \ ( F: x ((x+2)x+3)x )
   4e f+        \ ( F: x (((x+2)x+3)x+4) )
   fover f*     \ ( F: x (((x+2)x+3)x+4)x )
   5e f+        \ ( F: x ((((x+2)x+3)x+4)x+5) )
   fover f*     \ ( F: x ((((x+2)x+3)x+4)x+5)x )
   6e f+        \ ( F: x ((((x+2)x+3)x+4)x+5)x+6 )
;
```

在指令 `poly5` 之後加上了堆疊效果,這是撰寫 Forth 指令的好習慣。

指令 `\` 會忽略其後至換行的所有字元,因此類似 `(` 的作用,是另一種註解方式。在此採用此一註解方式的原因是註解中有很多括弧,不適合使用 `(`。

直式方式加上使用 `\` 開頭的註解,方便初學者瞭解堆疊變化。已熟悉 Forth 堆疊操作的工程師會以下列方式撰寫:

```
\ Caculate x^5+2x^4+3x^3+4x^4+5x^5+6 using Horner's method.
: poly5 ( x -- result )
   fdup
   2e f+ fover f*
   3e f+ fover f*
   4e f+ fover f*
   5e f+ fover f*
   6e f+ fnip
;
```

練習:假設浮點堆疊上資料為 ( F: x ),請設計指令 `poly3` 計算 x<sup>3</sup> + 2 x <sup>2</sup> + 3 x + 4 的值。從 `poly5` 的型式你應該能很快的寫出這個指令。

### 本節指令集

| 指令 | 堆疊效果及指令說明                        | 口語唸法 |
|-----|----------------------------------------|--------|
| `fdup` | ( F: r  -- r r ) &emsp; 複製浮點堆疊頂端的數值 | f-dup |
| `fdrop` | ( F: r -- ) &emsp; 拋棄浮點堆疊頂端的數值 | f-drop |
| `fswap` | ( F: r1 r2 -- r2 r1 ) &emsp; 交換浮點堆疊頂端的兩個數值 | f-swap |
| `fover` | ( F: r1 r2 -- r1 r2 r1 ) &emsp; 複製浮點堆疊從頂端數來的第二個數值 | f-over |
| `frot` | ( F: r1 r2 r3 -- r2 r3 r1 ) &emsp; 旋轉浮點堆疊頂端的三個數,使得第三個數被移到頂端 | f-rote |
| `\` | ( "comment" ) &emsp; 忽略其後到此行結尾的文字,可做為註解 | backslash |

---------------
## 第一個腳本程式

我們也可以以簡單的文字編輯器編輯 Forth 程式存檔,形成所謂的腳本。在之後執行。

請將以下腳本以自己熟悉的文字編輯器輸入並存於一 square.fs 檔中。
```
: square ( n -- n*n )
   dup    ( n n )
   *      ( n*n )
   ;

3 square  .
4 square  .
bye
```
在腳本定義 square 時,以垂直的型式撰寫並在後面加上了堆疊效果的註解,這有助於新手學習,也有助於閱讀的人理解這個指令。當程式複雜時這是一個很好的習慣。請注意註解以指令 `(` 開頭,以 `)` 結束,`(` 後要有空格,因為它是一個指令。

執行 `./target/debug/examples/rf /path/to/square.fs` ,

```
$ ./target/debug/examples/rf /tmp/square.fs
9 16
```

在這個腳本中我們還執行了 `3 square .` 及 `4 square .`,最後執行 `bye` ,完成任務離開 Forth 系統。若不執行這些, 執行完腳本後會出現 `rf>` 這個提示字串,容許我們繼續輸入 Forth 指令。於是這腳本就相當於一個函式庫。

-------------
## 本章重點整理

* 冒號定義
* 編譯模式
* Horner's method
* 腳本

------------
## 本章指令集

| 指令 | 堆疊效果及指令說明                        | 口語唸法 |
|-----|----------------------------------------|--------|
| `:` | ( "name" -- ) &emsp; 定義一個名為 name 的指令後,進入編譯模式 | colon |
| `;` | ( -- ) &emsp; 結束一個冒號定義,會编譯一個結束指令 EXIT 並且結束編譯模式 | semicolon |
| `."` | ( "text" -- ) &emsp; 編譯其後直到下一個 " 的文字,並於未來指令執行時印出此文字 | dot-quote |
| `.s` | ( ... -- ... ) &emsp; 顯示但不改變資料堆疊和浮點堆疊的內容 | dot-s |
| `dup` | ( n -- n n ) &emsp; 複製資料堆疊頂端的數 | dup |
| `drop` | ( n -- ) &emsp; 拋棄資料堆疊頂端的數 | drop |
| `nip` | ( n1 n2 -- n2 ) &emsp; 拋棄資料堆疊頂端數來第二個數 | nip |
| `swap` | ( n1 n2 -- n2 n1 ) &emsp; 交換資料堆疊頂端的兩個數 | swap |
| `over` | ( n1 n2 -- n1 n2 n1 ) &emsp; 複製資料堆疊從頂端數來的第二個數 | over |
| `rot` | ( n1 n2 n3 -- n2 n3 n1 ) &emsp; 旋轉資料堆疊頂端的三個數,使得第三個數被移到頂端 | rote |
| `-rot` | ( n1 n2 n3 -- n3 n1 n2 ) &emsp; 旋轉資料堆疊頂端的三個數,使得最頂的數被移到頂端算來第三個位置 | minus-rote |
| `2dup` | ( n1 n2 -- n1 n2 n1 n2 ) &emsp; 複製資料堆疊頂端的兩個數 | two-dup |
| `2drop` | ( n1 n2 -- ) &emsp; 拋棄資料堆疊頂端的兩個數 | two-drop |
| `2swap` | ( n1 n2 n3 n4 -- n3 n4 n1 n2 ) &emsp; 交換資料堆疊頂端的兩對整數 | two-swap |
| `2over` | ( n1 n2 n3 n4 -- n1 n2 n3 n4 n1 n2 ) &emsp; 複製資料堆疊從頂端數來的第三、第四個數| two-over |
| `fdup` | ( F: r  -- r r ) &emsp; 複製浮點堆疊頂端的數值 | f-dup |
| `fdrop` | ( F: r -- ) &emsp; 拋棄浮點堆疊頂端的數值 | f-drop |
| `fswap` | ( F: r1 r2 -- r2 r1 ) &emsp; 交換浮點堆疊頂端的兩個數值 | f-swap |
| `fover` | ( F: r1 r2 -- r1 r2 r1 ) &emsp; 複製浮點堆疊從頂端數來的第二個數值 | f-over |
| `frot` | ( F: r1 r2 r3 -- r2 r3 r1 ) &emsp; 旋轉浮點堆疊頂端的三個數,使得第三個數被移到頂端 | f-rote |
| `\` | ( "comment" -- ) &emsp; 忽略其後到此行結尾的文字,可做為註解 | backslash |