2025年9月15日 星期一

為eForth設計高階定義的DO...LOOP指令群

為eForth設計高階定義的DO...LOOP指令群


曾慶潭 Ching-Tang Tseng
ilikeforth@gmail.com
Hamilton, New Zealand
16 September 2025


 

\ High level defined DO…LOOP words set for eForth
\ Author: Ching-Tang Tseng , New Zealand , 10 Dec. 2008
\ Reference: (1). FORTH DIMENSIONS, V. XIII No. 6 , p.34 , Rob Chapman, 1992
\ (2). MASTERING FORTH, Chapter 12, p.172 , Martin Tracy, Anita Anderson, 1989
\ (3). FORTH ENCYCLOPEDIA, Mitch Derick & Linda Baker, 1982 
\ (4). ALL ABOUT FORTH, Glen B. Haydon, 1982
\ (5). eForth Implementation Guide, Dr. C. H. Ting, 1990
: (DO)  ( limit, index - - )
 SWAP R> SWAP >R SWAP >R >R ;
: (LOOP)  ( - - )
 R> R> 1 + DUP R@ <
    IF >R DUP @ +
    ELSE R> DDROP CELL +
    THEN >R ;
: (+LOOP)  ( n - - )
 R> SWAP DUP R> + SWAP 0< OVER
 R@ < XOR
    IF >R DUP @ +
    ELSE R> DDROP CELL +
    THEN >R ;
: DO COMPILE (DO) HERE ; IMMEDIATE
: LOOP COMPILE (LOOP) HERE - , ; IMMEDIATE
: +LOOP COMPILE (+LOOP) HERE - , ; IMMEDIATE
: LEAVE R> DROP R> DROP R> DROP ;
: I=L? R> R> R> DDUP >R >R = SWAP >R ;
: ?DO [COMPILE] DO COMPILE I=L?
     [COMPILE] IF COMPILE LEAVE
     [COMPILE] THEN ; IMMEDIATE
: J R> R> R> R> DUP PAD ! >R >R >R >R PAD @ ;
: K R> R> R> R> R> R> DUP PAD ! >R >R >R >R >R >R PAD @ ;



為了方便讀者節錄程式,與本文切要相關的資料領文先行,引用時請尊重原始出處。

1. 緣起

採用eForth32發展地震早期警報系統期間,希望轉殖Tiny BASIC Compiler程式於此系統時,遇到下列設計:

… ?DO … IF…LEAVE THEN …+LOOP…

因eForth缺DO … LOOP指令群,致使希望直接引用程式的目的無法立即實現,甚覺遺憾。若欲修改程式,則因結構煩瑣,費時費事,因而暫時中止了發展,只希望有機會能設計出此指令群,以便日後又有其他程式想直接套用時,更為方便。
此事顯示在發展新系統的初期,發展者最好考慮歷史的包袱,不要將任何傳統習慣隨便棄之不顧,否則日後有好的現成程式可資利用時反成困擾。當然,eForth原作者繆比爾(Bill Muench)已經表明他個人喜好FOR…NEXT勝於DO…LOOP。只用FOR…NEXT也一定可以設計出任何你想要的有限迴路程式來,本人也數度如此實際運用於須要的程式中,證明所言不假。
我尊重Bill Muench的個人見解,但我也尊重任何可貴的前人設計,二者皆不可棄。欲解決因此而導致不能相容的問題,最好的辦法就是自己動手建立起兩者間的友誼橋樑,給自己也給世人帶來永久的便利,我參考了前人已經提供的資料與技術,整理出此指令群與世人分享。
所要設計的指令群,還強調必須用FORTH高階定義的方式完成設計,才符合eForth精神。此前,我們已經有無數的FORTH系統可供參考來設計此指令群,但全都是以低階方式完成的設計,只能參考,不能照抄。eForth精神的可貴處,就是在其發展初期,係以簡化整體結構為主,只有少數限量的基本指令要用低階方式設計,執行效率不是強調重點,系統中大部份指令則都用高階定義方式完成設計。因此,若欲將DO…LOOP指令群也加入eForth的範本系統,卻又不破壞其原始精神,就必須是高階定義方式完成的設計。
以eForth為起點發展出FORTH所有功能的觀念一直強烈的深植我心,我希望能以實務進行驗證。我已見到了許多人在各種CPU上的努力成果,這是橫向的實例,表示未來出現任何新的CPU硬體,FORTH都可在第一時間,以最迅速的手法立即跟隨實現。然而,向上繼續加強的垂直發展則仍有廣泛空間,很須要好手透過實際使用經驗來累積成果,達到所謂『不做不知,一做便知』的進展。我在這次刻意挑選典型eForth32系統來發展應用程式期間,發現了這個問題,覺得可能會是未來eForth被引用時的障礙,只要力有所逮,不必推卸責任,應該促其實現,因而花了一點心思,設計出可以久放的程式。

2. 程式設計經驗

完成這個指令群設計的經過是一次很好的經驗累積,其中有許多寶貴知識是從FORTH專業書本中學習得來的,也有自己經過思考後,新獲得了對FORTH的深刻體認。一般而言,很少有人會為文介紹FORTH的核心指令是如何設計出來的?就像學術論文的發表一樣,常略去一些寶貴的研究細節。我們看FORTH系統的源程式時,看到的是創作人的最終成果,看不到創作人研發期間的心路歷程。如果學習FORTH或用FORTH發展程式,能同時學到一些傑出的設計方法,必然是很有助益,本文留下一些此方面的記錄資料。

3. DO、LOOP、+LOOP

在廣讀群書後,找到一份最接近設計要求可供參考的文獻是來自FORTH DIMENSIONS 1992年3月出版的V. XII No. 6期刊,作者:Rob Chapman在期刊的讀者來書欄中提供了一些高階定義方式完成的FORTH指令設計,上列(DO)、(LOOP)、(+LOOP)三個核心指令便來自該文,純高階方式完成的漂亮定義,引用時僅將ENDIF改成對應的THEN,CELL在eFroth32中使用4,其他均直接引用。相關於此指令群的其他指令設計則未在文中出現,因此,必須另行設法。
由於此指令群除?DO指令外均屬於F83標準指令,在『FORTH Encyclopedia』及『All About FORTH』兩本書中可以找到指令的詳細說明與執行細節,整理出DO、LOOP、+LOOP三指令的相關設計,以倒敘法列出如下:

: DO COMPILE (DO) HERE 3 ; IMMEDIATE
: LOOP 3 ?PAIRS COMPILE (LOOP) BACK ; IMMEDIATE
: +LOOP 3 ?PAIRS COMPILE (+LOOP) BACK ; IMMEDIATE
: BACK HERE - , ;
: ?PAIRS ( value1, value2 - - ) – 13 ?ERROR ;
: ?ERROR ( flag, error message number - - )
SWAP IF ERROR ELSE DROP THEN ;

更進一步的ERROR指令追蹤不擬繼續了,因為早期系統的完整設計,多加了在編譯過程出差錯時要提示使用者訊息的功能,我們的eForth力求精簡,指令設計的內容能省則省,但仍要不失其基本功能,套用的結果就得到了本文中這三個指令的最精簡設計。

4. LEAVE

不幸,另一個同為標準指令的LEAVE,書中全以低階定義的方式進行解說,一時也看不出其關鍵意義,最後放棄了參考書本中的說明,改採自行思考尋求解決方法。
我記得fig與83標準的差異中,LEAVE性能不同屬其中之一,fig於LEAVE被執行時,在DO…LOOP中,由LEAVE到LOOP之間的剩餘程式仍然會被執行最後一次,而83標準則由LEAVE直接跳至LOOP之外。
根據這個靈感,產生了純用高階定義方式設計出LEAVE的方法,可以強迫留在回返堆疊內的兩個DO…LOOP有限迴路指標之值相等,系統便會在檢查指標值時因相等而跳出迴路,問題是這樣的LEAVE規格就等同於fig標準,自LEAVE至LOOP之間的程式仍然會被執行最後一次,它的設計方式如下:

: LEAVE R> R> DROP R> DUP >R >R >R ;

測試的結果也差強人意,勉強有了一個fig標準式的LEAVE指令可用了,但仍心存遺憾,還想從書本中找出更好的設計,使之符合83標準,可惜身邊資源並不豐富,網上FORTH DIMENSIONS資料要逐期瀏覽又非常辛苦,因此又回到自行思考的模式來解決問題。
有一天心血來潮,想到了如果能用強迫迴路指標值相等的方法來終止迴路,那麼,系統隨後豈不也得將回返堆疊上的兩個無用指標值丟棄以終止迴路?我何不更進一步,讓LEAVE主動進行丟棄的工作?在執行LEAVE時,迴路的兩個指標值放在回返堆疊的第二及第三個單元內,第一個單元內放的則是指到LEAVE後面那一個指令的位址,若要執行LEAVE離開迴路,此執行位址就可以不要了,因此,可以確定LEAVE被執行時應該一次要丟掉回返堆疊上最頂部的三個值,至於第四個單元內放的是那一個執行位址?當時我也毫無概念。
設計程式測試就會得知結果,因此,再將LEAVE指令重新設計如下:

: LEAVE R> DROP R> DROP R> DROP ;

測試結果令我驚訝!達到目的了!當我再度閱讀范瑞克(Rick VanNorman)在Os2中設計的eForth系統源程式時,見到了低階設計的LEAVE指令內容,它就是以組合語言直接執行將回返堆疊指標減去三個單位量的方式完成的,現在了解它的真正意義了,也知道回返堆疊指標用的是CPU內的暫存器,高階指令無法直接更改其內容,只好連續棄除三個值來達到目的,上列意外完成的設計是對的。

5. ?DO

這個指令不屬於標準指令,因此在上述基本參考書中找不到相關資料。它也不是一個非有不可的指令,一個?DO…LOOP形成的結構,可以用下列程式取代:

2DUP = IF 2DROP ELSE DO … LOOP THEN

亦即,前面一序列程式可以取代?DO,但最後面兩個黏在一起的指令也得同時被使用,才能取代整個?DO…LOOP,若所配合的成對指令是+LOOP時,情況相同。以人工方式調整出對應的程式,不是此處要強調的重點,設計出?DO才是主要目的。我也不希望因為要增加一個?DO,卻必須回頭去修改已經完成了的LOOP及+LOOP指令。如此一來,就得自行思考解決辦法了。
崔馬丁(Martin Tracy)是一位設計FORTH系統的個中好手,台灣Forth同好都不應忘記當年我們採用他設計的MicroMotion Forth-79系統當推廣標準時,只買了一份他的創作,他的系統非常好用,完全透通,他算是讓FORTH在台灣推廣起來的偉大貢獻者,我們應當經常心存感激。他也有不少有關FORTH的著作,在他的『MASTERING FORTH』這本書中,我找到了有關?DO指令的設計方法,第十二章介紹Compiling words時解釋得非常詳盡,MacFORTH系統也已經採用這個方法讓?DO指令存在系統之中。但是,研究他介紹的設計後,就可以發現,前面已經設計好的DO、LOOP、+LOOP的執行內容都得跟著再增加變化方能配合,非我所願。
拜讀Martin Tracy的大作,給自己增加了設計FORTH程式時的另外一個新的技術經驗,他特別指出:集結一序列指令來替代另一指令的技術,稱之為巨集指令擴充法。( The technique of substituting one sequence for another is called Macro expansion. ),也舉了一個簡單例子,介紹在FORTH環境中要如何運用這種技術?例子是:
: 2DUP OVER OVER ;
: 2DUP COMPILE OVER COMPILE OVER ; IMMEDIATE

這兩種設計完成的指令,都可以達到相同的功能,但在後續的使用上,後者被限制成要用[COMPILE]指令來配合編譯。可是當系統隨後執行到2DUP時,兩種設計就會呈現絕對不同的執行方式。僅對這兩種設計進行比對時,前者多了兩個額外負擔的動作,首先要將執行位址碼從FORTH系統IP擠入回返堆疊,然後才執行兩次OVER,末了還要多執行另一個動作,必須將原擠入回返堆疊的執行位址碼取回,置入FORTH系統IP。而後者就直接執行兩次OVER,完成2DUP所要求的功能。後面這個設計顯然執行速度較快,它就是一種所謂的巨集指令擴充法的技術,與一般FORTH ASSEMBLER中經常用到巨集指令的功能完全相同,習慣上又被稱之為使用了指令別名( Aliasing )。
這是一種很可貴的技術,也讓我能實現?DO的設計。因為我已解決了LEAVE指令的問題在先,利用已存在的LEAVE,以及Martin Tracy所傳授的巨集指令擴充法,我便可以不用更改其他DO…LOOP指令群內已有指令的內容結構,亦能完成?DO指令的設計。
如果再添加一個檢查迴路二指標數是否相等的指令如下:

: I=L? R> R> R> DDUP >R >R = SWAP >R ;

那麼?DO的實際意義就可以等同於下列程式的意義,但不能就這樣直接使用:

DO I=L? IF LEAVE THEN

巨集指令擴充法設計出來的指令,都必須是立即執行式的指令,因此,?DO最後被設計如下:

: ?DO [COMPILE] DO COMPILE I=L?
[COMPILE] IF COMPILE LEAVE
[COMPILE] THEN ; IMMEDIATE

有限迴路的使用,除了使程式能固定的執行幾次迴路外,迴路內部程式有時可能必須使用迴路被執行當時的指標量,對單一層的迴路而言,迴路指標的引用指令,習慣上稱之為I,這個指令對FOR…NEXT型的迴路或DO…LOOP型的迴路而言,沒有差別,均可使用此同一功能的指令I,而I的內容則為R@的意思,也就是取用回返堆疊頂端之值,便可得到迴路執行當時指標之值。
但若有限迴路的使用不只一層時,單使用一個I指令便無法獲得各層正確的迴路指標值,必須設計相同意義但內容不同的指標引用指令,我們習慣上將其稱之為J、K、L、…等,它們的高階定義型式設計,依理,便是由回返堆疊中更深位置的單元來取得,J為第四個單元,K為第六個單元,L為第八個單元…。

6. 延伸性的應用

如果完全了解上述說明,就可以進一步設計系統中須要的其他類似指令了,例如:eForth32中缺乏從FOR…NEXT跳出來的類似LEAVE指令,用騙的,騙系統迴路指標已經是0了的設計,就是fig標準的LEAVE,如下:

: LEAVE-fig R> R> DROP 0 >R >R ;

被執行時,從LEAVE-fig至NEXT的程式還會被執行一次。要符合83標準的LEAVE應該是:

: LEAVE-83 R> DROP R> DROP ;

為了尊重Bill Muench只設計FOR…NEXT的個人觀點,我贊成別的系統也可添加。而Win32Forth中缺此指令,我們也不該厚此薄彼,放任其缺乏而不顧,我為Win32Forth設計它們如下:

: (NEXT)
R> R> DUP
IF 1 - >R ABS>REL @ REL>ABS >R EXIT
THEN DROP CELL + >R ;
: FOR ( n - - ) COMPILE >R HERE ; IMMEDIATE
: NEXT COMPILE (NEXT) APP-, ; IMMEDIATE
: I@ R> R> DUP >R SWAP >R ;

看懂了前文,這裡就不用解釋設計內容了,在我心中,迴路指令群的紛爭想法,到此也已天下太平,皆大歡喜。

7. 其他的檢討

也許有人根本就不喜歡eForth,比較起來,其他的FORTH系統性能比eForth好太多了,所以不想浪費時間在搞這個舊系統、舊學問上面,可能會認為此處留下的相關文章形同無用之物,沒有價值。
我倒認為,做為一個FORTH的傳承者,眼光要放遠,不要拘泥於一時的不得意,而心胸狹隘放棄發展,要知道目前的流行並不代表未來的世界,試想,如果微軟明天就收攤了,那一種FORTH才表現了永恆?
eForth還有很多的不好用處,我所用的eForth32甚至於沒有FORGET指令,每次設計錯了,都得BYE掉重來,要上XP系統,就得換跑Os2的版本,它的EDITOR要藉助於Microsoft Word來編寫才好用,缺與視窗的連結功能……..等等等,許多的不方便,讓使用者覺得真是不好用。也許只有我們從艱苦環境中長大的人才能忍受這許多的不便,但要知道,全世界公認最好的poly Forth系統,它在使用上也一直都有許多的不便,它卻依然賣得很好,顯然很多人買來做了很多事情,總不會一直有人花大錢買它,只是為了表示自己在FORTH領域的身份吧?
作者花費了許多時間在與eForth相關的系統上,主要目的是想驗證此系統是否真正具有一貫性,真有則更可貴,別的FORTH系統較不能存活久遠,eForth希望較大。證明的方法無他,就是用它來進行實際設計,每逢阻礙就予以突破,走到盡頭就知道它很可行,如此而已,我也身體力行了,證明了它很可行。
FORTH在軟體界號稱可像一個『窮人的原子彈』,我自己深信,個人、不花錢、猛搞一通、將它搞大了,確實可以轟轟烈烈的爆炸一場,就像在網路上大作戰一般,只是事在人為,宜不宜如此做而已。
網路讓全中國的中國人有機會來接觸FORTH了,我不知道這些同胞能不能像我們當年一樣吃苦耐勞的學通FORTH,然後用它來做可貴的事情,整個華人的世界,比台灣大太多了,相對的發展潛力也將非常可觀,我常寫FORTH相關文章,寄望的也就是華人的世界在這方面能有點作為。
剛開始搞FORTH的人,可以不要把事情搞得又大又複雜,我先翻譯了F83標準的中文指令集,因為它規模較小,提供初學者參考,意思是想告訴想進入這個領域的新人,先搞這些就很夠了,初看的人最好將其妥善保存。
將本程式加入eForth核心系統後,除了FORGET指令外,大致已符合F83標準,足夠使用了。回顧FORTH標準指令的發展,事實上從無、到fig、到79、到83、再到ANSI,彼此之間差異不是全面的,而且還很有限,學通FORTH就知道了。

2025年9月2日 星期二

理論與實際

理論與實際


曾慶潭 Ching-Tang Tseng
ilikeforth@gmail.com
Hamilton, New Zealand
2 September 2025


子彈跟砲彈都不是飛彈,發射出去以後的飛行軌跡無法修正,它們的理論彈道是固定的,如果不考慮環境因素的影響,也不考慮彈藥質地的統計性差異,落點就是確定值。排除這些因素,根據理論公式設計砲彈射擊程式,是件趣味事情。

我從網上的社群網頁收集到這一張簡圖,它就在顯示砲彈彈道的理論公式,我拿它來設計程式,實際上的有用應用,應該還要添加以程式修正發射角度的能力。


我是職業軍人,只是專長比較特殊,從中正理工學院核子工程系畢業後,必須前往最基層的正規野戰部隊服役,我前往的部隊番號是陸軍第 57 師,這個師傳統上對軍官的要求比較嚴格。我駐防馬祖時,幹到 57 師連長後才考進中山科學研究院,看過軍史記錄,確認 1941 年 57 師打過長沙會戰,1974 年,連上還有經歷過長沙會戰的老士官班長。 57 師是輕裝師,新報到的軍官一律拔掉軍階,在幹訓班重訓單兵、伍、班、排之槍砲操練三個月,成績記入軍籍表。


以砲操為例,解釋理論彈道公式的用途,才能結合理論與實際來設計程式。

砲操的射擊口令是這樣喊出來的:

第一砲注意
12 點鐘方向
目標正前方機槍陣地
俯角 30 度
榴彈一發
裝藥兩包


戰場的口令都很簡潔,我們從口令的內容來解釋理論彈道公式中那些參數比較有用。

一門砲有裝填彈藥的士兵、操作瞄準具的士兵、射手。

聽到 12 點鐘方向口令時,砲口會轉置到正前方的大約位置。
聽到目標正前方機槍陣地口令時,瞄準手會進行精確的直線對準瞄準。
聽到俯角 30 度口令時,瞄準手會將發射砲口調整出俯角 30 度的角度。
聽到榴彈一發口令時,彈藥兵與裝填手會將一發榴彈砲彈塞進砲管發射座。
聽到裝藥兩包口令時,彈藥兵會塞進兩包炸藥包,關上砲栓。
聽到放的口令時,砲彈就發射出去了。轟的一聲,震耳欲聾。

現在回頭看彈道貼圖,砲彈射擊時,完成砲口直線瞄準後,確實只剩二維內的事項必須考慮,而且只須調整砲口的發射俯角單一個參數。

那一種砲彈、裝藥幾包,決定了砲彈的發射初速,這項決定由喊口令的砲長負責,砲兵只管操作。換句話說,每次發射砲彈,最後,只有一個參數需要調整,也就是發射俯角。

從實務的觀點來看貼圖,實際上彈道的軌跡,也就是 y 與 x 的關係,在沒有繪圖功能時,不太有用。我只令 x 等於最高高度之值來敷衍過去。

這樣理解砲操,就可以開始設計程式了。我設計的數學計算系統很簡明,彈道公式完全可以直接搬入程式。為了表示式能更清晰的顯示,幾個用到的三角函數數值,直接在輸入數據後就立刻執行,產生結果,所以我不吝於變數用量,直接宣告出 14 個變數的名稱來設計程式。

砲彈不是發射出去就算了,砲長直接觀察落點,或前進觀測官會回報落點與目標差了多少角度與多少公尺。砲長在發射下一發砲彈時就得修正涉及參數。修正的主值,除了直線瞄準外,仍然是發射俯角需要改為幾度。

於是,我再根據公式,增加設計了一個 adj 指令,這個指令可憑需要增加或減少落點的距離直接換算出新的發射俯角,提供的答案仍是砲口應該升或降的新角度。應該增加的公尺數用正值輸入,必須減少的公尺數用負值輸入。操作方式只用兩個指令,執行 main 指令時,只根據結果印出彈道相關之數據供砲長參考。需要修正發射俯角時,執行 adj 指令便可。程式附帶簡單的操作,列示如下:


\ shell.f

\ degree : 發射俯角(度度量)
\ z : 弳度量
\ T : 飛行時間 ( sec )
\ R : 炸點距離 ( m )
\ H : 最高高度 ( m )
\ y : 彈道軌跡 ( m )
\ x : 水平距離 ( m )
\ u : 砲彈初速 ( 250 m/sec )
\ g : 重力加速度 ( 9.8 m/sec^2 )
\ a : 調整距離 ( +/-m )

14 reals degree z u g T H R y x sinz sin2z cosz tanz a

: InputData basic
10 let { degree = 30.0 e 0 }
20 let { z = degree * f(pi) / 180.0 e 0 }
30 let { u = 250.0 e 0 } 
40 let { g = 9.8 e 0 }
50 let { sinz = sin ( z ) }
60 let { sin2z = sin ( f2.0e0 * z ) }
70 let { cosz = cos ( z ) }
80 let { tanz = tan ( z ) }
90 end ;

: main basic
10 run InputData
20 let { T = ( f2.0e0 * u * sinz ) / g }
30 let { H = ( (  u * u ) * ( sinz * sinz ) ) /  ( f2.0e0 * g ) }
40 let { R = ( u * u ) * sin2z / g }
50 let { x = H }
60 let { y = x * tanz - ( g * x * x ) / (  f2.0e0 * u * u * cosz * cosz ) }
70 run cr ." 發射俯角 = " degree f.  ." 度 "
       cr ." 飛行時間 = " T f. ." 秒 "
       cr ." 最高高度 = " H f. ." 公尺 "
       cr ." 炸點距離 = " R f. ." 公尺 "
       cr ." 砲彈初速 = " u f. ." 公尺/秒 "   
\ 70 print { z , T , H , R , u }
80 end ;

: adj basic
10 run cr ." 輸入修正距離為幾公尺? " cr
20 InputR a
30 let { R = R + a }
40 let { z = asin ( ( R * g ) / ( u * u ) ) / f2.0e0 }
50 let { degree = z * 180.0 e 0 / f(pi) }
60 run cr ." 修正射角成為 " degree f. ." 度 " cr
70 end ;

hing@ctt:~$ ./l

AMDX86 ciforth 5.3.0 
fload shell.f
g : ISN'T UNIQUE                                                
A : ISN'T UNIQUE                                                
 OK
main

發射俯角 = 30.0度 
飛行時間 = 25.510204081秒 
最高高度 = 797.19387755公尺 
炸點距離 = 5523.1211976公尺 
砲彈初速 = 250.0公尺/秒  OK
adj

輸入修正距離為幾公尺? 

? +230.0 e 0

修正射角成為 32.21704191  度 
 OK
adj

輸入修正距離為幾公尺? 

? -45.0 e 0

修正射角成為 31.756360366  度 
 OK