Forth 語言是 1960 年代末期,由 Charles H. Moore 發展出來,做為天文台電腦自動控制與程式設計的一種程式語言。在80 年代,一群愛好者成立了 Forth Interesting Group(FIG),在世界各地推廣,並在各類電腦上建立 Forth 系統與標準語言。
Forth 以可延伸的辭典為核心,使用堆疊(Stack)為基礎,是一種高度模組化的程式語言,更特別的是一種將解譯器與編譯器合併使用的系統。在開發的過程中,可在編譯過程裡檢查系統狀態與錯誤,並且逐步擴展程式功能。
因為符式語言使用後敘式,跟我們常用的中序式不太一樣,也是一般使用者不容易入門的原因。舉個實例來說,我們平常用 MicroPython 計算 (3+7) * 5 / 2 ,使用 Forth 語言則要改寫成 3 7 + 5 * 2 /,有沒有發覺什麼地方不一樣呢?是不是括弧都去掉了?雖然輸入的順序不太一樣,但可以省略掉這麼多括弧,是不是覺得 Forth 語言很特別?
各位讀者如果對於 Forth 語言有興趣,可以參考 Forth Inc. 的 「Starting Forth」的入門書,您會對於 Forth 這個語言有著更深一層的認識。
我們可以從幾個實例來了解Forth 語言的優越性,「簡單就是好」。因為系統核心簡單,容易專注在要處理的問題上。
太空應用: 歐盟的 Philae 衛星 https://en.wikipedia.org/wiki/Philae_(spacecraft) 上面的主要 CPU ,使用的是 RTX 2010 。
PowerPC 時代的蘋果電腦,開機時按住 command + Option + O + F 鍵,會進入 Open Firmware 環境。這個 Open Firmware 底層就是使用 Forth 語言開發的。您可以使用內建的相關指令,對於整台麥金塔電腦進行硬體檢測。甚至是開發應用程式或遊戲。
2003 年左右,丁陳漢蓀博士出版了「嵌入式系統,使用eForth」這本書,告訴大家如何使用Forth 語言與 VHDL 硬體描述語言,設計一台電腦的硬體、系統程式與應用軟體。
以我們的程度要寫一個 Forth 語言模擬器是相當困難的事情。因此在網頁上尋找相關範例是比較可行的做法。過去筆者曾參加過臺灣符式推廣協會的活動,知道該會會員曾經寫過 JeForth 與 PeForth 這兩個以別種程式語言開發 Forth 模擬環境的系統。陳厚成先生編寫的 PeForth 環境也有收入到 Python 套件庫中,您可以在 Thonny 的套件管理程式搜尋並安裝。
不過 PeForth 功能強大,但原始碼超過兩千行,初學者實在是無法讀懂。筆者又在另外一個教學網站中找到程式碼更少,解釋註解詳細的範例程式,僅有176行程式。請各位讀者參考 https://www.openbookproject.net/py4fun/forth/forth.html
可以試著用瀏覽器的翻譯功能,來看懂裡面的解說。
Forth 模擬器的程式碼在此:
https://www.openbookproject.net/py4fun/forth/forth.py
一開始,筆者也很擔心要將純 Python 開發的模擬器移植到玩學機上很困難。移植程式到新環境上常會遇到幾個問題:
經過實驗後,筆者發現自己真的是想太多。原則上只要將原始程式的第5行與第16、17行註解掉,然後另存為 F.py 。再將此檔案上傳到玩學機的檔案系統中。就可以讓 Forth 的模擬器在玩學機上執行。
我們把原程式第 102 行
pcode = []; prompt = "Forth> " 修改為 pcode = []; prompt = "OK> "
符合Forth 語言編譯器的慣例。
符式語言系統內分為系統可以立即執行的字(指令)或需要編譯過的字(指令)。我們在這個範例中,教大家如何定義可以立即執行的字。在原始程式,是從第52行開始一連串定義了類似
def rAdd (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a+b)
這樣的定義。
還記不記得 Forth 要怎麼計算加法? 原本 1 + 3 要改成 1 3 + 。那我們要怎麼用Python 語言來定義這個符式字呢?看看上面這段程式範例,我們來逐個指令解釋:
我們可以試著做一個一次複製兩個堆疊元素的 2Dup 指令,建造方法如下:
def r2Dup(cod,p) : ds.append(ds[-2]); ds.append(ds[-2])
指令建造好後,要在 rDict 物件中加入指令名稱,這樣 Forth 引擎才會找得到您定義的字,系統才可以正常執行該指令。
最後將 F.py 存檔,傳到玩學機後,重新啟動玩學機,到命令列下執行
import F as F
F.main()
就會叫出 Forth 模擬器,您就可以用 Forth 語言來開發應用程式。
我們試著做幾個 Forth 字來控制玩學機:
程式定義如下:
檔案存檔,重開機並啟動Forth 環境後,就可以執行相關的定義字。
清除畫面,請執行 CLS。這時您會發現螢幕會變成全黑的畫面。
要將畫面清除成紅色背景,請執行 SCR_RED。
要畫一條紅色的線,則需要輸入 X1, Y1, X2, Y2 與線段顏色 五個參數,請參考底下的指令來執行
CLS
0 0 64 60 RED LINE
這時您就會看到玩學機中畫了一條紅色的線。
讀者可以試著定義更多的指令,您就會了解使用 Forth 開發軟體的好處。
Forth 引擎範例程式
本程式是修改自 https://www.openbookproject.net/py4fun/forth/forth.html 的原程式,未來在使用與散佈程式請記得尊重原作者的智慧財產權,請務必說明相關引用資訊。
# forth.py
# 原始程式: 引用自 https://www.openbookproject.net/py4fun/forth/forth.html
# 程式修改: Samsuan Chen, Derek Lai, Daniel Teng 2023.09
# =========================================================================
# ============================= 引用模組
import sys, re
import wifiboy as wb
#============================= 全域變數宣告
ds = [] # The data stack
cStack = [] # The control struct stack
heap = [0]*20 # The data heap
heapNext = 0 # Next avail slot in heap
words = [] # The input stream of tokens
initCode = ""
#============================= 主程式
def main() :
global words, initCode
# if len(sys.argv) > 1 :
# initCode = open(sys.argv[1]).read() # load start file
# MicroPython 不支援直接使用 python F.py 加入參數的使用法
while True :
pcode = compile() # compile/run from user
if pcode == None : print(""); return
execute(pcode)
#============================== Lexical Parsing
def getWord (prompt="... ") :
global words, initCode
while not words :
try :
if initCode : lin = initCode; initCode=""
else : lin = input(prompt)+" "
except : return None
tokenizeWords(lin)
word = words[0]
if word == "bye" : return None
words = words[1:]
return word
def tokenizeWords(s) :
global words # clip comments, split to list of words
words += re.sub("#.*\n","\n",s+"\n").lower().split() # Use "#" for comment to end of line
#================================= Runtime operation
def execute (code) :
p = 0
while p < len(code) :
func = code[p]
p += 1
newP = func(code,p)
if newP != None : p = newP
# ================================ 定義字(可直接執行) ================================
def rAdd (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a+b)
def rMul (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a*b)
def rSub (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a-b)
def rDiv (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a/b)
def rEq (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a==b))
def rGt (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a>b))
def rLt (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a<b))
def rSwap(cod,p) : a=ds.pop(); b=ds.pop(); ds.append(a); ds.append(b)
def rDup (cod,p) : ds.append(ds[-1])
def rDrop(cod,p) : ds.pop()
def rOver(cod,p) : ds.append(ds[-2])
# ========= Sam Start
def r2Dup(cod,p) : ds.append(ds[-2]); ds.append(ds[-2])
# def r2Dup (cod,p) : rOver(cod,p); rOver(cod,p)
# ========= Sam End
def rDump(cod,p) : print("ds = %s" % ds)
def rDot (cod,p) : print(ds.pop())
def rJmp (cod,p) : return cod[p]
def rJnz (cod,p) : return (cod[p],p+1)[ds.pop()]
def rJz (cod,p) : return (p+1,cod[p])[ds.pop()==0]
def rRun (cod,p) : execute(rDict[cod[p]]); return p+1
def rPush(cod,p) : ds.append(cod[p]) ; return p+1
def rCreate (pcode,p) :
global heapNext, lastCreate
lastCreate = label = getWord() # match next word (input) to next heap address
rDict[label] = [rPush, heapNext] # when created word is run, pushes its address
def rDoes (cod,p) :
rDict[lastCreate] += cod[p:] # rest of words belong to created words runtime
return len(cod) # jump p over these
def rAllot (cod,p) :
global heapNext
heapNext += ds.pop() # reserve n words for last create
def rAt (cod,p) : ds.append(heap[ds.pop()]) # get heap @ address
def rBang(cod,p) : a=ds.pop(); heap[a] = ds.pop() # set heap @ address
def rComa(cod,p) : # push tos into heap
global heapNext
heap[heapNext]=ds.pop()
heapNext += 1
# ===================== Daniel 定義 Start =========================================
# def rdotS (cod,p) : print("ds = %s" % ds);
def rCLS (cod,p) : wb.cls(); # 清除螢幕
def rRED (cod,p) : ds.append(wb.RED);
def rSCR_RED (cod,p) : wb.cls(wb.RED); # 將螢幕清成紅色
def rLINE(cod, p):
print("Please input (x1, y1, x2, y2, color)!", len(ds))
if len(ds) >= 5:
color = ds.pop()
y2 = ds.pop()
x2 = ds.pop()
y1 = ds.pop()
x1 = ds.pop()
wb.line(x1,y1,x2,y2,color)
# ===================== Daniel 定義 End ==================================
rDict = {
'+' : rAdd, '-' : rSub, '/' : rDiv, '*' : rMul, 'over': rOver,
'dup': rDup, 'swap': rSwap, '.': rDot, 'dump' : rDump, 'drop': rDrop,
'=' : rEq, '>' : rGt, '<': rLt,
',' : rComa,'@' : rAt, '!' : rBang,'allot': rAllot,
'create': rCreate, 'does>': rDoes,
# ==================== Samsuanchen 新增
'2dup' : r2Dup,
# ==================== Daniel Teng 新增
'.s' : rDump, 'cls' : rCLS, 'scr_red' : rSCR_RED, 'red' : rRED, 'line' : rLINE,
}
#================================= Compile time
def compile() :
pcode = []; prompt = "OK " # 修改成符合 Forth 語言的習慣,OK 代表一切安好。
while 1 :
word = getWord(prompt) # get next word
if word == None : return None
cAct = cDict.get(word) # Is there a compile time action ?
rAct = rDict.get(word) # Is there a runtime action ?
if cAct : cAct(pcode) # run at compile time
elif rAct :
if type(rAct) == type([]) :
pcode.append(rRun) # Compiled word.
pcode.append(word) # for now do dynamic lookup
else : pcode.append(rAct) # push builtin for runtime
else :
# Number to be pushed onto ds at runtime
pcode.append(rPush)
try : pcode.append(int(word))
except :
try: pcode.append(float(word))
except :
pcode[-1] = rRun # Change rPush to rRun
pcode.append(word) # Assume word will be defined
if not cStack : return pcode
prompt = "... " # 指令還沒輸入完成
def fatal (mesg) : raise mesg
def cColon (pcode) :
if cStack : fatal(": inside Control stack: %s" % cStack)
label = getWord()
cStack.append(("COLON",label)) # flag for following ";"
def cSemi (pcode) :
if not cStack : fatal("No : for ; to match")
code,label = cStack.pop()
if code != "COLON" : fatal(": not balanced with ;")
rDict[label] = pcode[:] # Save word definition in rDict
while pcode : pcode.pop()
def cBegin (pcode) :
cStack.append(("BEGIN",len(pcode))) # flag for following UNTIL
def cUntil (pcode) :
if not cStack : fatal("No BEGIN for UNTIL to match")
code,slot = cStack.pop()
if code != "BEGIN" : fatal("UNTIL preceded by %s (not BEGIN)" % code)
pcode.append(rJz)
pcode.append(slot)
# ==================== SamSuanChen ===== Start
def cAgain (pcode) : # 無窮迴圈
if not cStack : fatal("No BEGIN for AGAIN to match")
code,slot = cStack.pop()
if code != "BEGIN" : fatal("AGAIN preceded by %s (not BEGIN)" % code)
pcode.append(rJmp)
pcode.append(slot)
# ==================== SamSuanChen ===== End
def cIf (pcode) :
pcode.append(rJz)
cStack.append(("IF",len(pcode))) # flag for following Then or Else
pcode.append(0) # slot to be filled in
def cElse (pcode) :
if not cStack : fatal("No IF for ELSE to match")
code,slot = cStack.pop()
if code != "IF" : fatal("ELSE preceded by %s (not IF)" % code)
pcode.append(rJmp)
cStack.append(("ELSE",len(pcode))) # flag for following THEN
pcode.append(0) # slot to be filled in
pcode[slot] = len(pcode) # close JZ for IF
def cThen (pcode) :
if not cStack : fatal("No IF or ELSE for THEN to match")
code,slot = cStack.pop()
if code not in ("IF","ELSE") : fatal("THEN preceded by %s (not IF or ELSE)" % code)
pcode[slot] = len(pcode) # close JZ for IF or JMP for ELSE
cDict = {
':' : cColon, ';' : cSemi, 'if': cIf, 'else': cElse, 'then': cThen,
'begin': cBegin, 'until': cUntil, 'again' : cAgain,
}
if __name__ == "__main__" :
main()