2026年5月2日 星期六

自建動態連結程式庫檔案

自建動態連結程式庫檔案


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


本文係十五年前還用 Win32Forth 系統時所留下的文章,是為了使用 FORTH 系統外部資源時所需要建立之一種觀念的文章記錄。

這是一種有關 FORTH 系統中,叫用微軟作業系統內應用程式界面(API)的觀念問題。這原本不是我喜歡談論的問題,我不喜歡 API ,所以也一直不去碰觸,我個人有能力完全隔離 API ,繼續輕鬆使用任何新出現的 FORTH 系統,對許多人而言,尤其是 FORTH 的新進使用者,則可能不習慣這樣子使用 FORTH 。

API 的出現,確實因大量污染了 FORTH 系統,而影響許多人保持接觸 FORTH 的意願,影響了FORTH 社群的發展,非我所願。有障礙,我們就突破,不如直接研究 API 。

我可以舉一個很簡單的實例,將 API 建進動態連結程式庫(DLL)式的檔案,由 FORTH 系統來自由叫用,讓大家都可以輕鬆的了解,這種使用的技術是如何被引進 FORTH 系統的?此後您就不用再摒棄它,繼而善用它。為此,我才在 60 歲時重新接觸 C 的環境,也只有這樣,才能解決這項別人不願意解決的問題,解決問題的過程,也算得上是一項寶貴的經驗,留下記錄,提供大家參考。

我有能力將實際範例寫得非常簡單易懂,只需四列簡直不像程式的 C 語言程式,而且其中還有兩列只是單個大括弧,就能辦到這樣的事情,但您得仔細的看完本文。這樣的教材,期望能夠告訴大家,精簡的傳遞我們專精的 FORTH 技術是很重要的,如果您是 C 式程式語言專家,也請不要輕視像這樣的 C 程式,它足以解釋許多人搞不清楚的問題,我相信任何人只要肯做,絕對能做得比我還好,我指的是要比四列還少,長篇 C 程式會讓 FORTH 的使用者看不懂,我實在是忍不住了才這樣做的,中國人避免用複雜問題欺侮中國人,四列就夠了。

開始研究問題時,上網瀏覽相關資料是免不了的事情,但很難找出一篇純粹以 FORTH 立場來實現這項技術的精簡文章,也就是說,還沒有這方面正式的相關 FORTH 教材,可以直接翻譯來供給大家參考使用。

我在完成解說問題前,習慣於自己先行實現論述的內容,網路上可以找出無數不在乎 FORTH 而偏重於 C 方面的資料,講述如何在 C 環境中建 DLL ,例如:您就由 Google 鍵入『building and using DLLs』,有數不清的文章可以免費閱讀,我看了上百篇,這些東西不像 FORTH ,是完全不透通的資料,您不能過問或自己追查資料內所敘述的操作步驟,生產 C 編譯器的公司發行產品時,資料內寫了使用者該這樣做,您就只好照著做,否則得不到該系統告訴您的結果,可是得到的結果卻不能用。我使用 FORTH 幾十年,從來沒受過這種氣,非常委屈,為了完成本文,我忍下來了,也希望非得用 C 來解決問題不可的電腦程式用法,這是最後一次。

網上資料通常使用了很大且空洞的篇幅,與花俏又多色的畫面來美化成品,當我記不住一大堆指定操作命令或參數用名時,想將關鍵對象摘錄下來,也出現了許多麻煩,要浪費時間去掉那些沒價值的東西,然後才能以比較節省成本的方式,將重要資料印出來仔細研究,避免浪費紙張、墨水,凡事我都講求節省,我只重內容而不重外表,因此,不會浪費紙張來印出視窗畫面。

想寫本文之前,心中早已具有在 FORTH 環境中如何使用 DLL 內之 API 的觀念,只是不曾依樣自建一個來試一試。這一次這樣做,當然不可能一次就能成功,而且每次在純 C 環境中試出一個成品後,滿懷自信的相信『 C 號稱其轉置能力比別種程式語言強』之說法,立刻就將結果轉進 FORTH 試用,但送進 FORTH 就是不能實現,結果不但不成,問題一耗就是三天不能前進。起先,我以為自己多年不接觸新進電腦系統,沒搞清楚一些奇怪的電腦術語,誤解了說明資料的意思,特地在參加本地 WLUG 組織 Linux 例行活動時,請教這裡的專家,是不是我誤解了術語?做了錯誤的『死性』操作。結果不但是無功而返,反而是我得大費口舌,必須對 Linux 專家講解 C 中的編譯程序概念,當然這些概念也不是我發明的,全是由其他相關資料讀來的,卻對我想解決的問題毫無幫助, Linux 專家大部份也對這些『死程序』一知半解,規定性的說明資料實在太多了,只講英文的電腦專家也看不懂那些英文,但那些英文沒有字典上查不到的單字。

我人在紐西蘭,能直接來往的環境中,現在已經沒有會講中文,還具備電腦技術與共同興趣的朋友,網路上呼叫台灣的朋友,實際上也沒那麼方便,鑽牛角尖的問題只好靠自己了。但是,我還是相當幸運,我有一個在大學裡主修過電腦科學的女婿,而且他仍在電腦科技的領域工作,只要我將電腦問題解釋清楚,通常他都可以協助我解決關鍵問題。女婿平日上班時要忙他自己的工作,假日裡需要累過幾天工作後的休息,我只好自己先行釐清問題,過濾出非問不可的項目,與女婿約好時間逐項解決,能完成這本文,寫下這些記錄,不得不感謝他的協助,這裡只有他能完全聽懂我向他解釋的 FORTH 觀念,他不用 FORTH ,但能幫助我解決 FORTH 以外的電腦問題。

從 FORTH 環境叫用 DLL 中的 API 技術,不是近幾年的最新技術,自從 DLL 使用資料公告給大眾,以便大家可以用它來發展軟體產品時, FORTH 也就同時建立了這項使用方法,但在 90 年代以前,沒有 DLL 的時代, FORTH 系統中不建這種功能,我們卻早已用過這樣的呼叫,只不過以前稱為作業系統 BIOS 中公用中斷程式 INT xx 的功能呼叫而已。

既然這是一項老技術,大家又不熟悉,要搞清它,最好就從一個 FORTH 系統的發展歷史資料中探討相關的問題,如此便能了解當初系統設計這項功能的來龍去脈。我們需要透過一個目前共同推廣的系統才能進行溝通,以 Win32Forth 系統為主,所以大家最好也應了解這個系統存在的可貴。我在網文中找到系統原始設計者支湯姆(Tom Zimmer)回答大眾的信件,才明確的知道某些動態連結程式送進 FORTH 系統時,跑不通的關鍵問題所在。我有種感覺,無論您學那一門學問,這門學問的發展歷史也該仔細讀一讀,否則就很難了解這門學問的精要,此即一例。另外,我從網文中也讀到了,當年歐洲 FORTH 組織,曾因 Tom Zimmer 公開 Win32Forth 系統,供大家免費使用,而頒給他一份金質獎章,這是我在 FORTH 界從未見過的殊榮,也令我更加尊重 Tom Zimmer 及其 Win32Forth 系統。

除此之外,我根據歷史記錄,仔細回顧所有業經公開之 Win32Forth 的所有版本,才知道只有 Fig 總會網站上提供的最原始 V4.2 版是原作者的創作,此後的所有其他版本,均經其他組織刻意修改過,而主要的修改內容,竟然就是以配上後來新出產的 .DLL 檔案內不同之 API 應用,為主要區別所在。讀者可以回顧 V4.2 原始版本的內容,眾多檔案中只有一個容量 382K 名為 wincon.dll 的檔案為這個系統所用 API 的主要來源,沒有其他的 .DLL 檔案了,後來的版本都加裝了其他的 .DLL 檔案,以便取得其他功能的 API 。

因此,V4.2 版以後的 Win32Forth 系統都很花俏,注重畫面表現,雖也修正了幾項後來發現的錯誤,實質內容並沒有很大的改變,但卻將幾項傳統上很有用的 FORTH 專屬性能,刻意的埋沒掉了,例如:將很有用的蛻變編譯(meta compile)操作,轉換成只選畫面中一個指定字母的『死性』操作,這樣做就讓後來接觸 FORTH 的人,更不容易了解蛻變編譯的概念。發行 V4.2 以後版本的幾個組織,好像有意扼殺 FORTH 的研究發展,希望大家只用他們做給您的系統就算了,這些新版本的性能除了畫面漂亮外沒有特出處。

只有一個組織,比較老實,網頁畫面記錄,直到 2009 年,依然只推行 V4.2 的版本,我完全能體會各個組織的用心,但我不願逐一介紹,或區分這些網站。 Tom Zimmer 個人已經不再在 Win32Forth 系統上下功夫,以後的路子要靠使用者自己走,如何走?也由使用者自己決定。也許了解這些歷史,才有一面鏡子,能夠照亮過去、現在與未來。

當初,我只選擇 Fig 總會提供的標準 V4.2 版本發展 ABC FORTH 是正確的,但我也不排斥別人的成就,我曾誇口,任何具有浮點計算功能的 FORTH 系統,我都有能力為它加裝我所設計、可用 50 年以上的 ABC FORTH 系統。目前號稱最新推出的 Win32Forth 版本是 V6.15 版,我當然也能加裝,為了實現我的信念,我就重新在這個版本上實現了同樣的設計,做完了這件事情,我就知道這個版本的問題在那裡了,所以敢進行上述批評。

不僅如此,我早已在數個 FORTH 系統中完成了 ABC FORTH 系統,但沒有必要繼續釋出,理由有三:
一是我不希望已經公開之 ABC FORTH 系統內的指令,被隨便更改,造成因為指令不標準,而無法使用那份『使用說明』的問題。
二是屢次重新編譯我的原始設計,是一件很容易辦到的事情,這樣做當然也容易造成它的不穩定,因此,我刻意將其固定下來後才公開推出。
三是我堅持暫時只在指定地區使用,一切相關發展便易於掌握,目前按照我的安排與大家的認真配合,應該可以達到這樣的要求。
ABC FORTH 系統架建在 Win32Forth 系統上,這個系統的所有問題,應該也都屬於我該關心的問題, API 是個問題,我就理所當然、義不容辭的應該協助解決,現在讓我們來仔細探討, API 是如何建在 DLL 中供 Win32Forth 使用的?

從網文上可以找到的相關資料,都是站在 C 的立場解釋 DLL 的,那是擅長 C 式程式語言者的做法。我要一反常態,從 FORTH 的立場來解釋這種應用,而且將純粹屬於在 C 環境內,如何叫用 DLL 中 API ?的問題排除掉,完全不予討論,那是只會搞 C 的人,才需要關心的問題,與我們無關。

在 Win32Forth 中,想要叫用一個 .DLL 檔案中的 API 來用時,可以簡單到只需使用兩個簡單現成指令 WINLIBRARY 及 CALL 。

假設,有一個在微軟作業系統中堪用的動態連結性程式,檔案的名稱為 mydll.dll ,而這個檔案內有許多具有指定名稱的『功能性程式』(請注意,並接受這個我所使用的適當專有名詞,如果您將這個術語套到文章中所有使用了 API 的位置,那麼,文章唸起來就通順了,而且,意義也淺顯了),在 C 中的術語稱為函數(function),它們實際上就是大家所謂之可以被外部程式叫來使用的 API 了, Win32Forth 也可以隨意叫來使用。我們再度假設,有一個典型的 API ,它的名稱就叫作 subb ,它的功能是將兩個整數相減後得到差值。就憑這兩個假設,就足以讓我們解釋清楚本文打算論述的主題了。此處『mydll』及『subb』是隨意給的命名。

mydll.dll 檔案一旦被放置在與 Win32Forth 系統同一個資料夾中, Win32Forth 系統執行起來之後,想直接使用這個 API 的操作,就只需要執行下列輸入指令,指令當然也能被設計進程式:

WINLIBRARY mydll.dll
10 3 CALL subb

其中,第一列是用來宣告將 mydll.dll 檔案的所有功能,連接進入 Win32Forth 系統所需使用的宣告式操作指令。而 subb 是檔案中的一個功能程式,亦即 API ,它此後就可以自由的被 Win32Forth 系統,以單一個 CALL 指令,如同上述的方式直接叫用了。這樣的教材只能告訴大家, Win32Forth 中要如何叫用一個API ,對已經存在此 FORTH 系統中,與微軟作業系統相關的原始 API ,確實只用 CALL 指令叫用就夠了,但它只是簡例,全面正式的使用,則另有當其他狀況出現時,必須注意的使用規則。例子舉得太簡單了,用法就不容易記住,但如果我教您如何親自來自製出這麼樣的一個 API 後,您就可以終生記得它的用法了。 接下來,我們不得不碰 C 了,但可以不必學得那麼多,有概念將其實現才是最重要的關鍵。

我剛開始嘗試設計這個教材時,規規矩矩的建立了好幾個標準的 C 程式語言的測試程式,吃過不少苦頭,唸了許多很基本與 C 相關的書籍,還做 60 歲自修老學生的筆記,記下 C 標準程式中有那些東西,例如:前處理指引、標準庫存程式、簡化程式中變數名稱之宣告方式、程式主體、程式敘述集合、庫存程式指令……等等等,一大堆術語,然後糊里糊塗的在 Linux 系統跑通了幾十個程式,到現在還是糊里糊塗的使用C系統,只會用系統來產生 .DLL 的檔案,但不會純用 C 自己設計一個能產生 .DLL 檔案的編譯器(Compiler)。

請不要笑我的學習經驗記錄怎麼這麼幼稚? BORLAND 公司才能設計出 TURBO C 之 compiler 的,我是何德何能啊?才玩幾天 C ,就想自己設計 compiler!直到現在,大約它外部 90% 的系統操作命令我都還不會用呢。可是,憑良心講,如果我已經在 FORTH 系統中下了同樣時間的功夫,我應該可以設計出迎合這種需要的一個簡單編譯器了,所以我是不是太小看 C 的 compiler 了?我不堅持我的答案應該是『是』或『否』,但一切事在人為。單純的用 FORTH 寫出能編譯出 .DLL 檔案的一個編譯器,不是大話也不是笑話, FORTH Inc. 發行的 swift FORTH 早就已經能夠辦到了,這個系統可以用 FORTH 直接設計出 .DLL 的檔案,十幾年前就見過它的廣告詞,幾萬元台幣才能買一套,所以大家都不知道,我也沒用過。學會能產生 .DLL 檔案的編譯器,對我而言並不困難,因為我熟悉 FORTH ,能讀通 swift FORTH 的程式,所以不是笑話。我雖對 C 只是一知半解,但已經敢寫這篇文章,教大家 FORTH 需要的部份了,這個教材裡面,一點都不需要涉及將指標指來指去,在 C 中耍帥的技術。

剛開始寫成的幾個程式,都還有 C 的規矩,等到我搞通問題,將結果送進 FORTH 系統,實際執行成功後,我就花時間搞調皮處理,目的就是想寫出本文,教大家一個簡單到不能再簡單之程度的範例,讓大家終生記得。

我的程式只有下列四列:

int subb(int x, int y)
{
return y – x ;
}

就這樣,沒了。這就是我全部的源程式教材,是不是很可笑?可是,它通過了編譯,編譯過程中沒有出現任何的錯誤訊息(Error message),程式被製成 14.7K 份量的 .dll 式檔案,送進了 Win32Forth 系統,實現了前述兩列直接執行的 WINLIBRARY 及 CALL 指令要求,我得到了正確答案 7。

從上列程式看起來,好像我很不會寫程式,不適合當程式設計師。我承認我的程式很不漂亮,很不規矩,很不標準,很不像樣,簡直就在亂搞,不像是程式。可是,都執行出了我自己想要的結果。這些程式跟高手的作品比較起來,我確實只能算是一個調皮的程式實踐者。這些東西我都沒有打算要賣錢,搞得那麼漂亮幹什麼?為了解決問題我才設計程式,不是嗎?只要能解決問題,我為什麼一定要守那些不必要的規矩?所以四列就是四列,不必多列。

接下來,我們要探討上述教材,能夠被執行出實質結果的一些關鍵問題,這一部份全都是重要觀念,文章可不是信手拈來隨便寫成的,得到確切的結論前,當然也必須實現遠多於需要的研究與測試。

四列 C 例程式, API 的名稱為 subb ,按 C 規矩來解釋:

執行 subb 前,系統堆疊上要先準備好兩個整數,一個是 int x ,另一個是 int y ,它們被包括在小括弧中,注意 x 與 y 被宣告的次序, x 在先, y 在後。 subb 功能程式的執行內容,就是被包括在兩個大括弧之間的程式敘述,此例被刻意簡化成只做一件事,就是將 y-x 計算後的整數結果回傳給系統,而且就將其放置在堆疊上。由於整個功能程式 subb 執行後的結果,回傳給系統的資料型態(Data type)為一個整數,因此,在 subb 的名稱前面,需要使用 int 前引。沒了,這就是全部 C 程式的詳細解釋。

我在 ABC FORTH 系統中,設計了屬於 BASIC 式程式的大括弧指令,也做了跟這裡大括弧之意義完全一樣的功能設計,但當初設計 ABC FORTH 前,從未探討過 C 中大括弧的真正意義。從這裡看起來, C 中大括弧的意義,跟我為 BASIC 設計的大括弧意義,完全一樣。換句話說,我個人就有能力設計出一些 C 的編譯程式,但我可以把編譯環境設計得比 C 更人性、更友善。

回到 Win32Forth 的環境,我們實現這份教材時使用了『10 3 CALL subb』一列參數與指令,系統執行時, 10 比 3 先進堆疊,傳到了 C 環境後, 3 卻先傳給x , 10 才隨後傳給 y ,但兩個數字都沒有消耗掉,仍然保留在 FORTH 系統的堆疊上,執行完 y-x 後得到了 7 ,這個數字再度被擠進 FORTH 的堆疊,才結束程式。所以, FORTH 系統執行完這一列程式後,堆疊上有 3 個數字: 10、3、7 ,這就是全部外顯的執行結果。

我設計的教材,指令執行前,需要兩個整數,而且故意用減法,兩種系統,在堆疊參數的傳遞上,誰先?誰後?的不同效果,才能顯現出來。程式雖簡單,卻代表了一切,兩個大括弧間的程式敘述內容想寫進多少?那是專搞 C 的人才需要傷腦筋的事情,與 FORTH 無關,所以您不必學,我也不必教。想設計出許多個不同的功能程式,全部放在同一個 mydll.dll 程式中備用,依上例增建與 subb 功能程式同樣的結構,就能辦到,這也不是我該教的問題。您有沒有想過?用這種方法可以全用 C 程式提供 FORTH 系統檔案界面功能、浮點計算功能、繪圖界面功能……等等,這也不是我想教的課題。全力以赴,都能辦到,但界面並不單純,例如:浮點數算完了還是得顯示出來,就又非用 FORTH 不可了,所以沒有人用 C 這樣子為 FORTH 系統設計程式。

至於 FORTH 與 C 兩個系統間的參數傳遞格式,我們只需就 FORTH 這方面提示一下就夠了。浮點數也能傳遞,但您要先搞清楚格式,兩者必須相同。如果傳遞的是記憶體位址,那麼,在 C 環境中使用的是絕對位址,在 Win32Forth 系統中使用的是相對位址,二者可執行 rel>abs 或 abs>rel 指令來換算。如果傳遞的是字串時,在 Win32Forth 系統中使用特殊的『z”………”』指令,來進行包裹。由於這種技術並非標準用法,而且我也不認為再過多少年後,這種使用技術還能永遠存在,所以這些說明,只宜作為應景教材。

※以下係關於 C 系統方面的操作細節,與 FORTH 無關。

使用 C 的系統來產生我們所需要的 .DLL 檔案,方法不是固定的只有一種,因為就 C 的立場而言,全世界所有供應 C 系統的公司,也未將此事統一起來。曾經較為流行的 C++ 或 C# ,所安排出來的作法,是在源程式緊接著前處理指引宣告之後,增加一列額外的宣告:

extern “C” {………………}

然後在功能程式(Function)的前引,再增加另一個宣告指令,例如:

_declspec(dllexpot) FUNCTION-NAME() {………………}

如此設計程式, C 系統才能編譯出您想要的 .DLL 程式來。也許其他的 C 系統還有其他的設計格式,但都只能從研讀發行 C 系統公司所提供的使用說明書中,才能得知詳細的使用方法,沒用到前沒有先行學習的價值。

這樣的用法,有一個很大的缺點,想造 .DLL 程式的人,必須取得 C 的源程式,然後明確的按照要求,如同上述的方式,在源程式的相關位置,填入上述宣告或指令。必須有源程式,還必須修改,系統程式可能還得花錢買,隨後還得進行特殊的編譯操作,才能得到可用的結果,以這種方式實現自行建造 .DLL 的整套方法,確實不太好。因此,我僅只保留收集所得的相關資料,而不進入那種環境去研究發展。

評估現況之後,覺得使用公益軟體,來了解這項 FORTH 與 C 間的初步性能,可能比較理想,於是先從 Linux 中的 gcc 開始發展, gcc 建 .DLL 的方式有一個很方便的特點,就是完全不用修改基本的 C 源程式內容,而只需在操作 gcc 的編譯過程中,使用指定操作命令按步驟操作,使用者只須集中注意力於編譯操作上。

C 的範例源程式並不難建立,但有經驗的使用者,建議使用只產生素文字的編輯器,設計 C 的源程式,在微軟作業系統中指的是記事本(NotePad)而非文件檔案編輯器(WordPad),以避免文字控制碼夾雜在源程式中,妨礙後續的編譯操作。

此前,我曾使用過此 Linux 系統中的 gcc ,編譯出 gForth 及丁陳老師傳給我的 eForth in C 源程式,所以系統值得信賴。根據收集到的資料顯示,以 gcc 產生 .DLL 檔案,只有兩次的操作,例如:源程式的檔案名稱假設為 mydll.c ,則先用此源程式編譯出一個物件檔案(Object file),操作方式為:

gcc –c mydll.c

操作完成後,可以 ls 操作命令檢查資料夾內的檔案,就會增加了一個名為 mydll.o 的物件檔。接下來,要用這個物件檔,直接產生一個外界可以使用的動態連結檔,也就是我們想要的 .DLL 檔案。操作時要用到一個意義上為外界可以共享(Shared)的操作命令,操作方式為:

gcc –shared -o mydll.dll mydll.o
如此便完成了我們想要的 mydll.dll 共享式庫存程式檔案,這兩個操作程序,算是已經被我濃縮到了最簡單的程度。

隨後,我利用 E-mail 傳送信件時夾帶檔案的方式,發了一封自己傳給自己的信件,再到另外一台電腦上取出此 mydll.dll 檔案,放置在 Win32Forth V4.2 系統的資料夾內,立刻以上述指令直接操作測試,不幸失敗了,失敗現象是一執行到叫用的功能程式時,整個視窗就立刻消失了,因不留痕跡,無法除錯。

檢討問題時,直覺想到也許就是因為作業系統完全不同,所以不能這樣用。於是在網路上下載了一個 cygwin 的系統,它是一個在微軟作業系統中模擬 unix 作業系統的設計,提供相同功能的 gcc ,我在這個系統中重新做了許多次與 Linux 中完全相同的操作,並將結果直接送進 Win32Forth V6.14 測試,仍然失敗了,失敗現象比前一狀況好了一些,視窗沒有消失,畫面上顯示錯誤訊息,告知找不到一個稱為 cygwin1.dll 的相關庫存程式,於是,我從下載的系統 bin 資料夾內複製了這個程式,併同 mydll.dll 一起放進 Win32Forth V6.14系 統的資料夾內,再度進行同樣的操作,仍然失敗,視窗畫面也消失了。

最後,我仔細思考問題所在,並從網路上印出了一篇 Tom Zimmer 於 1997-12-04 寫給 Thomas 的網路論壇公開信,信中強調, Win32Forth 當初設計時,考慮到微軟作業系統提供的動態連結程式庫,佔用了記憶體的固定位址,任何程式不得侵犯,否則就不能在微軟的作業系統中執行,因此, Win32Forth 系統中的程式碼改採以相對位址的方式被執行,速度會慢一點,但此系統便可以被彈性的安排在記憶體的任何位置來執行。信中也提到,凡想被 Win32Forth 系統叫用的 .DLL 程式,被叫用而進入系統時,絕對不可以使用與微軟作業系統衝突的記憶體位置。關鍵性的三句話是:

1. WindowsNT allows an absolute program in the area between 0x400000 and about 0x600000.
windows95 I believe places programs well below this.
So if you are specifying an absolute program address below 0x400000, then it won’t run under WindowsNT.
2. You have to be careful to not allocate the same absolute address to two Dlls.
3. This is basically why I decided to make Win32Forth use relative addressing. It is slower, but more flexible.

因此,我直覺上知道前述的兩個 gcc 都產製了與微軟作業系統衝突的 .DLL 檔案。要解決這個問題,只能找合乎規格的 C 編譯系統來用,因為只是試用,不值得花錢買任何系統,但公益系統必須是藉由微軟的基礎開發出來的系統,才會考慮產生的 .DLL 檔案程式,執行時不與微軟作業系統衝突的問題,我缺乏這方面的概念,只好找女婿幫忙,他很快的從網路上找到了我需要的東西,就是 MinGW 系統,裡面也有 gcc ,但要到 DOS 的原始視窗中去執行,執行的結果可以與 gcc 暫時同放置在 MinGW 的 bin 資料夾內,要使用時,才將 mydll.dll 檔案移到希望放置的資料夾中去。

做好了這樣的安排,一次便完成了希望的測試,得到了很正確的結果,證明了根據 Tom Zimmer 信中所言而思考的方向是對的。有了結果,我便有機會進行精簡範例程式的發展,最後,將寫成這篇教材所需要的範例程式,精簡成了只剩四列。

妥善操作 gcc 所需用到的整體操作命令選項(Overall options),是能夠完成自行產製 .DLL 檔案的最重要關鍵,有關整體操作命令選項的使用資料非常多,而且不容易建立概念,想熟悉它們,最好的方式,還是從網路上參考別人完成的實例,並練習實作後,體會操作結果所表示的意義,才能逐漸的熟悉起來,做這件事,必須在學習 C 程式設計方面下許多功夫,操作這些東西,完全沒有學問,所以我也很不想學。

講完了這些技術,並不表示一切就到此為止,您如果好學,在 FORTH 的領域,完全可以學通這種兩個程式語言間的橋接技術, FORTH 系統是完全透通的,使用 SEE 指令,可以直接追蹤這些指令是如何設計而成的,看懂了程式與系統的意義,您也可以在自己的環境中依樣設計一個,除了 FORTH ,這個世界目前還沒有任何其他程式語言,能夠提供如此健全的功能給使用者。

如果大家都從小就開始學 FORTH 程式語言,會有許多好處,它不僅只是可以供您寫程式解問題而已,它還提供了完全可以學得通的一切與電腦有關之哲理。如果您也像我一樣使用了 FORTH 已經 30 幾年,能夠像我一樣將 FORTH 系統執行的原理寫成文章,那麼,上述文章中大家可能會認為是我在講笑話的事情,就不再會被認為是笑話了。

我可以不去追蹤本文所論述的這項技術所牽扯到的指令,單憑自己的想法,設計出另一套由 FORTH 去叫用其他程式語言功能程式的指令來,問題只在於需不需要一直發展這樣的程式?我總認為別的程式語言最後都會死去,而永遠都可以存在的 FORTH ,只能在別的程式語言還活著的時候,跟它們連一連,等它們都死掉了,連在一起也就沒有任何用處了,到頭來還是只剩下 FORTH 。這幾十年來的日子, FORTH 是怎麼過的?好像是千山它都獨行,請不必相送。

早期的系統駐留功能(Resident 例如:side kick用法),我用幾個指令就能完成設計,只要不搞破壞,讓兩個系統可以串來串去,不也是互相叫用功能程式嗎?我們曾用這樣的方式請 BASIC 幫忙算浮點計算,也讓 FORTH 幫忙 BASIC 執行一些程式,將結果放進 BASIC 固定的參數堆放位置。

我親自設計過好幾次多工程式(Multi tasking),用得漂亮時,只控管一個使用者指標 UP(User Pointer)的技巧,就能讓原 FORTH 系統完全停頓下來,轉執行其他任意的別種功能程式,別的功能程式結束了,仍然只依靠那個被控管的 UP 指標值,再執行很少的低階快速指令,就能恢復整個 FORTH 系統的繼續執行。 使用同樣的技巧,當然也能讓 FORTH 去執行 .DLL 內的功能程式。本文所探討的 FORTH 指令,大部份都在執行功能程式名稱的處理,接受 CALL 的後續輸入字串後,到 .DLL 的檔案內去尋找這個功能程式名稱所在的位置,找到了就取得這個功能程式該有的起始執行位址,合理控管 UP ,就能在執行完功能程式後,恢復 FORTH 系統的原有正常狀況。我也因此能夠體會出為什麼 Win32Forth 系統在執行完 CALL 功能程式之後,從堆疊傳遞過去的參數,依然還擺在原來的堆疊位置,交還給 FORTH 系統。我講得出這樣的執行機制,我為什麼不能自己也設計出這樣的程式來?

進一步誇口,我已經很熟練於操作我自己設計的檔案讀寫程式了,我能看到所有其他檔案程式的內容,我讀得進別人的標準結構,我當然也設計得出別人所需要的結構,因為工作只是填資料。那麼,前述所說的笑話就不是笑話,單用 FORTH 設計一個可以產生.DLL 程式供別人使用的檔案,當然能夠辦到,這就是產生 .DLL 程式編譯器的設計方法,我不是在講笑話吧?我用 C 辦不到,我用 FORTH 絕對辦得到,現今,別人已經辦到了,想做時,也能拿來參考。

而且,早就有商售 FORTH 系統,在網頁上做廣告,強調可以直接叫用 Visual BASIC 編譯產生的程式,我卻不認為這是甚麼偉大的新技術,也不值得投入太多精力去發展,因為, Visual BASIC 也早就開始逐漸地沒落了。叫用其它程式語言編譯出來的程式之技術,最後的處境都將相同,等到發行公司因無利可圖而不再賣這個系統時,前面的努力,都會成為泡影。為了節省非常有限的 FORTH 人力,這種發展就不必重覆浪費了。

目前, C 式語言還在流行階段,熟悉這些同類程式語言的讀者,可以參考本章的內容,依樣畫葫蘆,滿足一下新奇感就夠了,我並不鼓勵長期這樣子發展系統,因為產生的結果,就像 C 式程式語言的本質一樣,系統本身是完全不透通的,對技術的傳承,完全沒有幫助。我也希望,此後永遠不再需要使用 C 來寫程式。


2026年4月15日 星期三

邏輯運算(Logic operation)

邏輯運算(Logic operation)


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


本文所探討的邏輯運算,為『條件整合』所需的邏輯運算,而非『位元操作』方面使用的邏輯運算。

為了讓ABC FORTH數學計算系統增添這種邏輯運算的功能,我才慎重其事的重新探討此一主題。討論的內容仍然憑藉著邏輯運算的傳統原始定義,但討論的結果,可以產生創新的作法,並實際實現將系統中的邏輯運算指令,設計成以適當的中文意思來表達,且令其在程式中的使用方式,符合中文的語法規格。

大體而言,數學計算系統中的處理對象,若能夠被明確的劃分成各個獨立模組,系統便容易規劃、設計、發展。舉例而言,ABC FORTH數學計算系統,將整數、浮點數、複數…等數字處理對象,都當作各自獨立的模組來考慮,模組化後可以大大的簡化整個系統,讓各模組的問題,只須在各模組的範圍內自行解決。

因此,在ABC FORTH系統中,欲添加邏輯運算的功能時,最佳的作法,當然也就是繼續將這項主題,考慮成一個新建的單一模組來設計程式,讓邏輯運算的位階等同於整數運算、或浮點數運算、或複數運算,是一個獨立的新模組。

事實上,邏輯運算在實際的科學應用中,原本就與各種數字的運算不相干。數字運算計算3乘2得到6,1加1得到2,配合上其他所有的運算,形成一套完整的數學體系。邏輯運算執行0與1的各種運算後,也會得到一些運算結果,但這裡的0與1不表數字,而是表真或假、對或錯、是或非、有或無、開或關、高或低…等等所謂的邏輯相對狀態,0與1僅只是借自數字系統的兩個元素當作符號,這個1也可以使用除了0以外的其它任何數字來代表,但都與數字的大小或正負意義完全無關。總而言之,邏輯運算原本就與各種數字運算無關。

邏輯學是研究理論數學、應用數學、高深數學之基礎工具,也一直都是電腦科技發展的基石,電子邏輯電路工程更是經常需要用到邏輯運算設計產品。數學計算系統不應排除邏輯運算的功能,它係電腦以執行軟體程式的方式,實踐邏輯學問,沒有邏輯運算功能的系統,性能便有所欠缺。因此,將全部的邏輯運算功能建立在數學計算系統中,是有必要的,健全的邏輯運算功能,也有助於處理人工智慧方面的程式。我將加建邏輯運算功能的方法搞成極其簡明的方式,當然也就有助於各方面的應用發展。

2010年七月,我才開始設想『如何增建處理邏輯運算的功能進入系統?』的問題,自覺很容易實現這樣的設計。但是,那時只是初步的構思,只考慮仿照一般傳統程式語言的習慣,暫時僅融入『而且』(AND)與『或者』(OR)兩個單純指令。這篇文章則進行廣泛性的探討,包括如何給予適當的邏輯算子中文名稱?我個人認為,應該研究過它們在系統程式語言中之表達位置後,方給予適當的命名,才會比較恰當。讓指令的名稱符合語言上下文的啣接要求,這樣才能有助於設計出具有優良語法的程式系統。

一般而言,若只討論邏輯運算不同的名稱,基本邏輯運算的算子共有六個,它們是:

(1) AND (2) OR (3) XOR (4) NAND (5) NOR (6) NOT。

在FORTH程式語言中,邏輯算子(Operator)所需要的算元(Operand)操作對象,除了NOT只需要先行提供一個算元外,其他五個,都需要先將兩個算元操作對象放在堆疊上,然後才能執行邏輯運算。執行的結果都只得到唯一的狀態表示值,並將其留在堆疊上,供後續指令使用。換句話說,FORTH的邏輯運算,是一種後算符式的運算。1994年訂定的ANSI FORTH指令標準中,只將前三個當作標準基本指令,後三個可以導得,設計如下:


 
: NOT	 0=		;
: NAND AND NOT ;
: NOR OR NOT ;


如果單純的以1表示『真』,以0表示『假』,來製作一份邏輯運算真假值表,NOT不製表,原為1則得0,原為0則得1,其它的邏輯運算真假值表示如下:


上述全屬邏輯運算的原始性質定義,我們透過它來討論問題。

學過電子學上邏輯電路設計技術的人,一定知道,只要根據單一種邏輯閘,NAND或NOR僅擇其一,把它當作萬用基本邏輯閘,就可以設計出另外五種邏輯閘的知識。目前,市面上販售所謂的以NAND或以 NOR單種電路製造的快閃記憶體,大概也就是依據這個道理設計而成的。

事實上,在FORTH程式語言中,我們單純的只以軟體程式來設計,也一樣可以完成這種方式的設計,只不過是我們另得配合使用簡單的堆疊操作指令來達到目的。

我們若只以NAND當萬用基本指令來設計時,其它的五個是:


 
: NOT	( n – n ) 	  	DUP NAND ;
: OR ( n1 n2 – n ) DUP NAND SWAP DUP NAND NAND ;
: AND ( n1 n2 – n ) NAND DUP NAND ;
: NOR ( n1 n2 – n ) DUP NAND SWAP DUP NAND NAND DUP NAND ;
: XOR ( n1 n2 – n ) OVER OVER DUP NAND NAND –ROT SWAP DUP
NAND NAND NAND ;

我們若只以NOR當萬用基本指令來設計時,其它的五個是:


 
: NOT 	( n – n ) 		DUP NOR ;
: OR ( n1 n2 – n ) NOR DUP NOR ;
: AND ( n1 n2 – n ) DUP NOR SWAP DUP NOR NOR ;
: NAND ( n1 n2 – n ) DUP NOR SWAP DUP NOR NOR DUP NOR ;
: XOR ( n1 n2 - n ) OVER OVER DUP NOR NOR -ROT SWAP DUP
NOR NOR NOR DUP NOR ;


這些FORTH指令簡單易懂,是初學者很好的程式設計練習指令,而且上述設計方法不是唯一解答,您若有興趣試一試自己的本領,不妨就自己另搞一套,並自行摸出如何證明自己的設計絕對正確的辦法。這些整理出來的結果,也是當初發展出eforth系統前,曾經被仔細探討過的題材,是想要將FORTH反璞歸真前的一個重要程序。

有了這些根據,我們可以開始探討如何將這些功能全部加進ABC FORTH數學計算系統的問題了。在這個系統中,若以BASIC式的方式來設計程式,則『條件整合』用的邏輯運算,只會出現在條件分支指令組IF…THEN之間,一個典型的『而且』(AND)指令使用範例如下:

100 IF ( A > 5 ) AND ( B > 5 ) THEN 700

其中,( A > 5 ) 與( B > 5 )是兩個範例條件,本文只使用它們來代表一切可能的條件,是假設性的算元,AND便對這兩個條件進行邏輯運算,所得結果用來決定是否分支到THEN後面所指示的前往位置700去?改成中文程式則為:

100 如果 ( A > 5 ) 而且 ( B > 5 ) 前往 700

這樣的程式書寫方式,幾乎與我們中國人的語言表達方式完全一致了。有這麼重要的意義在其中,就更值得為其它另外五個邏輯運算指令,好好的設計出適當使用規則。另外一個『或者』(OR)指令,性質與『而且』(AND)相當,就照上述安排,成為標準的中算符指令使用方式,非常理想,由英文譯成中文的指令名稱也很恰當,不須要再進行任何修正。

這兩個指令,還有一項特點,也就是可以連續使用無限次於IF…THEN之間,用幾次都不成問題。因為每經過一次邏輯運算之後,都只留下一個邏輯狀態值,繼續以上列中算符方式安排在程式中時,便也能繼續與後續的新條件亦即算元操作對象進行邏輯運算,直到不算了為止,仍只得到唯一的結果,供給『前往』(THEN)指令使用。

另外的四個慣常邏輯運算指令,在英文環境的適當表達方式,則與中文有所不同,這是我從上述真假值對照表中看出來的問題。

當然,表達方式可以有很多種,只要使用方式合理,使用者使用時,可以覺得足夠友善,都可以作為設計的依據,而且對我而言,都很容易設計得出來。例如:我可以懶到根本就將所有的邏輯運算當作也是一種函數來考慮,那麼,六個邏輯運算,在程式中,就可以全部統一成前算符方式,使用於程式設計中。

我在ABC FORTH系統整數體系內所設計之『位元操作』應用方式的邏輯運算,就是前算符的格式,以便讓這六個指令可以呈現函數表示式的性質,如果在IF……THEN間的『條件整合』情況,繼續延用前算符規矩,程式就必須寫成下列格式:


 
AND 	( ( A > 5 )  ,  ( B > 5 ) )
OR ( ( A > 5 ) , ( B > 5 ) )
XOR ( ( A > 5 ) , ( B > 5 ) )
NAND ( ( A > 5 ) , ( B > 5 ) )
NOR ( ( A > 5 ) , ( B > 5 ) )
NOT ( A > 5 )


系統將這些函數式的邏輯算子執行完畢之後,都只得到一個唯一的狀態值,當然就能提供後續的THEN指令使用了。可是,這樣子使用之後,就完全失去了與人類語言直接對應的關係,結果就等同於傳統FORTH中的後算符使用方式,這兩種類型都與人類慣常語言完全不匹配。

另外還有一項因素,上述AND與OR兩個較完美中算符式的設計,也是一般傳統程式語言中所採用的格式,若不跟著以同樣的方式使用,想轉譯舊程式進入ABC FORTH系統時,就會產生困擾。因此,必須保留這兩個邏輯算子,成中算符的格式來使用。

其它的邏輯算子就不是這樣了,如果生平根本就不用英文思考問題,不必管它XOR、NAND、NOR、 NOT的實質意義為何?把這些算子都譯成『與…進行某種邏輯運算』,那麼,前三個指令就設計成也是中算符就很理想,例如:

( A > 5 ) XOR ( B > 5 )

表示條件( A > 5 ) 與條件( B > 5 )進行XOR邏輯運算

至於NOT,則只好維持原來FORTH後算符的方式了,我在ABC FORTH系統中,就將英文的邏輯運算指令,設計成這樣的使用格式,但深覺它忽略了中文,不尊重我們中國人的存在。

考慮在中文程式中使用這些邏輯運算指令時,若純用中文進行思考,便能夠深刻的體會出指令名稱、實質意義,與語言規格間的重要關聯。為顧全大局,要讓這四個指令都必須以『後算符』的格式來使用,才比較恰當。為什麼?請回頭仔細看看邏輯運算真假值對照表中顯示的各種狀態,您就可以明白其中的道理。

我在台灣讀過的書本中,幾乎所有與邏輯算子有關的譯名,大約都被固定的翻譯成非常怪異難懂的名稱了。『XOR』被譯成『互斥或』,『NAND』被譯成『反且』,『NOR』被譯成『反或』,『NOT』大概被譯成『非』吧。如果要拿這樣的譯名來設計中文程式,那我寧可不設計程式了,不信請讀一讀下列用法,看看您能不能直接讀出程式的意義來?

100 如果 ( A > 5 ) 互斥或 ( B > 5 ) 前往 700

講實在話,我讀不出來。

於是,我為這四個邏輯運算指令想好了比較一致、比較恰當的中文名稱,將它們都設計成後算符的使用格式,並且在兩個條件算元操作對象之間,添加一個逗號『 , 』當作分隔符號,完成了設計。此處再度複製一份真假值表,以便就近參考,請先行觀察四個指令在表中呈現的意義,然後再看看分別被譯成的適當中文名稱,對照如下:



 
XOR		一是一非
NAND 不為皆是
NOR 兩者皆非
NOT 狀態相反


我在ABC FORTH系統中實現了這樣的設計,它們的使用規格則如下列:


 
100 如果 ( A > 5 ) , ( B > 5 ) 一是一非 前往 700
100 如果 ( A > 5 ) , ( B > 5 ) 不為皆是 前往 700
100 如果 ( A > 5 ) , ( B > 5 ) 兩者皆非 前往 700
100 如果 ( A > 5 ) 狀態相反 前往 700


如此一來,這些邏輯運算就都符合我們的中文語法了,而且想進行連續邏輯運算的程式設計時,使用小括弧,清楚的將各個條件算元操作對象,按同樣格式區分開來,也能完成沒有次數限制的邏輯運算。以前,從未有人這樣子翻譯這四個邏輯算子的中文名稱,我現在則以邏輯算子在中文話語中的上下文關係,作為考慮基礎,譯名也包括了中文的直覺意義,可以通順的使用於程式語言中,這樣做算是創舉。

我在習慣於快速中文打字的操作後,已經不再認為:給予指令四個字,例如:『狀態相反』,會比只給單個字,例如:『非』,的譯名要差了。好的指令中文譯名,應該是清楚第一,通順至上。在以打字方式設計程式的作業環境中,字有幾筆、幾劃?更不重要,今人考慮的問題,早已與前人大不相同。

邏輯運算的功能,完全能應用在任何需要邏輯運算的場合,包括設計電子邏輯電路時的應用,使用者可以在未接觸硬體前,先以執行軟體程式的方式來實現。但是這種邏輯運算,屬於『位元操作』應用方式的邏輯運算,並非『條件整合』應用方式的邏輯運算。因此,在ABC FORTH數學計算系統中,就必須使用整數體系內的六個函數式的邏輯算子來設計程式,有別於此處探討的邏輯算子。

邏輯運算是數學運算中的一門分支,本文僅只處理了六種慣用邏輯算子,如果另有新的邏輯算子產生,隨時可以迎合要求,將其加入這個完全按照自己的理念設計出來的ABC FORTH數學計算系統。

我可以不堅持自己的譯名就是最好的選擇,大家若有更高明的見解,也可以經過討論後,重新設計進系統,這就是不必受制於人的好處,我們隨時辦得到。

這樣子的研究,純粹衝著我們中國人自己的文化而來,打從幾十年前開始學習電腦程式語言起,我心中便對中國人想使用電腦設計程式時,始終受制於西方文化設計出來的規範,感到忿忿不平,如果自己完全沒有能力在這方面進行改善,那就不必空談,只能拈香跟拜(台語,表示只能忍受一切羞辱,任人擺佈的意思)。

整理出這樣的研究,它的好處在那裡?完全無利可圖,所以何必曰利?但是,如果大家都不願正視這樣的研究,那麼,只好一輩子永遠拈香跟拜。

現在不一樣了,我早已習慣於在自己的電腦上大肆使用中文,譬如打文章、寫信、作記錄、看資料、看新聞、看程式…等等,我也有能力設計出可以完全只使用中文來當指令的程式語言,那麼,為什麼不更進一步的發展?也許我的個人研究不太成熟、不夠完美、不算甚麼,但是,至少我已經能夠這樣做了,上述的中文程式語言功能,我確確實實的實現了!而且,我會繼續思考,繼續努力,繼續設計,在數學計算系統方面,別人能做甚麼,我也絕對能設計得出對等功能的系統,性能還可以中文為主,並且直接超越別人的設計!

與數學計算相關的大部份實際程式中,必須使用到『條件整合』式邏輯算子的情況非常簡單,幾乎只要準備AND與OR的功能就夠了。在進行專家系統方面的程式設計時,反而更需要全部邏輯運算的功能,尤其是如果想用中文來設計專家系統時,除了要讓執行結果絕對符合專家資格,不出差錯外,對於日後的程式維護,也要講求精簡明確,那麼,這一篇文章內所強調的要點,就會很有價值。

2026年4月2日 星期四

常微方程式之 Runge-Kutta 解法

常微方程式之 Runge-Kutta 解法


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


本文專注於引用單一文獻,將一個標準的 FORTRAN 程式,直接改寫成 ABC FORTH 數學計算系統可以執行的程式。改寫的方式幾乎是一列一列對應式的翻譯,執行解得的答案必須與原資料文獻一致,操作的便利性必須符合現今時代潮流,內容則是一個實用的數值分析標準範例。

參考文獻源自:

Brice Carnahan, H. A. Luther, James O. Wilkes, “Applied Numerical Methods”, Chapter 6. Example 6.3. p.367 ~ p.380,

“Forth-order Runge-Kutta method - transient behaviour of a resonant circuit”

本文的主要目的,並不想詳細講解,如何使用計算機設計程式來求解常微方程式?也不想詳細介紹所謂的 Runge-Kutta method ,數值分析專門書籍內可以找到這方面的知識,上述文獻就是非常完整的標準教材。重點集中於強調:如何將任何 FORTRAN 數值分析程式改寫成 ABC FORTH 程式?並展示,大型、複雜、實用的 FORTRAN 數值分析程式很容易改寫,所得的程式,比原 FORTRAN 程式使用起來更為方便。

過去幾十年,普天之下,已不知有多少的專家學者,發表過難以計數卻都非常有用的 FORTRAN 數值分析程式。電腦科技的演進,如果會廢棄這些先聖先賢費盡心血提供出來的研究成果,那就實在是人類的悲哀了,很不幸的是,目前電腦軟體技術的走向,傾向於如此。當年,許多聖賢,公開電腦技術,研究解決數學問題所做的貢獻,都寫成程式、發表成論文,有許多都算得上是精彩的博士論文。但是,現今的電腦環境,幾乎已經不容易再去執行那些有用的程式了。本文單舉一個例子,以 ABC FORTH 數學計算系統實現這種功能。幾十年來的專家學者,在數值分析實務上所做過的貢獻,完全可以輕易的使用 ABC FORTH 數學計算系統令其重現,並且化腐朽為神奇。

本文的主題,大約是理工科系大學三年級以上的學生,才可能遇到的特別數學問題。如果 ABC FORTH 數學計算系統,能夠解決這一等級的數值分析問題,那麼處理一般性的數學計算問題就當然是輕而易舉。已經擁有 ABC FORTH 數學計算系統的使用者,請耐心的讀完本文,以這個實例學會譯寫 FORTRAN 程式,這是本章的宗旨。

常微方程如何用程式來求得解答?

大學中的常微方程課程,教材內容偏重於理論數學,專挑一些理想化了的典型常微方程,例如:線性、常數係數、低階、規則型態的方程式來解釋分析,最重要的是,要能手解求得存在的唯一解答,如此才能讓學生建立常微方程式所代表的觀念,各個理工科系的學生,各有應該專注的不同型態之常微方程,各自深入了解後,就可以應用到後續所學的專業科目上去,能用手解得到的答案,有一個專門術語,稱作解析解(Analytical solution),也就是透過數學理論解釋分析後歸納出來的答案,健全完整,考試要考。

理論上的手解方法,通常都已固定落實了,甚至於都可以寫成公式直接套用。通常的解法,是以固定的演算程序,寫出常微方程之係數尚未確定前的通解(General solution),然後,將已知的起始條件(Initial condition)或邊界條件(Boundary condition)代入通解,最後,確定通解中各項式的未定常數係數,便能獲得代表符合所有條件的特解(Special solution)。

理工科系的學生,要用這些解答來探討他所研究學習的專業科目問題,例如:機械工程系的學生,解得流體力學或熱傳導學的常微方程答案後,就可以了解系統內物理現象變化的趨勢。電機工程系的學生,解得電磁學的常微方程答案後,就容易明白電磁現象的變化。土木工程系的學生,也按同樣的方式,就能夠了解建築結構上力的分佈。化學工程系的學生,用同法來了解化學反應變化。換句話說,工程科技,一定要學常微方程,甚至於偏微方程。

上述僅用一段話,說明求解常微方程的目的,而常微方程通常都是表達自然現象的一種數學式子,能夠被手解的類型所佔比例是非常少的,科學家要大刀闊斧的合理簡化式子後,才能讓原來表示自然現象的常微方程變成可解析,這就是科學上常說的大膽假設。

現實世界中,如果學過了某門工程科學,日後工作時遇到了實際的常微方程,不能每次都用大膽假設法解問題。從事於工程工作是負有責任的,不能全憑假設解問題;較無責任問題的研究工作,有時也不搞假設,因為如果假設不成就會長期沒有進展,研究停滯不前。不能手算獲得解析解的常微方程,不一定就無法可解,科技發展到今天,有了電腦可以協助人類進行快速數學計算,產生了所謂的數值分析技術,以分析問題後採用有依據的數值計算方法,用逼近答案的演算方式求得解答,其結果同樣可以讓人類了解自然現象變化的趨勢而解決科學問題,這樣的解題方法,在學校兩小時的考場內,考試沒辦法考。

求解常微方程的答案時,使用數值分析法與使用手算推演解析解法完全不同,就比較二者在得到答案程序的差異上而言,甚至於可以說是採用完全相反的步驟與方式來獲得解答的。數值分析法一開始,就得使用唯一已經確定的起始條件點或邊界條件點,做為起算基點。古時候,有很多的聖賢應用數學家,例如:本文採用的 Runge-Kutta method 即為其中之一,研究出各種逐步演算推進到下一點的合理方法,寫成數學通式,再翻譯成電腦可以執行的計算程式,藉由電腦的快速演算能力,逐點計算,逐點推進,便可算出此後所有合乎常微方程式所有條件的答案值來。

上述兩種解法所得的答案,最大的不同點,顯示在答案的表示方式上。解析解的答案通常都是可以寫成漂漂亮亮的數學函數的解答,所有工程科系的學生都習慣見到這樣的解答。數值分析解法所獲得的答案,見不到函數表示式,僅只有好幾筆一序列長串印出的數目字,乍看之下,實在不容易看出個所以然來。然而,在應用上,兩種答案意義是一樣的,而且理論上必須相等。這就好比您在查函數表的數值時,您也看不到函數表示式一般,但可以立刻用來解決工程問題。

數值分析技術一定要用到『函數曲線繪圖程式』,利用繪製函數曲線圖形的程式,就可以用圖形來顯示上一段敘述內所得到的數目字解答,將答案繪製成圖後,您就能夠完全了解,數值分析法解得答案的意義了。

以前大學教育裡不太教這種解題技術,當時,電腦的運用沒有那麼方便,情有可原,現在的大學應該要重視這方面的教材,因為,這是一種實際的解題方法,正式的工程問題就得使用。您用得好,工程或研究就做得好,您用不好,工程或研究就做不好。只會解析解的工程科系學生,考試可能可以考得高分,稱得上是秀才。但如果秀才抗拒電腦,拒絕學會數值分析法解工程或研究問題,秀才就會經常沒有用。兩種方法都搞得很好,才能算是人才,我們的社會需要人才而不是秀才。

本文採用的大型例題,要完全研究透徹,須要仔細讀完大約五十頁的英文原文著述,是數值分析教材中一章的份量,若要全譯成漂亮的中文,就得大費周章。我創作 ABC FORTH 系統的主旨不在這裡,我就是希望今後這種文章都不必寫了,只要忠實的告訴您,題材的原始出處,您自行參考,直接取來運用,為後代子孫著想,節約人類寶貴的資源,才是主要宗旨。

這個例題是電路學上,一個分析電阻、電容、電感(RCL)三個電子元件,串聯成一封閉迴路所形成的振盪電路,它可以寫成一個二次的常微方程,來表示電壓隨時間變化的關係,希望能解得具有起始條件後電壓隨時間暫態變化的趨勢解答。


起始條件係假設迴路原為開路狀態,瞬間在電容兩端充以 10 伏特電壓,並立刻將開路改成閉路,則電容兩端電壓隨時間變化的二次常微方程表示式,及這樣的初始條件可以表示式為:

LC*(d(dV/dt)/dt)+RC*(dV/dt)+V=0
V(0) = 10
dV(0)/dt = 0

三個電路元件的物理量假設為:
電容值固定為 C = 2 * 10 ^ -6 farads
電感值固定為 L = 0.5 henries (程式中被表示為 HL )
電阻值可選擇為 R = 0, 100, 1000, 1500 ohms
電容兩端瞬間起始電壓固定為 10 volts
分析時步進間隔值分採 H = 0.00001, 0.0001, 0.001, 0.002, 0.005, 0.01 sec
分析最長時間設定成小於 0.02 秒,亦即 TMAX < 0.02 sec

這個典型的例題是屬於一個可以手算求得解析解的問題,因此,解析解也被用在程式中直接計算,以便將所得結果,與使用 Runge-Kutta method 數值分析解法所得的答案,進行直接比對,驗證數值分析法的合理有效。

詳細的分析與討論,本文不再贅述,有興趣的讀者請自行參考文獻。

這個程式可以被應用到求解所有的常微方程上去,包括高於二次以上的高次常微方程,前處理分析出來的相關通式改寫成程式後,被安排在 MAIN 指令的第 120 及 130 兩列中,想應用於別的常微方程式問題時,就得在這裡替換通式表示式,詳細過程仍請參考文獻後仿照應用。

我可以把這個程式寫得更好、更有效率、更有通用性,但達不到教大家直接改寫 FORTRAN 程式進入 ABC FORTH 系統的效果。因此,我以最大的耐心,逐列翻譯。除了前文曾經提及的輸出、輸入全自創,而不改寫外,整個程式幾乎沒有需要大肆更動之處,簡單的程度,就好比將四川話翻譯成普通話那麼簡單。 FORTRAN 裡面有一個可以多重分支,GO TO (1,2,3,4,5), M 的指令,也很容易依理改寫。程式改寫完畢後的執行結果,當然要與原文列印的結果相同,最多僅能有小數點後面少數幾位數的系統計算性能誤差。這些基本要求, ABC FORTH 都一一實現了,而可以迅速的操作性能,則是 FORTRAN 程式語言所無法辦到的。

微分方程的數值分析解法,其實強調的重點是在試出最恰當的每次步進間隔值,如本文範例程式中的 H ,應該取多少才恰當?取的太大了,逐點推進演算的結果就會算不準。取的太小了,每次可以推進的距離就很小,要算很久、很多點後才有結果。這個現象就如同使用離散(Discrete)表示法,以可隨時間變化的加速度來計算下一個速度值,離散時刻的時間分割至很小時,解答會較準,但要推進很多點後才會有最後希望知道的速度答案。一次就想算得許久時刻後的速度,答案就會很離譜,表示時間間隔值取的太大了,根本不能這樣算。如果重點是這樣,那麼容易操作的數學計算系統就會很有用,可以快速修改、調整、獲得恰當的步進間隔值結果。 ABC FORTH 當然比 FORTRAN 好用多了,這就是我為什麼創作 ABC FORTH 數學計算系統的主因,它可以使運用電腦解決問題的習慣完全改觀。能夠執行數學計算的系統不算甚麼,擁有即時性操作效果的系統就非等閒之輩了。

我以這種方式貢獻給大家的 ABC FORTH 數學計算系統,能使此前出版過的所有數值分析教科書永遠有用,永遠不必被淘汰,因為應用數學的理論部份,是大約 200 年不會過時的,書中的實務範例程式,則可以經由 ABC FORTH 數學計算系統輕易再生,這樣做等於是『化腐朽為神奇』,講求社會道德,減少隨便廢棄有用物質而造福全人類。

我不想浪費時間在電腦編輯系統中,尋找漂亮的數學表示符號編寫這篇文章,所以一些數學表示式,就編寫成意義相同,外觀卻有一點不尋常的格式。

原 FORTRAN 程式去除刊頭文字說明及無用的 FORMAT 內容後,重新打字輸入於此處,數據及執行結果則不再編列於此。改寫完成的 ABC FORTH 對應程式,及其中一套數據輸入後的執行結果,則用電腦系統的可選擇性複製、轉換環境後再貼上的功能,轉置到此處,這是一篇實用教材。


 
   
c     APPLIED NUMERICAL METHODS, EXAMPLE 6.3
c     ELECTRICAL TRANSIENTS USING THE RUNGE-KUTTA METHOD
      IMPLICIT REAL*8( A-H, O-Z)
      INTEGER RUNGE
      DIMMENSION F(2), V(2), IMAGE(1500)
c     …..READ AND PRINT DATA
   1  READ (5,100) R, HL, C, VZERO, H, TMAX, IFREQ, IFPLOT
      WRITE (6,200) R, HL, C, VZERO, H, TMAX, IFREQ, IFPLOT
c     …..INITIALIZE T, V(1), V(2) AND ICOUNT
      T = 0.0
      V(1) = VZERO
      V(2) = 0.0
      ICOUNT = 0
c     …..COMPUTE ALPHSQ AND ALPHA, PRINT HEADINGS
      ALPHSQ = 1./(C*HL) – R* R /(4.0*HL*HL)
      ALPHA = DSQRT (DABX(ALPHSQ))
      RO2L = R/(2.*HL)
      IF ( IFPLOT .NE. 1 ) GO TO 3
        CALL PLOT1( 0, J, 11, 6, 19 )
        CALL PLOT2( IMAGE, TMAX, 0., DABS(VZERO), -DABS(VZERO) )
    3 WRITE (6,201)
c      …..IS CIRCUIT OVER-, CRITICALLY-, OR UNDER-DAMPED
c      …..COMPUTE ANALYTICAL SOLUTION, PRINT AND PLOT
    4  IF (ALPHSQ) 5, 6, 7
    5  TRUV =VZERO*DEXP(-RO2L*T)*((1.+RO2L/ALPHA)*
    1 DEXP(ALPHA*T)+(1.-RO2L/ALPFA)*DEXP(-ALPHA*T))/2.0
      GO TO 8
    6 TRUV =VZERO*DEXP(-RO2L*T)*(1.+RO2L*T)
      GO TO 8
    7 TRUV=VZERO*DEXP(-RO2L*T)*DCOS(ALPHA*T-DATAN(RO2L/ALP
    1 HA))/(ALPHA*DSQRT(C*HL))
    8 IF ( IFPLOT .NE. 1 ) GO TO 10
      CALL PLOT3( 1H*, T, V(1), 1, 8)
   10 WRITE (6,202) T, V(1), TRUV, V(2)
c     …..CALL ON THE FOURTH-ORDER RUNGE-KUTTA FUNCTION
   11 K = RUNGE(2,V, F, T, H )
c     …..WHENEVER K=1, COMPUTE DERIVATIVE VALUES
      IF ( K .NE. 1 ) GO TO 13
      F(1)=V(2)
      F(2)=-R*V(2)/HL –V(1)/(HL*C)
      GO TO 11
c    …..IF T EXCEEDS TMAX, TERMINATE INTEGRATION
   13 IF ( T .LE. TMAX ) GO TO 16
      IF ( IFPLOT .NE. 1 ) GO TO 1
      WRITE ( 6,203)
      CALL PLOT4( 7, 7HVOLTAGE )
      WRITE ( 6,204)
      GO TO 1
   16 ICOUNT=ICOUNT+1
c     …..PRINT RESULTS OR CALL DIRECTLY ON RUNGE
      IF ( ICOUNT .NE. IFREQ ) GO TO 11
      ICOUNT = 0
      GO TO 4
c     …..FORMATS FOR INPUT AND OUTPUT STATEMENTS
100   FORMAT ( …………..)
200   FORMAT (……………)
201   FORMAT (…全略……)
202   FORMAT (……………)
203   FORMAT (……………)
204   FORMAT (……………)
      END

FUNCTION RUNGE( N, Y, F, X, H )
IMPLICIT REAL*8(A-H, O-Z)
REAL*8 Y, F, X, H
INTEGER RUNGE
DIMENSION PHI(50), SAVEY(50), Y(N), F(N)
DATA   M/0/
M=M+1
GO TO (1,2,3,4,5), M
c     …..PASS 1
   1  RUNGE=1
      RETURN
c     …..PASS 2
   2  DO 22 J = 1 , N
      SAVEY(J)=Y(J)
      PHI(J)=F(J)
    22 Y(J)=SAVEY(J)+0.5*H*F(J)
X=X+0.5*H
RUNGE=1
RETURN
c     …..PASS 3
   3  DO 33 J = 1, N
      PHI(J)=PHI(J)+2.0*F(J)
    33 Y(J)=SAVEY(J)+0.5*H*F(J)
RUNGE=1
RETURN
c     …..PASS 4
   4  DO 44 J=1, N
      PHI(J)=PHI(J)+2.0*F(J)
    44 Y(J)=SAVEY(J)+H*F(J)
X=X+0.5*H
RUNGE=1
RUTURN
c     …..PASS 5
   5  DO 55 J = 1, N
  55  Y(J)=SAVEY(J)+(PHI(J)+F(J))*H/6.0
      M=0
      RUNGE=0
      RETURN
      END

改寫完成的 ABC FORTH 完整程式及十群範例輸入數據如下:

\ Runge-Kutta Method for ODE

INTEGER M     INTEGER J     INTEGER RUNGE
INTEGER N
10 ARRAY Y    10 ARRAY F
REAL X        REAL H
50 ARRAY PHI  50 ARRAY SAVEY

: F(RK4)     BASIC
10 REM ***F(RUNGE) = FUNCTION RUNGE( N, Y, F, X, H )
20 LET M = M + 1
30 IF M = 1 THEN 60
40 GOTO 80
50 REM ***PASS 1
60 LET RUNGE = 1
70 GOTO 9999
80 IF M = 2 THEN 110
90 GOTO 180
100 REM ***PASS 2
110 FOR J = 1 TO N
120 LET { SAVEY ( J ) = Y ( J ) }
125 LET { PHI ( J ) = F ( J ) }
130 LET { Y ( J ) = SAVEY ( J ) + 0.5 * H * F ( J ) }
140 NEXT J
150 LET { X = X + 0.5 * H }
160 LET RUNGE = 1
170 GOTO 9999
180 IF M = 3 THEN 210
190 GOTO 270
200 REM ***PASS 3
210 FOR J = 1 TO N
220 LET { PHI ( J ) = PHI ( J ) + 2.0 * F ( J ) }
230 LET { Y ( J ) = SAVEY ( J ) + 0.5 * H * F ( J ) }
240 NEXT J
250 LET RUNGE = 1
260 GOTO 9999
270 IF M = 4 THEN 300
280 GOTO 370
290 REM ***PASS 4
300 FOR J = 1 TO N
310 LET { PHI ( J ) = PHI ( J ) + 2.0 * F ( J ) }
320 LET { Y ( J ) = SAVEY ( J ) + H * F ( J ) }
330 NEXT J
340 LET { X = X + 0.5 * H }
350 LET RUNGE = 1
360 GOTO 9999
370 IF M = 5 THEN 400
380 GOTO 430
390 REM ***PASS 5
400 FOR J = 1 TO N
410 LET { Y ( J ) = SAVEY ( J ) + ( PHI ( J ) + F ( J ) ) * H / 6.0 }
420 NEXT J
430 LET M = 0
440 LET RUNGE = 0
9999 END ;

INTEGER K       INTEGER ICOUNT常微方程式之 Runge-Kutta 解法
INTEGER IFREQ   INTEGER IFPLOT
REAL RT1   REAL RT2      REAL RT3
REAL R     REAL HL       REAL C         REAL VZERO
REAL RO2L  REAL ALPHA   REAL ALPHSQ
REAL TRUY  REAL TMAX

: GROUP1
  {{ R = 100.00  }} {{ HL    =  0.50 }}
  {{ C = 2.0E-6  }} {{ VZERO = 10.00 }}
  {{ H = 0.00001 }} {{ TMAX  = 0.025 }}
  [[ IFREQ =  25 ]] [[ IFPLOT =   0  ]]
;
: GROUP2
  {{ R =    0.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00010 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =  10 ]] [[ IFPLOT =    0 ]]
;
: GROUP3
  {{ R = 1000.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00010 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =  10 ]] [[ IFPLOT =    0 ]]
;
: GROUP4
  {{ R = 1500.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00010 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =  10 ]] [[ IFPLOT =    0 ]]
;
: GROUP5
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00001 }} {{ TMAX  = 0.020 }}
  [[ IFREQ = 100 ]] [[ IFPLOT =    0 ]]
;
: GROUP6
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00010 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =  10 ]] [[ IFPLOT =    0 ]]
;
: GROUP7
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00100 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =   1 ]] [[ IFPLOT =    0 ]]
;
: GROUP8
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00200 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =   1 ]] [[ IFPLOT  =   0 ]]
;
: GROUP9
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.00500 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =   1 ]] [[ IFPLOT =    0 ]]
;
: GROUP10
  {{ R =  100.00 }} {{ HL    =  0.50 }}
  {{ C =  2.0E-6 }} {{ VZERO =  10.0 }}
  {{ H = 0.01000 }} {{ TMAX  = 0.020 }}
  [[ IFREQ =   1 ]] [[ IFPLOT =    0 ]]
;

: PRINT-INPUT-DATA   BASIC
10 REM ***PRINT DATA
20 PRINT "      R = " ; { R }
30 PRINT "     HL = " ; { HL }
40 PRINT "      C = " ; { C }
50 PRINT " VZERO = " ; { VZERO }
60 PRINT "      H = " ; { H }
70 PRINT "  TMAX = " ; { TMAX }
80 PRINT "  IFREQ = " , IFREQ
90 PRINT " IFPLOT = " , IFPLOT
100 END ;

: INIT    BASIC
10 REM ***INITIALIZE X, Y(1), Y(2), AND ICOUNT
20 LET { X = 0 }
30 LET { Y ( 1 ) = VZERO }
40 LET { Y ( 2 ) = 0.0 }
50 LET ICOUNT = 0
60 LET N = 2 
70 END ;

: WRITE(6,201)
\ PRINT HEADINGS
  CR 10 SPACES ." X "      10 SPACES ." CALC. Y "
     10 SPACES ." TRUE Y " 10 SPACES ." CALC. Y' " CR
;

: COMPUTE-CONSTANT   BASIC
10 REM ***COMPUTE ALPHSQ AND ALPHA, PRINT HEADINGS
20 LET { ALPHSQ = 1. / ( C * HL ) - R * R / ( 4.0 * HL * HL ) }
30 LET { ALPHA = SQRT ( ABS ( ALPHSQ ) ) }
40 LET { RO2L = R / ( 2. * HL ) }
50 IF IFPLOT <> 1 THEN 80
60 REM ***CALL PLOT1
70 REM ***CALL PLOT2
80 RUN WRITE(6,201)
90 END ;

: WRITE(6,202)   BASIC
10 REM ***PRINT SOLUTION
20 PRINT { X , Y ( 1 ) , TRUY , Y ( 2 ) }
30 END ;

: DAMPED   BASIC
10 REM ***IS CIRCUIT OVER-, CRITICALLY-, OR UNDER-DAMPED
20 REM ***COMPUTE ANALYTICAL SOLUTION, PRINT OR PLOT
30 IF { ALPHSQ < 0 } THEN 50
40 GOTO 100
50 LET { RT1 = ( 1. + RO2L / ALPHA ) * EXP ( ALPHA * X ) }
60 LET { RT2 = ( 1. - RO2L / ALPHA ) * EXP ( NEGATE ( ALPHA ) * X ) }
70 LET { RT3 = ( RT1 + RT2 ) / 2.0 }
80 LET { TRUY = VZERO * EXP ( NEGATE ( RO2L ) * X ) * RT3 }
90 GOTO 170
100 IF { ALPHSQ = 0 } THEN 120
110 GOTO 140
120 LET { TRUY = VZERO * EXP ( NEGATE ( RO2L ) * X ) * ( 1. + RO2L * X ) }
130 GOTO 170
140 LET { RT1 = COS ( ALPHA * X - ATAN ( RO2L / ALPHA ) ) }
150 LET { RT2 = ALPHA * SQRT ( C * HL ) }
160 LET { TRUY = VZERO * EXP ( NEGATE ( RO2L ) * X ) * RT1 / RT2 }
170 IF IFPLOT <> 1 THEN 190
180 REM ***CALL PLOT3
190 RUN WRITE(6,202)
200 END ;

: CALL-FUNCTION-F(RK4)
  F(RK4)
;

: MAIN   BASIC	\ 執行Runge-Kutta的主要指令
10 RUN GROUP2  \ (1)輸入數據建立了十組,於此處自行選擇更換
20 RUN PRINT-INPUT-DATA
30 RUN INIT
40 RUN COMPUTE-CONSTANT
50 RUN DAMPED
60 REM ***CALL ON THE FOURTH-ORDER RUNGE-KUTTA FUNCTION
70 LET M = 0
80 RUN CALL-FUNCTION-F(RK4)
90 LET K = RUNGE
100 REM ***WHENEVER K=1, COMPUTE DERIVATIVE VALUES
110 IF RUNGE <> 1 THEN 150
120 LET { F ( 1 ) = Y ( 2 ) }
130 LET { F ( 2 ) = NEGATE ( R ) * Y ( 2 ) / HL - Y ( 1 ) / ( HL * C ) }
140 GOTO -80
150 REM *** IF X EXCEEDS TMAX, TEERMINATE INTEGRATION
160 IF { X <= TMAX } THEN 210
170 GOTO 260
\ 180 IF IFPLOT <> 1 THEN -10
\ 190 REM ***WRITE(6,203),CALL PLOT4,WRITE(6,204)
\ 200 GOTO -10
210 LET ICOUNT = ICOUNT + 1
220 REM ***PRINT RESULTS OR CALL DIRECTLY ON RUNGE
230 IF ICOUNT <> IFREQ THEN -70
240 LET ICOUNT = 0
250 GOTO -50
260 PRINT " ***Run next GROUPn by change ( 10 RUN GROUPn )*** "
270 END ;

直接操作ABC FORTH系統,執行MAIN指令,所得結果顯示如下:

MAIN
     R =     0.000000000E-1
    HL =      .5000000000
     C =     2.000000000E-6
 VZERO =    10.00000000
     H =      .0001000000
  TMAX =      .0200000000
 IFREQ =    10
IFPLOT =     0
          X              CALC. Y         TRUE Y              CALC. Y'

   0.000000000E-1      10.00000000     10.00000000          0.000000000E-1
    .0010000000         5.403029671     5.403023059     -8414.704778
    .0020000000        -4.161452687    -4.161468365     -9092.979918
    .0030000000        -9.899919391    -9.899924966     -1411.224448
    .0040000000        -6.536459532    -6.536436209      7568.001143
    .0050000000         2.836581058     2.836621855      9589.251198
    .0060000000         9.601684950     9.601702867      2794.201656
    .0070000000         7.539057071     7.539022543     -6569.818977
    .0080000000        -1.454933809    -1.455000338     -9893.586642
    .0090000000        -9.111266133    -9.111302619     -4121.250371
    .0100000000        -8.390754644    -8.390715291      5440.137662
    .0110000000          .0441656076     .0442569799     9999.894840
    .0120000000         8.438479098     8.438539587      5365.808798
    .0130000000         9.074504988     9.074467815     -4201.568624
    .0140000000         1.367486013     1.367372182     -9906.048042
    .0150000000        -7.596790229    -7.596879129     -6502.966258
    .0160000000        -9.576622425    -9.576594803      2878.902739
    .0170000000        -2.751765848    -2.751633381      9613.924740
    .0180000000         6.603046592     6.603167082      7509.961785
    .0190000000         9.887056797     9.887046182     -1498.614135
***Run next GROUPn by change ( 10 RUN GROUPn )***  ok

執行後所得結果,與原始資料內容比較,毫不遜色。


2026年3月16日 星期一

如何設計高低階混用碼指令?

如何設計高低階混用碼指令?


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


在某些特殊情況,當我們使用 FORTH 系統提供的所有現成指令設計程式時,會希望高低階指令可以任意混合使用,這種特殊編寫程式的功能稱之為混碼程式(Mixing code)技術。程式語言可以有許許多多的古怪技術,但對 FORTH 而言,只要是你描述得出來的技術, FORTH 沒有設計不出來的,而且強調僅由個人私下就可以解決,這是 FORTH 具有延伸性性能使然。不像其他程式語言,特殊功能通常要等原販售公司將其添加於系統,發行新版後才有可能,因為大多數程式語言均屬封閉性,不容使用者隨意修改系統既定功能。本文介紹如何為 FORTH 系統添加混碼程式功能?它就是一個 FORTH 程式語言與其他程式語言比較時,在眾多性能差異上,特別成為鮮明對比的例子。

1 預備知識

初學 FORTH 之人,可能較難看懂這種指令的設計原理,因為初學者通常不會一開始就直接接觸理論性的『FORTH系統結構』與『FORTH指令基本結構』,更進一步的『FORTH執行原理』,則要等學過 FORTH 後,還有興趣深入探討系統時才會去研究它,而欲設計這種指令時,必須先具備這些基本知識,否則不易透徹了解它們的設計方法。另外,因為要混用高低階指令編寫程式,系統當然要具備 FORTH Assembler 的功能,在使用 FORTH 組合語言時,所有的規矩也都得遵循,要有『CPU內有那些暫存器不可亂用?』的限制觀念。上述的預備知識就成了能看懂本文的基本要素了。屬於系統部份的預備知識,你可以從 1987 年 4 月我們出版過的『FORTH期刊』中獲得。組合語言的部份要自行下功夫實作後才能真正體會了解,要先行標出 CPU 內那些暫存器被 FORTH 系統規劃掉了?必須避開不用,或用前搬開內容,用完再合理恢復,所寫的程式執行時才不會出錯。

這些指令不屬於標準指令,在歷來的標準指令表中找不到他們的蹤跡,甚至於最新的 ANSI 標準中也絕口不提,可是它在 FORTH 發展的早期就已經被經常使用了,很久以前,我們在使用這種性能已經多年之後,才在後來新發行的 C 類程式語言系統中見到它的仿照設計。 FORTH 有許多性能領先別人許久,一直到今天依然如此,我們還可以舉出多項性能是一般程式語言所沒有的,這也是為何學會 FORTH 後便愛用 FORTH 的原因。學會設計混碼程式需用指令的設計方法,將更加強對於 FORTH 系統的深入認識,也將終身受用。看懂本文,將來就能輕易的為任何新出現的 FORTH 系統添加此一性能,這是作者為傳揚 FORTH 的最主要目的。因此,特別挑選了一個原本欠缺此一性能的大眾化系統來驗證它,就以 eForth32 做為設計基礎,為其添加此一性能。如果你手頭上使用中的系統已經有此性能,或者只有一半性能,例如:只能在高階定義指令中混入低階指令編寫程式,而於編寫低階定義指令時,還不能混入高階指令。那麼,你就可以根據本文自行添加此性能,自己磨練系統指令的程式設計能力,並與原已有功能比較,將是毫不遜色,因為不同的設計都將殊途同歸,對於程式執行速度或執行效果毫無影響。

如果你尚未準備好上述背景知識,建議你暫時不用細看本文,留供日後參考便可。通常一般商售的 FORTH 系統都提供了此一性能,它們較常用的指令名稱為 : [C 、 C] 、 C: 、 ;C 之類的名稱。前二者成一對,使用在高階定義中轉入低階再轉回高階。後二者成另一對,使用在低階定義中轉入高階再轉回低階。不用讀通本文,你也可以直接使用這些指令,使用手冊的說明文獻也只能做到這個地步。

留供日後參考的建議係作者個人的贈言,四十幾年前,我們在中山科學研究院內的週五研討會上,討論過此項性能。我記得當時藍瑞賢個人獨衷 Zen-Forth 商售系統,其中只提供了這項功能的半套指令,而當時大家公認使用的推廣標準是無版權問題的 F83-FORTH 系統,已有完整的這項功能。為此問題我們另有 Poly-FORTH 系統,也有完整的這項功能,可以比照參考。陳爽的 FORTH 功力當時最好,他帶頭講解的結果,讓與會者此後便有能力自建此功能於任何系統,我則仔細的做了筆記。三十年前我們移民到了紐西蘭,我隨身攜帶來此的書籍、簿本,只有三樣,一本中英對照的『聖經』,一本精裝大字足本的『四書讀本』,另外就是所有的 FORTH 歷年筆記,這些都是精神糧食,好東西就應該留供日後參考,單純只留在腦袋中,老了是會忘記的。我在 eForth32 中重建此功能時,只花了半天時間,而且一次完成,為什麼?靠的就是筆記。

2 使用時機

我們可以舉出許多例子來強調這項功能非常方便,尤其是對一個想玩進 FORTH 系統內部的人更具有吸引力,因為它涉及了低階程式的可以無所不做能力,以及執行速度可以發揮到系統極限的先天優點,另外配上高階程式的現成與複雜功能,二者組合起來,在許多場合就相當方便。因此,若輕易就能擁有的功能,為何不用?它不是標準指令又有甚麼關係? 舉例來說:如果我有一個程式想測系統性能,必須斤斤計較執行速度到系統的基本時脈週期程度。我曾寫過這種程式,就以讀取電腦中的標準時基計數器內容當標準,要讀得又快又穩又準,當然得用低階組合語言來寫程式,想像中幾乎是一瞬間就得到了參考時基,而且只用幾個低階指令,自己耗了幾個時脈數很容易清算,易於隨後扣除。接下來要處理計算與顯示時,若還只能用低階程式,那就非常痛苦了,這項高低階可以混用的功能就很有用處,讓問題極容易解決。

另外,我們在設計中斷處理程式最後,免不了要用低階程式來安放執行位址(Entry point)於中斷執行向量位置,中斷瞬間想以低階最快速度做一件事,隨後想做的事通常就很複雜,不用高階就不方便,做到關鍵處還有可能又要求用低階設計比較有效,如此又高又低的翻來覆去,這項功能更能提供方便。

還有一些平日設計的高階程式,在某些過程中想穿透系統去做一些沒有指令可用的工作,例如:像除錯工具指令一樣可以取得當時 FORTH 系統高階定義指令的程式計數器(Program counter) 內容,高階指令設計中便可混入幾個低階指令來完成。等同於 FORTH 系統高階指令程式計數器的專有名詞,在 FORTH 領域,則稱之為指令指標暫存器(Interpreter Pointer or Instruction Pointer,IP ),系統一般均不提供操作它的指令,古時候的系統則有此 IP 指令可用,它會直接取得系統執行當時的 IP 內容,有幾個結構化跳躍指令,可以藉助於此指令之存在而容易完成設計,後來有新觀念被發展出來才不用它了。

另外,一個接近終了的程式設計,也許會有強調必須改善執行速度的要求,大家都知道,此時非用低階指令取代設計不可了,如果可修正的指令不想搬離原設計位置,那麼,這項功能理所當然的可以提供服務。

還有無數的例子可以舉出,那怕僅只是玩一玩,當基本教材訓練學生,都是有用的,越用 FORTH 設計程式,就越能發現那些場合?可以讓這項功能盡量發揮。我常用,可是很不喜歡用這種程式來當作寫 FORTH 文章的題材,因為要用組合語言,文章只能在限用時間內有效,我擅長用卻不擅長描述這種程式,痛苦之至,最好不寫。很不幸本文的程式設計就不可或免的得用到一些組合語言,我只好將就一點,勉為其難的在該部份進行簡單說明。

3 基本原理

在解說問題時,必須將問題簡化才方便解釋,我們就以這個方式開始。 FORTH 系統中,強調所有的東西都盡可能為統一的格式,因此,無論是高、低階指令,或變數、常數、或字彙,它們的基本結構都是一致的,至少有名稱欄、連結欄,解碼欄、參數欄,某些系統還多了觀察欄或其他欄,我們暫時別管它們的存在與否,只考慮基本上標準的前四欄。

無論你如何寫程式,能被 FORTH 系統接受的最後結果,其格式仍必須符合標準結構的要求,否則不能被正常的執行。因此,我們現在探討的主題,也不能脫離這個範疇。也就是說,高階定義指令中混用低階的指令,或低階指令中混用了高階指令,這個基本結構規矩仍得遵循,不可以在基本結構中另出一基本結構,我們想解說的問題因此被簡化了一半。

四欄的結構有時又被分成兩個部份,一是頭部,也就是名稱欄與連結欄,另一個就是體部,也就是解碼欄與參數欄。分成兩部份,有時有利,有時有害,利害分析這部份,超出本文探討範圍,擱置不予置評,有許多系統都這樣做, eForth32 、Win32Forth 都是, F83 則不是。高階定義指令中使用低階,其中的低階既然不能也是一個原來的標準結構樣子,那麼,必須是怎樣的結構才能被系統接受呢?根據『FORTH執行原理』來考慮,如果我們能夠去除這個下屬於高階指令的低階指令其頭部,系統的執行機制就可以符合正常的 FORTH 系統執行原理。換句話說,如果我們將這樣的程式設計,搞成包括進來的低階指令沒了頭部,只有體部,那麼整個系統的執行機理就能維持而未被破壞。反之,低階指令中使用了高階,其理亦然,只能留住下屬者的體部,而不能有其頭部。根據這樣的原則,我們就能按照FORTH的執行原理,在邊界位置設計程式,來切換系統執行高階或低階指令時的相關程式計數器內容,以達到目的。

所謂的程式計數器,我們暫時可以將情況比擬想像成, Forth 系統中高階定義指令的程式計數器是 IP 。而低階定義指令的程式計數器是 W ,也就是 Forth 系統中所謂的工作暫存器(Current Word Pointer or Working Register)。一般 CPU 所擁有的程式計數器則仍然稱之為 PC ,如果還想再延伸推理到 FORTH 系統中是否還有其他類似意義的程式計數器?那麼我們也可以說負責多工任務(Multi-task)的另一個指標,使用者系統變數指標暫存器 UP(User Pointer),可以算是多工(Tasks)運轉時的程式計數器,接受這一段描述,很可以加強你了解所謂『FORTH虛電腦』這個專有名詞的意義。

如果你很能融會貫通新學的東西,也很有創意,用心想一想,有一天也許能在 FORTH 領域創作出另一個程式計數器,例如:一種中階定義指令的指標暫存器,名叫 MP(Middle Word Pointer ),或者是比 UP 更高的超級程式計數器,用它供作多人使用時、許多系統相連時、甚或全世界網域互通時的指標暫存器之用,它名叫 Super-P ,因為管得太大了,所以名字比較長。讀者千萬不要認為這些推理不切實際,它蘊涵了 FORTH 一貫的哲理。

將指令結構分成兩部份的系統,常強調最終的應用程式被執行時只有體部才被用到,頭部可以說是多餘的,因此分成兩部份放置後,最後可以一次就丟光系統所有的頭部,只留體部,應用程式仍然照舊執行,省了記憶體,還讓有心人無法以逆行追蹤的方式還原源程式的結構。混用碼的程式技術也就是根據這樣的原則,來將其所下屬的高低不平等指令埋進系統的,我們須要的指令就根據此理而設計,這一部份簡化了剩下來另外一半的問題。

4 程式設計

下列是一套設計完成後經過測試的指令組:


 

ONLY FORTH ALSO ASSEMBLER ALSO DEFINITIONS
: [C HERE 2+ , ASSEMBLER [COMPILE] [ ; IMMEDIATE
: C] [ ASSEMBLER ] HERE SPLIT DROP 6 + # SI MOV NEXT FORTH ] ;
CODE DOCOLON
      BP SP XCHG
      DS PUSH
      SI PUSH
      BP SP XCHG
      SI POP
      NEXT 
      END-CODE
CODE (;C)
      BP SP XCHG
      SI POP
      DS POP
      BP SP XCHG
      NEXT 
      END-CODE
: C: [’] DOCOLON CALL, FORTH ] ;
: ;C [ ASSEMBLER ] COMPILE (;C) ASSEMBLER [COMPILE] [ ; IMMEDIATE
FORTH DEFINITIONS



事實上,這個程式可以精簡到只有高階定義的四列程式,上列兩個低階定義的指令 : DOCOLON 與 (;C) 根本就是從原 eForth32 系統中抄來的。我不喜講解組合語言的程式,此處係為了方便而借來使用而已。這個 DOCOLON 在 eForth32 系統源程式中被命名為 doList ,而 (;C) 為 semi ,但系統經蛻變程式(Meta-compiler)長成後,將名稱埋掉了,使用者見不到指令名,因此也就無從直接叫用,指令根本不長,我們就重新以低階指令方式定義一遍又有何妨?也因此讓上列程式多了幾列,而實質重點確實只須四列。

隨便寫兩個測試程式試驗指令功能如下:


 

:  DOUBLE ( n - - )         \  高階混入低階舉例
   DUP                      \ 以高階指令設計,模擬慢速輸入數據
   [C   BX  AX  MOV         \ 以低階指令設計,模擬快速處理數據
   AX  AX  ADD    
   AX  BX  MOV   C]
   CR  .                    \ 恢復方便但慢速的顯示處理
   .” is the double of ”  .  ;

CODE LMH ( - - )            \ 低階混入高階舉例            
     DX  PUSH               \ 以低階指令設計,模擬高速輸入數據
     BX  PUSH
     5  #  BX  MOV
     0  #  DX  MOV
     C:                     \ 轉入高階指令環境
     CR                     \ 以高階指令設計,模擬方便但慢速的數據處理
     .”  The double of 5 is ”  
     DUP  +  . 
     ;C                     \ 恢復低階指令環境
     NEXT	
     END-CODE



它們均執行無誤,可是我得提醒你,在你並不清楚 eForth32 系統中組合語言的使用限制前,千萬不要隨便將一般組合語言的用法,直接設計程式來測試這一組指令,因為系統將 CPU 內的一些暫存器分配給 eForth32 虛電腦使用了,不得破壞,換句話說,你得遵循 FORTH Assembler 的規矩設計低階部份的程式才行。

5 程式說明

前一節中的四列高階設計指令,就是本文的主要重點,不詳加說明誠難理解。現在我們才開始正式的逐個說明程式設計的內容。首先是由高階中進入低階的第一個指令 [C ,為說明方便,它的定義重抄重排如下:


 

: [C   HERE   2+   ,
  ASSEMBLER  
  [COMPILE]   [   ;   IMMEDIATE 
  


我們希望這個『 [C 』指令執行之後,可以開始自由的書寫 FORTH Assembler 程式。說明前可以先行想像,如果不作此額外要求,而是正常高階定義的指令,系統編譯到此處時,應該要編進系統另一個高階指令,或一個低階指令解碼欄所在之所謂的執行位址,現在有此額外要求了,因此必須編進系統一個低階指令格式所須的解碼欄執行位址,就如同前述第 3 節基本原理中已經提到,此後插進來的低階指令,必須是一種沒有頭部,只有體部的格式,因此,我們必須為系統在此處先造好一個低階指令格式的解碼欄,然後接著的內容形同一個低階指令的參數欄,才能自由的書寫 FORTH Assembler 。因此,第一列程式就是這個參數欄。如果你對傳統組合語言很熟,一定看得懂組合語言程式中出現了『 * + 2 』時所代表的意義,它也是以前傳統 FORTH 系統(例如: F83 )源程式中,低階指令解碼欄內所設計的內容,它用來告訴實際上的電腦 CPU ,去執行後續 2 個 bytes 偏移量的組合語言指令。在 FORTH 領域的意義,則是指揮低階程式計數器 W 指到此處,以便將低階指令轉交給真實電腦的程式計數器 PC 執行程式, W 在 PC 處理完後續所有低階指令前則不再變化。因此,『 HERE 2+ , 』 就是安放了一個低階格式所須要的解碼欄,至此明焉!

看似簡單的程式仍有另一個變化,由於 eFroth32 是一個直接線串碼(DTC)式的系統,它將低階指令程式計數器 W 的操作機理,設計成指令的解碼欄中,直接就得開始安放組合語言式的程式,因此,此處可以用『 HERE 2+ , 』達到目的。相對於此的另一種系統,稱之為間接線串碼式的系統,如 F83 ,其低階指令解碼欄中放的就是 * + 2 ,我們反而要設計使用兩次同套指令,也就是『 HERE 2+ , HERE 2+ , 』才能達到目的,係因為其系統中 W 的操作機理有所不同的關係,才必須這樣設計。

接下來的部份就很容易看懂了,可以開始使用組合語言寫程式前,當然要宣告系統找字最優先秩序轉到 ASSEMBLER 字彙之下。然後,要特別注意了,在 FORTH 系統觀念中, FORTH 的組合語言指令被編譯時,是以被執行的方式編寫機器碼入記憶體,而非以編譯狀態來編譯。因此,這個程式中要設計許多次狀態的切換,第三列程式中的『 [COMPILE] [ 』就是第一個。『 [C 』當然不能在高階程式格式,冒號定義的外面使用,所以宣告 IMMEDIATE 。

第二個由低階狀況返回高階狀況的指令 C] 重抄重新排列如下:


 
:  C]  [  ASSEMBLER  ]
   HERE  SPLIT  DROP  6  +  #  SI  MOV
   NEXT  FORTH  ]  ;


第一列程式要求系統進入執譯狀態,宣告找字最優先秩序轉到 ASSEMBLER 字彙之下,然後立刻恢復成編譯狀態,這一列是僅為這個 C] 指令自身設計須要而設計的,因為第二列程式就要開始使用組合語言寫程式了。請注意,暫時摒除程式說明不談,轉看另一個狀況,這裡已經出現了高階內混用低階的情況,有一個 MOV 組合語言用指令,但是它卻出現在系統的編譯狀態下,系統此時不是將組合語言指令的機器碼編譯入記憶體,而是將它編譯成 [C 指令的執行內容,此後使用 [C 指令時,系統才會去執行 [C 指令的內容,包括這個 MOV 。正式的高階內用低階的情況不能像此處的用法,否則,每次都得在這個切換位置擺好這三列程式,整個系統也才能維持正常運作,把三列程式縮減成一個 [C 指令是設計它的目的。

回到此指令的程式說明,我們必須先行解釋第三列程式。這個指令的主要工作,是在用完低階設計程式後,要設法回到高階狀態。前一個 [C 指令造好了體部上方的解碼欄,這一個 C] 指令也要造好一個標準低階指令格式參數欄下方最後的尾綴結構,它們是 NEXT 與 END-CODE ,然後才能回到 FORTH 字彙及高階定義指令所須的編譯狀態,第三列程式即完成這項工作。

回頭看第二列程式所為何在?由低階回到高階狀態時,我們必須先調整好高階指令程式計數器 IP 的內容才能回去, CPU 中的 SI 被指定當 IP 使用,必須讓它指到用完低階,接著出現在 C] 後面那一個指令所在的記憶體位址,如此回到高階狀態時,系統才不會出差錯。這個位址隨著系統的不同,要經過核算才能決定, eForth32 是與當時系統的 HERE 相差了 6 個位元組的記憶體偏移量,它指的是各個使用到 C] 的處所,編譯 HERE 前到編譯完 NEXT 後之位址偏移量為 6 ,如果不能確定此量,可以使用 DUMP 指令看編譯後結果來加以確定。

eForth32 中的 NEXT 內容以普通組合語言表示如下:

LODS WORD \ 1 byte
JMP AX \ 2 bytes

而第二列程式中的 HERE SPLIT DROP 6 + # SI MOV 一長串 FORTH 程式,最後會被編譯成單一個低階指令,可用普通組合語言表示如下:

MOV SI, # value \ 3 bytes

這個總共為 6 的偏移量就是這樣核算出來的。 F83 系 統只要設計成 HERE 6 + 便可得到 value 之值。 eForth32 系統在執行程式時,使用 32 位元的執行位址,因此使用 HERE 後會得到 32 位元值,但是此系統在編譯程式的執行碼時,仍然只用 16 位元的執行位址,如此才能放進 16 位元的 CPU 之 SI 內,因此,我們使用了 HERE SPLIT DROP 6 + 來求得上列 value 之值,完成這個指令的設計。

如果你讀完了上述兩個指令的程式說明完全沒有問題,那麼該恭喜你,接下來的兩個指令的程式說明可以直接了解了。我們一次重抄重排兩個指令的程式設計內容如下:



:  C:   [’]  DOCOLON  CALL,
        FORTH  ]  ;
:  ;C   [  ASSEMBLER  ]
        COMPILE  (;C)
        ASSEMBLER  [COMPILER]  [  ;  IMMEDIATE


設計低階指令程式要轉入可用高階指令狀況前,執行 C: ,此指令首先就安排好去執行一個高階指令該有的體部解碼欄,由第一列程式『 [’] DOCOLON CALL, 』完成,高階定義指令的程式環境,需宣告成第一優先找字的字彙為 FORTH ,且系統須為編譯狀態,由第二列程式『 FORTH ] 』完成,此後在這裡使用任何高階定義方式設計程式就沒問題了。

用完了高階定義格式,一定得再回到低階定義格式的環境去,否則系統就亂了,因此執行過 C: ,就一定得再執行 ;C ,讓系統恢復。這個指令只做了類似前述 C] 指令的工作,將一個編完高階指令後該有的尾綴結構要求 (;C) ,也就是 eForth32 中的半冒號指令『 ; 』之核心結構 semi 編進系統。同時,再次強調,別忘了! FORTH 中的 Assembler 被編譯時是以被執行的方式編寫機器碼入系統,而非以編譯狀態來編譯,這就是最後一列程式的說明。

2026年3月1日 星期日

突破結構化設計測試記錄

突破結構化設計測試記錄


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



本文係 eForth32 系統中的測試記錄,原稿創作日期為 20111010 ,當時係為了紀念中華民國一百年十月十日而寫。

這是一個反其道而行的論題,程式設計觀念的演變是由原本可以非結構化的規格,演進到現代的強調要結構化,為了防止程式被執行期間因特例而出問題,且易於與人溝通,我也同意設計程式時應該結構化。然而,作為一個 FORTH 自由程式語言的使用者,根本不喜歡別人來干涉我的程式結不結構化?非結構化術語的存在意味著它應有存在的權力,這個自由也不應該被澈底剝奪。我個人的觀點則是:設計程式時應該盡量達到結構化的要求,但在萬一想直接引用前人設計出來的好程式,卻遇到了非結構化的情況,自己又不想大張旗鼓的重改其結構,則應該仍讓我擁有非結構化的權力。

所謂『結構化』,指的是程式執行流程若以繪線來表示,一些迴路或條件分支會形成封閉的迴圈,這些迴圈之線互不相交者就稱之為『結構化』,會相交者就是『非結構化』。舉例而言,早期的 FORTRAN、BASIC ..... 等程式語言,古今所有的 ASSEMBLY 程式語言,都是非結構化程式語言,近代的 BASIC、C++、FORTH ..... 則是結構化程式語言。我們可以這樣認為,所有的程式語言,原本都可由 ASSEMBLY 發展出來,包括其自身亦然。既然 ASSEMBLY 原本即有非結構化之本質,換句話說,程式語言就是有非結構化的天性,是人為的安排使之只能以結構化規格使用的,通常均因系統設計者在編譯程式(Compiler)中加了限制所致,否則所寫的程式無法通過編譯。

如果我們清楚了前述這些情況,當然就很容易了解,一般程式語言一旦有結構化要求之後,就很難再以不結構化方式設計程式了。 FORTH 不然!這也是一項它與一般程式語言有所不同的地方。通常一個市售的標準 FORTH 系統,編譯程式內也設計了結構化檢查的功能,但 FORTH 系統沒有使用者不得操控編譯程式或執譯程式(Interpreter)的嚴格限制,只要你有本事,你就可以進行隨心所欲的彈性運用,重點則在不要破壞了系統或毀了自己的設計。 FORTH 給你這種權力,但後果自行負責,是真正名符其實的『自由程式語言』。

許多年前,我在使用 FORTH 設計數值分析程式時,曾想直接引用一個 Gauss 法求解多元聯立方程式的 FORTRAN 程式,就遭遇到非結構化的問題,當時不想修改程式來適應系統,卻想操控 FORTH 系統來磨練本領,因此將問題提交到中山科學研究院舉辦的週五研討會上討論,陳爽提供了解答,他不愧為 FORTH 系統的個中好手,詳細的說明系統性能,並設計出解決方法,對此後的所有 FORTH 系統是否永久有效? 不能保證,特此聲明。此段記述納入這篇短文,目的在除了不該掠人之美外,也期盼在 FORTH 發展逐漸式微的今天,能與大家分享這個早年不易散播的技術資料。

在正常情況下,這種破壞結構化的程式設計是不被許可的,因為程式無法適應編譯程式,一般 FORTH 系統遇到此一情況,通常會顯示錯誤訊息,且無法通過編譯。我最近均努力於發展 eForth32 系統的應用程式,在一次偶然的試用中,發現這種程式可以通過此系統的編譯,但當然無法被正確執行,原因是 eForth32 非商售的標準系統,它可以不對出此錯事負責,系統為了精簡設計,在編譯結構化分支指令時不設計彼此配對的識別碼,這也是此系統跑得很快的原因之一。

無論 FORTH 系統結構為何?如果瞭解 FORTH 系統在編譯時的一項重點,這個破壞結構化以迎合自己要求的問題就可以解決,此一重點就是──『FORTH在編譯時不用回返堆疊來傳遞參數,反而使用數據堆疊來傳遞參數』。 FORTH 系統在編譯到結構化指令時,通常由於該類指令必須成對使用的要求,會在數據堆疊上留下兩個數值。一是成對要求的識別碼,例如: IF 配 THEN 有一代表碼, DO 配 LOOP 是另一代表碼。另一數值則為結構分支所需的位址值,以便分支指令可以據以編譯入系統。必須特別強調的是, eForth32 系統則簡化成只有一個位址值。

根據這個特點,我們就可以指揮系統但不破壞系統來完成我們的要求,可以形成迴路的指令不僅只是一兩個,我們不宜一一舉例來長篇討論,讀者應舉一能反三,自行活用,須要時仔細拜讀系統結構化指令的詳細內容,也就不可或免。本文僅舉一簡單例子,並設計程式測試,以驗證無誤,提供讀者日後引用時參考。設計驗證的方法也是另類技術,在正式引用前所應先行完成的工作。

例如:我們希望一個 IF ..... THEN 的結構能與 BEGIN ..... UNTIL 結構相跨,還能讓程式正常的被執行。也就是希望程式寫成形同下列的格式,照樣可以被執行:


 

: NON-STRUCTURE
  Statement-1
  Flag-for-IF
     IF     Statement-2
            BEGIN   Statement-3
     THEN   Statement-4
            Flag-for-UNTIL
            UNTIL   Statement-5  ;
 

分析過指令詳細內容後,我們可以發現,兩組結構化分支指令都各有一相關的位址值, IF 可能要跳往 THEN , UNTIL 可能要回跳至 BEGIN 。系統在編譯到 IF 時會留下一個當時系統的位址在數據堆疊上,等到編譯 THEN 時,再將當時系統的位址回填入此堆疊上留下的位址內,完成一組結構化配對指令的要求。同理,系統編譯到 BEGIN 時,也會留下一個系統當時的位址在數據堆疊上,等到編譯 UNTIL 時,系統會安排一個條件分支指令,此留在數據堆疊上的位址,便會被填入此指令之後,以供須要跳躍分支時前往。如果我們不對留在數據堆疊上的兩個位址值進行處理,而仍能通過編譯,那麼編譯的結果,當然會造成跳至錯誤去處的現象, eForth32 系統即為如此。如果我們希望系統仍能編譯出正確的結果,那麼該處理的對象也就是在數據堆疊上所留下的位址值,由上列程式的判讀,我們也可以得知,處理的時機在 BEGIN 與 THEN 之間,至於在 Statement-3 的前面或後面就不太重要了,端視 Statement-3 是否也會在數據堆疊上留下參數而定,因此較妥善的位置,應該是就緊接在 BEGIN 位置之後,處理的方法為在程式的這個位置插入一小段程式,宣告要求系統由編譯狀態轉入執譯狀態,然後立刻執行翻轉數據堆疊頂部當時被留置下來的兩個數值,末了再宣告系統恢復原有的編譯狀態,繼續編譯,這樣就可以人為的方式騙過系統,好像不曾發生過事情一般,完成正常編譯,而且程式可以執行。所添加的三個指令並不會增加系統的編譯量,因為它們在執譯狀態時立即被執行掉了。對 eForth32 系統而言,改寫後的程式如下:


 
: NON-STRUCTURE
  Statement-1
  Flag-for-IF
     IF     Statement-2
            BEGIN  [  SWAP  ]  Statement-3
     THEN   Statement-4
            Flag-for-UNTIL
            UNTIL   Statement-5  ;

至於一般 FORTH 系統,因為多了一組配對識別碼的關係,此處的 SWAP 要改成 2SWAP (不保證都有效!) 。我們就根據原描述程式,設計驗證程式如下:

VARIABLE flag-for-IF -1 flag-for-IF ! VARIABLE flag-for-UNTIL -1 flag-for-UNTIL ! : Statement-1 .” Statement-1 ” ; : Statement-2 .” Statement-2 ” ; : Statement-3 .” Statement-3 ” ; : Statement-4 .” Statement-4 ” ; : Statement-5 .” Statement-5 ” ; : NON-STRUCTURE Statement-1 Flag-for-IF IF Statement-2 BEGIN [ SWAP ] Statement-3 THEN Statement-4 KEY? UNTIL Statement-5 ;

注意!我們用 KEY? 取代了該處原本應該設計的一個可調內容之旗標變數 flag-for-UNTIL ,原因無它, BEGIN ..... UNTIL 如果被編譯完成且被執行, UNTIL 前列的變數就不再有可被調變的機會,它將形成一個永不終止的無限迴路,因此改用 KEY? 來人為控制。這個指令很好用,它是呈現出鍵盤可以被當作一個即時性(Real-time control)控制器使用的專門指令,它具有 FORTH 即時性控制的精神本質。

執行上述編譯完成的程式後,可以根據螢幕上列印出來的文字,判斷你所設計的程式正確與否?此處所舉的例子僅代表本文論述的問題可以被很技巧的解決,至於任意結構化指令的隨意混組,又想要突破其結構化限制,如何解決?則應視情況而定,了解指令執行機制,仿照本文敘述,找到處理對象、處理位置與處理方法並不困難,這不是一件經常要做的事情,不須強記,只須留下本文以供日後需要時參考,另外,則應記得 FORTH 經常與眾不同,它可以變化萬千。

如果仔細深究我設計的 ABC Forth 系統,就具有本文探討的特性,能夠在高階程式的環境,破壞結構化規格。它的應用實例,可以從我在這個網頁中曾經貼出之有關鑑定質數的程式中發現。

例如:程式用來鑑定某一數字是否為質數時,程式的設計方法是核算從 2 開始,直到該數字之開平方以前,該數能否被這些質數除得盡?能除得盡就不是質數,都除不盡就是新產生的質數。這樣的程式,在設計時,我採用了 BASIC 語法中有固定上限指標的 FOR ..... NEXT 迴路完成設計。

為了能在已核算出能被除得盡時不必續算剩餘指標,我就取巧的利用了執行一次 2drop 的技巧,令程式可以從 FOR ..... NEXT 固定迴路中跳出迴圈。這樣做,可以大量節省執行次數與執行時間,就是一種非結構化的用法。

能這樣做的原因,是因為我把 FOR ..... NEXT 迴路的指標 I 之上限與現行數字設計成放在數據堆疊上運作的關係,所以能輕鬆的只用 2drop 指令實現非結構化設計。實際程式可以從本網頁中已貼出的文章中找到,請自行參閱。

2026年2月16日 星期一

劃線程式

劃線程式


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


要電腦執行在兩點間劃出一條直線的程式設計,看似簡單,實不單純,想用電腦控制外部機械,劃出一條直線,狀況相同。有許多劃線的方法曾被大家探討,但在理論與實務間存在著一些問題,要實作後才能體會問題所在,理論根據可以任找一個,程式工具也可以任選一種,實現它的過程所會遭遇的問題,則可能都是一樣的。

本文探討一份已在美國申請取得專利之公開資料的內容,並以 eForth32 於 DOS 環境內的 EGA 可繪圖模式為基礎,進行程式測試設計,文內記述了測試結果、遭遇的問題、及其解決方法。

專利資料是公佈於網上的公開資料,欲正式使用者,必須自行接洽專利擁有者。本文僅對這份專利的內容進行探討,但絕不實際運用,因為,文末另附乙份沒有專利問題的標準 FORTH 程式,可以毫無問題的直接應用。

這個主題在許多方面有利用價值,例如:二維系統中的機器運動控制──焊接、繪圖、打孔、定位、置放零件…等眾多電腦自動控制機械中,均可派上用場,若延伸發展至三度空間,則可供機器人運動控制之用。


1 原理與依據

與電腦系統相關的劃線環境,可以說幾乎全是由方格子柵樣的點所構成的,假設我們只能用這些微觀不連續的點來形成兩個座標間的一條線,應如何劃?

我們取 BASIC 語言,其程式具有幾乎等同於不改數學式子的特點,來解釋這個問題,就非常容易了解,如果將其簡化成最容易解釋的幾何圖與數學式子,就如同下述:

假設如圖一所示,在 ( X1 , Y1 ) 與 ( X2 , Y2 ) 兩點間欲劃之線應由許多相鄰卻不連續的點所構成,其中各個點的座標為 ( X , Y )。
根據幾何相似三角形原理,很容易看出打算劃出之點,其在線上的座標被表示為 ( X , Y ) ,而 Y 之值為:

Y = Y1 + ( ( Y2 – Y1 ) * ( X – X1 ) / ( X2 – X1 ) )

以 BASIC 來寫程式時,其結果為:

FOR X = X1 TO X2
Y = Y1 + ( ( Y2 – Y1 ) * ( X – X1 ) / ( X2 – X1 ) )
PLOT ( X , Y )
NEXT X


圖一


以上的敘述,可以算是已經解釋完了兩點間劃一條直線的基本原理,但實際情況絕非如此單純。首先,實際的整數數位環境中,通常未必每次都剛好有一可劃之點的座標,剛好等於算出來的 ( X , Y ) 非整數數位座標,其次,當由一點逼近到另一點的方式必須由幾條線段完成時,必須講求均勻的效果,程式設計者得為電腦設定一套計算完畢後劃那一點的規則,最後才會有較佳結果,這些都是劃線方法強調的重點。至於利用浮點數計算能力,先行算出該繪之點的座標,再利用浮點數轉換成整數的技巧達到目的的方法,則不在本文討論之列,因為,經常會遭遇到的環境,通常都缺乏浮點數計算能力。

我們絕對不應當將兩點間劃一條直線視為理所當然的事情,以為直接叫用系統中已經設計好的劃線指令來用就可以了,螢幕並不代表所有的環境,要到螢幕以外的地方去劃一條直線,程式必須自己設計,通常不是現成可取的。反過來講,如果掌握了劃直線程式的機制,那麼想要在任何可顯示圖形的螢幕上劃直線,就不困難,如此便形成探討這個主題的價值,也莫怪有人以此申請專利,良法不願隨便公開了。本文不想侵犯專利,文內說明了引用來源,但前述已經強調,理論與實務間存在差距,實作後才能有所體會與發現,細讀此文,輔以自行磨練,以後面對相同問題時,就能顯現本文之效用。

作者係先選定主題後才去尋找依據,並透過網路尋找答案,可依據的理論不勝枚舉,本文選取一個較佳的方法,摘要其中最主要的文字敘述,作為原理依據。謹詳實記錄其來源如下:

網頁: www.patenstorm.us/patents/6847362 (附記 : 我於 20260216 進行核查操作後確定這個網頁已經失聯)
Inventors: Lu, Chung-Yen Yao, Jo-Tan ( Taipei, Taiwan )

根據:

 
1. A fast line drawing method, comprising the steps of:

(a) receiving the coordinates of two end points as (x1,y1) and (x2,y2);

(b) assigning the coordinate (X,Y) of a current point as (x1,y1), computing
the differences of x and y coordinates of the end points as D(x) and
D(y), assigning Q as the integer part of D(x) over D(y), and
assigning the initial sum of error E as -D(x), and two constants K1
and K2 as 2*D(y) and 2*Q*D(y);

(c) checking whether the x-coordinate of the current point (X) has reached
the x-coordinate of the end point (x2);

(d) testing whether the value of E is non-negative;

(e) drawing a point at the current point if E is negative, updating the
x-coordinate of the current point (X) and E by X+1 and E+K1 respectively,
and returning to step (c);

(f) updating the y-coordinate of the current point (Y) and E by (Y+1) and
(E-2*D(x)) respectively if E is non-negative;

(g) checking whether the x-coordinate of the last of Q points (X+Q-1) is
less than the x-coordinate of the end point (x2);

(h) drawing a span of pixels from (X,Y) to (X+Q-1,Y) if the x-coordinate of
the last of Q points (X+Q-1) is less than the x-coordinate of the end
point (x2);

(i) drawing a span of pixels from (X,Y) to (x2,Y) if the x-coordinate of
the last of Q points (X+Q-1) is not less than the x-coordinate of the end
point (x2); and

(j) updating the x-coordinate of the current point (X) and E by X+Q and
E+K2 respectively, and returning to step (c).

2. The method as claimed in claim 1 further terminating the line drawing if
the x-coordinate of the current point reaches the x-coordinate of the end
point (x2).


上列資料來源中,附帶了相關的流程圖,再引用如圖二所示。原資料中也附有以 C 語言寫成的相關程式,如表一所示。展示程式當然不能執行,只能供作參考,原因為何?與前列的 BASIC 語言說明程式一樣,只是輔助的說明工具。實際設計出程式,才能了解理論的說明資料與實際應用上的差異,這也是本文實現程式設計最主要的重點所在。


2 系統選擇

接著,我們可以回到 FORTH 的領域了。為了實現這個程式,作者斟酌了許多可選用的軟、硬體,最後選定以 eForth32 在 DOS 環境的 EGA 模式下模擬,實際顯示功能與效果。為說明之須,並易於了解,盡量使用最簡單的指令,不修飾程式的美觀及執行速度,但求促其實現,以供日後參考。

eForth 系統尚未完全可以使用於當前的個人電腦硬體與普遍被採用的軟體作業系統中, FORTH 界有 Win32Forth 更適合這種環境,我亦相信 Win32Forth ,可以更容易達到目的。但根據我們多年的經驗,如果一向只用系統現成提供的功能,使用者便失去了經由 FORTH 了解電腦相關技術的機會,尤其是當系統功能多如牛毛時,使用者容易失去方向。 FORTH 老手一向領著系統走,而非被系統領著走,否則如何奢談操控電腦?更何況本文的主題幾乎是只用最簡單的 FORTH 基本指令就可以完成,殺雞焉用牛刀?若為日後的實務著想, eForth 相當適當。

eForth 的出現是 FORTH 發展的一大創見,其可貴的程度有甚於為 FORTH 指令爭取出 ANSI 標準,因為前者未必為必然的,而後者可以說是必然的,它的出現,就好像是幾何原理的發展到達最後成熟的程度時,數學家歸納出五個基本公設與一些基本定義,然後就可以推衍出所有的定理與定律, eForth 就像是這五個公設,使 FORTH 更成熟了, ANSI 標準卻像那些基本定義,也有其必要性,使 FORTH 更完整了。

圖二


很可惜,有一段相當長的時日,因微軟作業系統的出現,令 FORTH 的發展受到阻礙,而這又恰巧是 eForth 初期誕生的階段,已經十幾年了, FORTH 界人人都知道它可以有很好的延伸性發展,卻僅有少數人做了後續工作。它出現的當年,作者曾用它來為一個 8086 族的 V25 CPU 單板電腦實現 FORTH 功能,知其基本性能。

表一


而 eForth32 的出現又稍解了這種遺憾,它刻意不改最原始的 eForth 基本精神,包括指令名稱與系統變數,卻繼續發展出許多難能可貴的必要系統功能,還是一個 16 位元環境下的 32 位元系統,精簡扼要,事隔十多年仍能令人愛不釋手,自然有其特色。 FORTH 的愛用者通常都是站在巨人的肩膀上往前看,許多巨人都令人尊敬,尤其該尊重他們的不吝公開,引用創作時,就應避免竄改原始設計,盡量站在巨人的肩膀上繼續發揮。

在 eForth32 中我們以其已經具有在 EGA 模式下繪出一點的功能做基礎,這個基本指令在其他相類似應用中,相當於一隻筆或一個刀具的放下動作,可以想像成在機臺上弄出一個點來,指令名為 EGA! ,它是一個作者自行設計完全獨立的低階指令,執行前要在堆疊上放妥三個參數:顏色碼及 X 與 Y 的座標值,至於要在螢幕上切換繪圖模式或文字顯示模式,作者仍叫用了 BIOS 中的 INT10 功能, INT10 功能的參考文件已經公佈不少了,足供我們所須,在 eForth32 中我們只要用到作者設計之 EGA 及 TEXT 兩個簡單相關宣告指令便足夠了,使用這兩個指令時,不須要先安放執行參數,作者未提供劃線指令,我們恰能為其增添一點貢獻,上列三個指令設計在 EGA.4 檔案中,功能摘要如下:

EGA ( -- ) 切換至 EGA 繪圖模式
TEXT ( -- ) 切換至文字顯示模式
EGA! ( colour, X , Y -- ) 繪出一個點

必須額外強調的是,凡涉及 IBM 硬體與 Microsoft 軟體的部份都是有時間性的,不能永遠流傳使用,日後軟硬體隨時代演進之後,這些部份僅供參考。


3 程式設計過程摘要

根據圖二中的流程圖,直接翻譯成 FORTH 程式並不困難,然而實際程式中必須添加許多部份,才能完成設計,為節省篇幅,下列表二直接列出一個可以執行的最後結果,然後我們再來解說發展過程。值得強調的是,發展過程就是採用了 FORTH 精神,可以快速的即時測試、除錯、改寫、添加以至於痛快地完成。

首先,我們必須接受一個事實,那就是我們根本不可能一次就完成由文字敘述到最終程式的翻譯編寫工作,出錯絕對難免,甚至於因為軟體動到了硬體,不斷地造成當機現象也絕對難免,要歸原(Reset) 硬體系統,重新開機。

初期譯寫出來的程式,要想很快的就能由實際硬體看到結果,大慨非屬 FORTH 程式語言不可了,這是有立即執行功能之程式語言的好處,莫管它程式成敗與否?人人都喜歡能夠趕快試一試程式的特殊性能,因為除錯要根據眼觀現象後才能再作決定,看完顯示後才能想問題,快不起來就影響發展,別種程式語言的產出,因此不如 FORTH ,實作可以體會。 在程式設計的小技巧上,只要問題不太複雜,指令名稱並不需要非得使用很貼切的全名,若以兩個字母當作指令名稱就不容易衝突,那麼,程式內就只用兩個字母作為名稱,要不然三個也可以,以前 PolyForth 就習慣只留三個字母,大家依然喜歡使用這個系統。

單純根據原始資料直譯出來的 FORTH 程式,基本上會有問題,尤其是在邊界情況,例如:末點與原點的X座標相同時,就算Y座標不同也不繪了。程式設計時,要在主要流程的關鍵點,進行例外處理,根據經驗,程式設計時的邊界效應,通常會是評鑑測試時的重點,果不其然,這裡就是如此。

另外一個更為嚴重的問題是:原始資料講的是一個通用情況,在幾何數學中,指的是只在X─Y平面座標的第一個象限有效。因此,當程式辛苦的除錯、測試完畢後,可以發現,只有當末點的 X 與 Y 座標值均大於原點時,繪線才會有效,這個現象依靠 FORTH 的立即執行功能,可以立即發現。謹慎思考之後,決定保留原專利的基本精神,另行設法補充解決。簡而言之,是採用了所謂的『在虛像中計算,在實境中劃點』之小技巧,完成了設計。

以幾何圖形思考這個問題時,原點無論想向四個象限中的那一個方向劃一直線,都可以在第一象限中找到唯一一條對應線的虛影像,只不過兩者之間的座標值有所不同,也不單純只是正負號的不同而已,但是可以經由一套單純的算術換算,建立對應關係。程式設計的發展過程中,這個問題以增設記憶變數,留存旗標值,來決定應當繪點的象限,然後根據固定的核算規則,算出實際應繪點的座標值,才實際繪點,演算機制永遠保留在第一象限中,新加的程式只須於編輯時插入適當位置便可。

此外,另有一些實作、實用後才能發現的問題值得記述:

早年 FORTH 使用者慣用的有限迴路指令是 DO ----- LOOP ,長久的使用經驗告訴我們,在實際環境中,讓有限迴路執行由負數過零點的情況,是會出問題的,才有後來有限迴路指令改用 FOR ----- NEXT 的演進,本程式設計時也免不了有此問題,因而體會出演進的意義。我們想控制的物品通常是處於正整數的實體,不是負數環境中的虛體,排除它會好用些。舊的 DO ----- LOOP 有限迴路指令在執行前,需指定兩個數字,作為迴路的上下限,可能會有過零點的情況,新的 FOR ----- NEXT 有限迴路指令在執行前,只須提供單一個執行限量,可以避免過零點的情況發生,實際上通常就是一個要執行幾次迴路的數字,符合實際。

有限迴路內可用的指標 I 或 R@ 也未必一定得在迴路中使用,基本起始值可以經由堆疊傳遞入迴路,然後在其中再設定加減量,來達到每次加減固定量的要求,自動控制經常要用到這項技巧,不可不知。本程式在發展過程中還經驗了一個很好的實務,經由 FORTH 的即時性能,曾經發現了程式執行時,繪線的方式是由末點繪向原點,這與該要求的實際情況不符,形同被控制機具直線運動的逼近方向相反,它可以由迴路中的變量是增或減來解決,但是,如果系統沒有方便到擁有 FORTH 的即時性能,發展過程必定很不方便。

新近電腦速度很快,記憶容量很多,對應用發展有利。可是,我們有時會碰到一些情況不領這些情,例如:自動控制機械的動件通常是有重量的,物理上說是具有慣量,那麼每逢要控制機具運動時,就不得不考慮速度問題,而且通常是並不想讓機具運動的速度,快到像電腦決定事情的速度那般快,有慣量的物體,是不能夠像慣量幾乎可以忽略掉的電子掃描光點,那樣子被指揮運動的。程式設計時,還得希望程式被執行時,能夠配合要求慢下來,以便觀察真正的效果, FORTH 面對此情況時的便利性更不待言,更容易進行調適設計。以前我們常設計一個被稱之為 Delay 的指令,它強迫 CPU 執行一定量的迴路,而迴路沒有內容,俗話說就只是乾耗,使用者自己去體會,感覺出大約一秒需多少量來做為一個延遲單位,此指令便能被插入程式中,來達到執行時變慢的效果,以便執行效果能夠被觀察。

前面已經強調,使用的是一個模擬環境,自然就得考慮繪線環境有其限度, eForth32 有 32 位元的處理功能,單整數可以大到四十二億以上 (4,294,967,295) 而不再是 65535 ,這也代表此量的尺度足夠涵蓋人類可能面對的實體點的數量,套句俗話說,它不僅只是幾萬點的解析度,而是四十幾億點,夠大到用不完了。然而本程式在實際繪圖時,EGA硬體的設計只能讓使用者在 640X350 點形成的平面空間自由操作,仔細研究繪出一點的指令 EGA! 便能明白這項限制,平面空間是二維的體系,每一點對應到實際的硬體記憶體時,要考慮記憶體的排列秩序是一維的結構,因此指令中便得大量設計這種對應關係的換算處理,本文僅借用原作者的設計,不打算深入解釋。

前述還曾提到,如果所繪之線必須由幾條線段連接完成時,這些線段要能均勻,所用方法才能稱好,要快速觀察後得知效果, FORTH 又幫了大忙。

eForth32 本身因強調精簡,有些性能會讓使用者感到不太理想,例如:它的執譯程式(Interpreter)就簡化到幾乎不可再簡的程度,萬一輸入指令須要的堆疊參數不足時,螢幕上出現許多的亂碼,原因是此系統刻意省了執行幾個檢查系統狀況的指令,但無傷於系統的繼續使用。 1987 年四月份『FORTH期刊』拙作 “Forth結構與執行原理” 一文中提到,一個去蕪存菁的執譯程式,結構可以相當簡單,而 1990 年公開推出的 eForth ,就進行這樣的安排,若非它的實作與實現,我們不易體會與瞭解這樣安排的結果會是如何?我們必須瞭解執譯程式是一個永無休止的執行迴路,令 FORTH 在每執行一個指令後,都得循迴一次,如果迴路內工作做多了,當然耗費時間,系統速度就會變慢。 FORTH 是透通且很有彈性的,修改它並不困難, eForth32 提供了很好的工具,容易完成。 M.4 是一個完整的蛻變程式,以前要等到學會 FORTH 的後期,才有機會接觸第四級難度之所謂的蛻變編譯(Meta-compiler),以蛻變程式來產生標的系統(Target system), eForth32 給予作者最大的印象是 : 一試用這個系統,就可以先執行蛻變編譯出系統自身,然後試用無誤,令人痛快高興?

eForth32 當然仍有些欠缺,難與發展較為完善的 Win32Forth 相比,但因此卻給予喜歡玩系統功能的愛用者很好的機會,它留下了許多可以繼續發展的空間,例如浮點算術的功能,與視窗系統間的連接功能,甚至於本身執行速度的改善,與人機界面的友善要求,都還很值得繼續發展。


 
\ FAST LINE DRAW
CODE CRT ( n --- \ send a crt mode command )
        BX AX MOV
        $10 INT
        BX POP
        DX POP
        NEXT
        END-CODE
: EGA ( --- \ setup 640 by 350, 16 color graphics mode )
   16 CRT 0 20 AT ;
: TEXT ( --- \ setup 80 column 24 line text mode )
   3 CRT ;
CREATE MASKS
   $80 C,  $40 C,  $20 C,  $10 C,
     8 C,    4 C,    2 C,    1 C,
CODE EGA! ( I X Y --- \ DIRECT TO EGA no BIOS )
   ( bx=y*80)
   BX AX MOV  BX 1 SHL  BX 1 SHL  AX BX ADD   ( bx=y*5   )
   BX 1 SHL   BX 1 SHL  BX 1 SHL  BX 1 SHL    ( bx=y*5*16)
   ( ax=x/8)
   CX POP  2 # SP ADD   CX AX MOV   AX 1 SHR  AX 1 SHR  AX 1 SHR
   ( bx=y*80 + X/8)
   AX BX ADD
   ( es=video memory base address)
   $A000 # AX MOV  AX ES MOV
   ( set video mode 2)
   $3CE # DX MOV   5 # AL MOV  DX AL OUT
   DX INC ( 3cf)   2 # AL MOV  DX AL OUT    ( mode 2)
   DX DEC ( 3ce)   8 # AL MOV  DX AL OUT    ( select mask register)
   ( get the proper bit mask)
   7 # CX AND  MASKS # DI MOV  CX DI ADD
   CS: 0 [DI] AL MOV
   DX INC ( 3cf)               DX AL OUT    ( column bit mask)
   ( ????)
   ES: 0 [BX] AL MOV
   AX POP  2 # SP ADD
   ( write the dot)
   ES: AL 0 [BX] MOV
   ( reset video mode)
   DX DEC ( 3ce)   5 # AL MOV  DX AL OUT
   DX INC ( 3cf)   0 # AL MOV  DX AL OUT
   DX DEC ( 3ce)   8 # AL MOV  DX AL OUT
   DX INC ( 3cf) $FF # AL MOV  DX AL OUT
   ( pop stack into dx:bx)
   BX POP
   DX POP
   NEXT
   END-CODE

\ random number generator
VARIABLE SEED
: RANDOM  ( max -- n )
   SEED @ 31415927 UM* DROP  87654321 +  DUP SEED !  SWAP MOD ;

VARIABLE COLOUR    15 COLOUR !
VARIABLE RX1  VARIABLE RY1 ( Real x1,y1 )
VARIABLE RX2  VARIABLE RY2 ( Real x2,y2 )
VARIABLE XQF  VARIABLE YQF ( Quadrant flag--- 0:1st Q, 1:non-1st Q )
VARIABLE X1  VARIABLE Y1   ( Image x1,y1 )
VARIABLE X2  VARIABLE Y2   ( Image x2,y2 )
VARIABLE DX  VARIABLE DY
VARIABLE QQ  VARIABLE EE
VARIABLE K1  VARIABLE K2
: PRECAL
         X1 @ RX1 ! Y1 @ RY1 ! X2 @ RX2 ! Y2 @ RY2 !
         X1 @ X2 @ > IF 1 XQF ! X1 @ DUP X2 @ - + X2 ! ELSE 0 XQF ! THEN
         Y1 @ Y2 @ > IF 1 YQF ! Y1 @ DUP Y2 @ - + Y2 ! ELSE 0 YQF ! THEN
         X2 @ X1 @ - DX !
         Y2 @ Y1 @ - DY !
         DX @  DY @  / QQ !
         DX @ NEGATE EE !
         DY @ 2 * K1 !
         DY @ QQ @ * 2 * K2 ! ;
: AFTCAL RX2 @ X1 ! RY2 @ Y1 !
         0 RX1 ! 0 RY1 ! 0 X2 ! 0 Y2 ! 0 RX2 ! 0 RY2 ! 0 XQF ! 0 YQF !
         0 DX ! 0 DY ! 0 QQ ! 0 EE ! 0 K1 ! 0 K2 ! ;
\ X Quadrant flag check then map onto Real if needed
: XQC ( Image x -- Real x )
   XQF @ IF DUP RX1 @ - DUP + - THEN ;
\ Y Quadrant flag check then map onto Real if needed
: YQC ( Image y -- Real y )
   YQF @ IF DUP RY1 @ - DUP + - THEN ;
: GXSPAN
   X1 @ SWAP  ABS  FOR 1+ DUP COLOUR @ SWAP XQC Y1 @ YQC EGA! NEXT DROP ;
: GYSPAN
   Y1 @ SWAP  ABS  FOR 1+ DUP COLOUR @ SWAP YQC X1 @ XQC SWAP EGA! NEXT DROP ;
: GLINE  ( x y -- )
  PRECAL
  BEGIN
  X2 @ X1 @ =
         IF
         Y2 @ Y1 @ -  ABS GYSPAN AFTCAL EXIT
         THEN
     EE @ 0 <
         IF  COLOUR @ X1 @ XQC Y1 @ YQC EGA!
             X1 @ 1+ X1 !
             EE @ K1 @ + EE !
         ELSE
                Y1 @ 1+ Y1 !
                EE @ DX @ 2 * - EE !
                X1 @ QQ @ + 1- X2 @ <
             IF
                X1 @ QQ @ + 1- X1 @ - GXSPAN
             ELSE
                X2 @ X1 @ - GXSPAN
                AFTCAL EXIT
             THEN
                X1 @ QQ @ + X1 !
                EE @ K2 @ + EE !
         THEN
  AGAIN ;
: GMODE DONE TEXT PAGE EGA ;
: DRAW ( x y -- )
  Y2 ! X2 !
  X2 @ X1 @ = Y2 @ Y1 @ = AND
  IF AFTCAL EXIT THEN
  GLINE ;
: INIT ( -- )
  0 X1 ! 0 Y1 ! AFTCAL COLOUR @ 0 0 EGA! ;
: RECTANGLE INIT 639 0 DRAW 639 349 DRAW 0 349 DRAW 0 0 DRAW ;
: ICECOLUMN ( n -- )
  FOR EGA INIT
  0 639 FOR 1+ DUP 100 RANDOM DRAW NEXT DROP
  NEXT RECTANGLE ;
\ One second delay
: SECOND ( n -- )
  FOR 200000 FOR NEXT NEXT ;
: COLORRUN ( -- )
  14 FOR I 1+ COLOUR ! 0 ICECOLUMN 1 SECOND NEXT ;
: STAR ( n -- )
  GMODE FOR COLOUR @ 639 RANDOM 349 RANDOM EGA! NEXT ; 

表二


4 程式說明

這個程式,免不了要用到許多變數來暫存變化值,程式處理此問題時,不用節省,直接宣告想用的變數,其中凡含 X 者均與 X 方向座標有關,凡含 Y 者均與 Y 方向座標有關,硬體系統中的 ( 0 , 0 ) 點位於實際螢幕的左上角,橫軸表 X 方向,縱軸表 Y 方向。 COLOUR 變數存的是彩色螢幕上可繪出點的顏色代表值,可以由 0 到 15 。 X1 、Y1 用來存每次欲繪線時原點的座標值。 X2、 Y2 用來存欲前往之末點的座標值。 XQF 、YQF 用來存末點與原點之間的象限關係,但只是 1 或 0 的旗標值,二者均為 0 表仍在第一象限中繪圖,同理,二者均為 1 時表應該在第三象限中繪圖。 DX、DY、QQ、EE、K1、K2 則均仍延用繪線根據文獻中所用的名稱,不必解釋。

比對設計完成的程式與本文引用的原始流程圖後,可以發現有幾項重要的不同。首先是在邊界處的處理,這也是程式設計時經常會出現的通病,例如:本程式係以 X 方向為基礎,然後考慮 Y 方向的變化以決定線要如何劃,那麼當所欲前往之點,其 X 座標與原點相同,而不論 Y 是否也相同時?原流程圖就不劃而結束了,完成測試的程式對此必須安排例外處理,至少要劃一條 Y 方向的直線。其次是在劃線段的最後情況時,我們可以確定是在執行最後一段了,因此結束後程式也該終止,否則程式在回到迴路起始點 X 座標的檢查時, X 的量可能因為超出而不停止劃線,變成停不下來的劃線程式。當然,上述問題是可以透過其他方法做更好的解決,程式可以變得漂亮一些,不過,本程式強調忠實的反應被引用原文的敘述,盡量不作更動,以 FORTH 的高度彈性來配合發展,顯現出用 FORTH 發展軟體的好處。

最後,必須指出的是:原引用資料僅能在第一象限中完成工作,也就是說,當欲前往之點,其座標值 X 與 Y 均大於原點時,方法才有效。為了解決這個問題,本文自創了一個可行的方法,簡而言之,就是設法在虛像中演算,然後在實境中繪點,發展程式時,是由實際觀察執行結果,才完成這個技巧的。剛開始試將引用敘述轉換成程式時,因為只在第一象限狀況繪圖,只出現了上述的兩項邊界狀況,易於解決,隨後的測試發現其他象限都無法工作了,因此再插入增加另兩組變數宣告,使用的名稱是以 R 為字首的,如 RX1,RY1,RX2,RY2 ,此處姑且稱之為實際座標,也就是實際應該繪出點的座標,相對於此組變數,原程式的變數就成了虛座標了,但名稱是 X1,Y1,X2,Y2, 也就是被用來演算用的座標, X1,Y1 是繪圖中待繪點的座標值,會隨著演算的進行一直變化,而 X2,Y2 存的是由實境末點座標換算所得,在虛像中之末點座標值,以便維持演算計算能恆在象限 1 中進行。

虛像中演算,實境中繪點的方法,舉一由 (50,50) 繪至 (100,30) 之例,如下圖所示:

圖三


簡單敘述圖示過程為:當 (RX2,RY2) 二變數有小於 (RX1,RY1) 情況時,先求出對應於 (RX2,RY2) 的 (X2,Y2) 座標,換算原則是,必須將 RX2 或 RY2 轉換成大於 X1 或 Y1 的對應座標值 X2 或 Y2 。如上圖中欲由 (50,50) 劃至 (100,30) 時,因 RY2 之 30 小於 RY1 之 50 ,故先將旗標變數 YQF 設定成 1 ,然後將 RY2 轉換成大於 RY1 的對應座標值, Y2=30 +2*(50-30)=70。

然後仍用原法計算。

每次執行到要繪點時,檢查象限旗標變數為 1 或為 0 ?為 0 則直接繪點,為 1 則換算回實際應繪點的座標才繪點。如上圖所示,因 YQF 為 1 ,故要在實境中劃出一點的所有座標,都將由虛像中之 Y 換算回實境中的 RY ,以此圖例而言 RY=Y-2*(Y-50) ,然後才在實境中實際繪出 RY ,直到繪至 RY2 止。

所有的變數內容在完成一次繪線任務後都要歸零,只留下末點 (RX2,RY2) 的座標,改存到原點 (X1,Y1) 去當做新原點,供下一次繪線時使用,而下一個新末點的新座標要經由 (X2,Y2) 傳遞進入系統程式。


5 結論

能繪點又能繪線後的系統,功能可謂已經不少了,當初這個主題發展的目的是想將電腦螢幕設計成類似示波器的功能,然後可以利用電腦永遠不會感到疲倦,就適合用來長期監控被測試的訊號,有了這個程式就很容易達到目的了,剩下的設計,已經不太再有困難技巧值得強調。

發展程式時,為了想讓繪圖功能有較為活潑生動的觀感,另外引用了 FORTH 的一項便利功能,也就是特別容易納入亂數產生程式,宣告一個種子變數 SEED 後,再加一列程式設計,便完成了亂數來源,在應用上多麼的精簡?程式也可以動態的測試亂數性能,即使要電腦執行十萬個亂數,例如:執行 100000 STAR ,也耗不了幾秒,就可觀察到結果,相信 FORTH 愛用者的這款感受,是別種程式語言所難望其項背的。

以亂數配合繪出直線的指令,可以橫向繪出類似示波器的圖來,測試結果圖形很像是一幅冰柱圖,指令 ICECOLUMN 執行結果便是如此。

繪圖可以創作出許多生動的畫面,因此,本文的最重要意義,應該在後序的發展與應用。任何人在面對同樣問題時,一定可以發現,就僅只是要求電腦繪一條直線,不是唾手可得的時候,本文就具有高度的參考價值了。


6 另一套繪直線高階定義程式

由於前述文章引用了一個具有專利的文獻作為程式設計的根據,它簡明扼要而且執行快速,但本文僅進行學術性的探討,要進一步實際應用此法設計產品時會有侵權問題,不可如此。凡欲使用此法設計產品的讀者,請主動聯繫專利持有人。

如果欲繪的直線要求條件不高,繪線程式的演算速度要求不必嚴苛到非用低階的組合語言設計不可時,此處提供一個業經公開,沒有侵權問題的 FORTH 高階定義完整程式,足夠滿足許多場合的應用,我在數篇公開發表過的 FORTH 學術期刊中看到過這個程式,它已被許多人引用,劃線方法也叫 Bresenham 法,公益程式都不宣稱專利,可以大方使用。

另為配合 EGA 硬體顯示,我們仍須要用到在圖形顯示或文字顯示之間可以切換的操作指令 EGA 與 TEXT 。前文使用了一個 eForth32 作者 Rick VanNorman 自行設計繪出一點的低階指令 EGA! ,但此處為了增加讀者更多可茲參考的資源,另行設計一個直接呼叫作業系統內 INT $10 以完成繪點功能的精簡指令 PLOT ,可取代原用的 EGA! ,但它必須由低階組合語言來設計。程式末段則設計可直接執行的幾個示範指令,作為日後引用時參考。

另為考慮引用時的方便,此處亦為 eForth32 系統添加或重新定義指令名稱,使系統指令符合 F83 指令標準,這一修正部份,相關指令均收集於 ALIAS.4 檔案程式中。因缺乏原始文獻,無法詳述或有系統的介紹此程式繪出一直線的實質方法,但引用後驗證無誤,可以執行,我們僅能分享成果而不追究原理。程式密集列示如下:

在進行畫線技術時,我所使用的 PC XT 電腦尚無能夠方便擷取螢幕影像的軟體可用,現今唯一能找出的相關舊記錄,只剩一張當年我用此項技術開發出來的語音示波器照片,當時錄下的音譜為『大家好』。照片附貼於文末,作為紀念。


 
\ EGA graphics, borrowed from RIForth by Bob Illyes
\ High level line draw
LOAD ALIAS.4
CODE  CRT  ( n - - send a crt mode command )
		BX AX MOV
		$10 INT
		BX POP
		DX POP
		NEXT
		END-CODE
: EGA ( - - setup 640 by 350, 16 colour graphics mode )
 16 CRT 0 20 AT ;
: TEXT ( - - setup 80 culum 24 line text mode )
 3 CRT ;
: GMODE DONE TEXT PAGE EGA ;
VARIABLE COLOUR		15 COLOUR !
CODE PLOT ( x y - - )  \ 自行設計,非原作指令
		CX POP
		AX POP
		BX DX MOV
		COLOUR # DI MOV
		0 [DI] AX MOV
		$0C # AH MOV
		0 # BX MOV
		$10 INT
		BX POP
		DX POP
		NEXT
		END-CODE
\ Bresenham straight line
VARIABLE X			VARIABLE Y
VARIABLE _SENSE		VARIABLE SENSE
VARIABLE _DEL		 	VARIABLE DEL
: SET ( x y - - ) Y ! X ! ;  \ 起始定位指令
: RSET ( dx dy - - ) Y +! X +! ;
: DOT SWAP DUP >R 1- SWAP X @ Y @ PLOT
	_DEL @ + DUP DEL @ >
	IF DEL @ - _SENSE @
	ELSE 0
	THEN SENSE @ R> ;
: RLINE ( dx dy - - )
	OVER ABS 2* OVER ABS 2* 2DUP < DUP >R
	IF DEL ! _DEL ! SWAP ELSE _DEL ! DEL ! THEN
	0< 2* 1+ _SENSE ! DUP 0< 2* 1+ SENSE !
	DUP ABS SWAP ( count) DUP 0< NOT – ABS ( error) R>
	IF BEGIN DOT WHILE RSET REPEAT ( more vertical)
	ELSE BEGIN DOT WHILE SWAP RSET REPEAT ( more horizontal)
	THEN 2DROP 2DROP ;
: LINE ( x y  - - ) SWAP X @ - SWAP Y @ - RLINE ;
: ORIGIN ( - - ) 0 0 SET ;
: RECTANGLE ORIGIN
 639 0 LINE 639 349 LINE 0 349 LINE 0 0 LINE ;
: WHITENOISE ( - - )
 GMODE 0 175 SET
 639 0 DO R@ 175 50 RANDOM – LINE LOOP 
 RECTANGLE ;
: DEMO ( - - )  \ 繪出白噪音示波器顯示式圖形
 16 1 DO R@ COLOUR ! WHITENOISE
	KEY? IF LEAVE THEN
	2 SECONDS
	LOOP ;
: STAR ( n - - )  \ 繪出指定數量的繁星圖形,可供評鑑亂數產生程式品質用
 GMODE 7 COLOUR ! 0 0 SET
 1 DO 639 RANDOM 349 RANDOM PLOT LOOP ;
 


2026年2月2日 星期一

一畝三分地的程式人生

一畝三分地的程式人生


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


地不大,就只有 866.67 平方公尺。

我現在住家的總地積為 1012 平方公尺,是座落於市區的大平房,後院裡有個大隱於市的小菜圃(12公尺 X 2公尺),不在菜圃範圍內的土地,也都可以隨便種植東西。

我只使用 Forth 程式語言設計程式,桌機、手提、單板電腦我都有,都裝 Forth ,研究問題需要寫程式時,盡量只用 Forth 解決問題。

這樣的人生格局,已經步入坐 7 望 8 的年紀,卻照樣喜歡過著一畝三分地的生活,居家種菜是這樣,發展程式技術也是這樣,好好的過日子,總是能悟出一些事物的道理。

紐西蘭的氣候四季分明,冬日裡菜圃沒有什麼東西值得種植。一方面是低於 10 度 C 的溫度時,植物停止生長。另方面是寒冷的冬天很不適合從事戶外農務工作,我們就更應該順應天理,讓土地休養生息,春天到了才再重新開始。我從小就喜歡種植蔬菜瓜果,有 70 年以上的經驗。我住過的房子都帶有院落,可以經營一畝三分地式的精緻農作,這是一種健康的人生,不只是所有的產物都不用化學肥料、不用化學農藥,除草、墾地也只用簡單農具,我從實際工作中悟出道理,讓工作順遂、收成理想。

好的收成來自於勤奮的耕耘,農務工作也事事都得講道理。我從老鄰居 Norman 先生那裡學到,紐西蘭要等到十月份第四個禮拜一的勞動節以後才適合開始播種、種菜,除非種子難以買到,否則菜種最好不要自己培育,自己繁衍的種子容易劣化,價格又很便宜,市價大約都是每小包2.5元紐幣,份量適當。我今年就買了玉米、小黃瓜的種子。

現在,小黃瓜的收成都已到了尾聲,種植它時有些特性。需要育苗後移植,才不會被無殼蝸牛吃掉。種下去之後就不宜再做除草之事。它是淺根性植物,除草、翻土都會導致瓜株枯萎,沒有收成。烈日下只宜澆水,要吃嫩瓜就得勤收成,新瓜才能後繼跟上,天天去收成就天天有得吃。小黃瓜是不宜授粉的瓜,因為授粉的瓜會苦,公花甚至於可以全摘除了加進蛋花湯食用,我覺得很好吃。

甜玉米是去年 11 月 3 日播的種,生長期 100 天,今年 2 月 10 開始適合收成,就是這幾天。我早已練就了識別玉米成熟與否的方法,用手輕捏玉米的尾端,顆粒都已飽滿圓滾了,就是成熟度夠了。這時若不收成,鳥都會飛來啄食,螞蟻也上來產卵、繁殖,這樣的損失就很划不來了。

菜圃裡我還大量種植了青蔥與空心菜,這兩種東西都很容易種,居家生活就能天天都可就地採收食用,方便、乾淨是我家這個大隱於市菜圃的最大好處。

我割草後都倒進堆肥槽,槽就建在菜圃的正中央,槽的兩邊特別適合種植瓠瓜,等同於此瓜不用施肥,只須大量澆水,堆肥分解得快,瓠瓜大量吸收。只種一棵,就能每天授粉、每天收成。我們吃起來很奢侈,每天都只吃一個低於一公斤重量的瓠瓜,種子都還沒有長成就收來吃,嫩而略帶甜味,一棵每年都能有超過一百個授粉後才能獲得的嫩瓜。

小黃瓜邊不宜除草,空心菜與青蔥邊就得勤於除草才有收成。親身做過除草工作才能悟出除草的道理,草芽很小時,想除也難除,草苗長大了就吃肥、浪費養分,標的菜株必定長不好。我除草時自行判斷,要等雜草長得夠高了才動手。草苗若長太高、根部已經分枝後就難除了。拔草時只宜一根一根的拔,手抓最底部,直接連根拔起,就能除得乾乾淨淨。貪心多抓幾根,根部反而容易斷在土裡,這樣拔,只除了表面,沒有春風,只要有烈日也照樣重新又生而且大長。除草並非只做一次便可,何時又該除草了?只能自己觀察。除草的重點學問是你得分得出那些東西才是雜草,否則會在除草時拔了青蔥。我的菜圃內早已是苜蓿大量氾濫,它卻是有益植物,它不容易連根拔除,最好也別拔除,因為它的球根有將空氣中的氮固定於根部轉換成為氮肥的效果,留在原地,地裡就肥。我種甜玉米的區域,長滿了苜蓿,它除了具有保溼、供肥的效果,苜蓿還排擠其他雜草,有多重好處。

一畝三分地的人生哲理是這種生活方式並不足以可以孤立、隔離、健康的維生,我們每星期六仍須去跳蚤市場買中國青菜。小白菜、白蘿蔔、大白菜、甜的高麗菜等是我們的最愛,這幾種菜也只有華人的菜地供應得了,一般超市不是不賣就是品種欠佳。我們只在超市買菠菜、番茄、茼蒿、芹菜等,因為超市這方面的供貨比較乾淨,易於進行食用前的處裡。

這些長期培養出來的規矩,就像寫程式一樣,是已經駕輕就熟的生活程式。看看這些今晨拍攝的菜圃照片,我以實際成果展示這種一畝三分地的踏實人生。


我在公開貼出的文章中很少展示繪圖方面的程式作品,一方面是這種程式的規模都比較大,另方面是能夠純用 Forth 繪圖的系統並不太多,所以我就少談。

今天,我以經營一畝三分地過活的精神,簡介一個純由我自己發展出來的繪圖範例,它是在 Win32Forth 系統中繪製出橢圓的實例。這是還在使用 32 位元環境之作業系統時,於 Win32Forth 發展末期的零星作品,那時的程式心態就是缺什麼就建什麼,自己經營。

用過 Win32Forth 系統的人都熟知,現成可用的繪圖指令中,缺乏可以直接繪出橢圓的指令,因此,我就自建一套繪製方法。這樣的耕耘,要身體力行於實際設計,並看到輸出結果後,才能領悟出這樣的繪製程序是否好用? 它是 2015 年的產品,由於數學演算部份全用 BASIC 語法的格式書寫,多年之後,我仍能直接讀出繪製方法的重點,一目了然。

關於 Win32Forth 系統的繪圖功能,我不擬詳細解說,只能大致描述如下:
基本上,它藉著小畫家軟體的繪圖觀念建立起基礎架構,繪圖時採用物件導向程式(Object-Oriented Programming (OOP) )的書寫方法設計程式,把被繪出的圖形當作一個物件(Object)來處理。
所以,使用 :OBJECT ..... ;OBJECT 的架構框住整個繪圖程式。
程式的起頭,通常使用幾個物件導向式指令,宣告規劃出視窗物件的定點與大小、給予視窗框架的抬頭名稱或標示,然後才開始繪圖。這些物件導向式指令都以 :M ..... ;M 的格式框住其內容,最後一個 ON_PAINT: 物件導向式指令的執行內容才是整個繪圖程式的重點。
為了讓我設計之 ABC FORTH 語法的程式能與物件導向式繪圖程式相容於此系統,必須在 ON_PAINT: 指令結束的末尾,採用一次 [ CLASSES ] 立即執行式的宣告,作為結束,繪完圖後才能安返系統。

我的橢圓繪圖方式係採用表示橢圓的參數方程式(Parametric Equation)進行繪圖。橢圓的關鍵參數是其長軸與短軸,也就是 a 與 b 參數的大小,繪出時需要知道橢圓的中心座標點(可以在視窗中任意指定)與被旋轉的角度 d (逆時鐘方向旋轉),這樣就能在平面上任意一點繪出各種橢圓。尺寸過大也沒有關係,觀念上就是畫到外面去了,不影響橢圓圖形的形成。有關橢圓之數學表示式及其幾何圖形的意義,是高中生學習二次方程時的課題,本文完全省略不討論,請自行複習相關資料。至於為什麼採用計算出橢圓參數方程式的方式來繪圖?係取其方便而用之。
簡單的繪圖觀念只須依靠提筆(MOVETO:)與劃線(LINETO:)兩個物件導向式指令就能完成。為了繪出粗紅線條,我另用 BRUSHCOLOR: 與 FILLCIRCLE: 兩個物件導向式指令展示了成果。
這套繪圖程式有那些現成的物件導向指令功能? 請自行參考系統,內容龐大,但是沒有能直接繪出橢圓的指令。
範例程式我只貼出一份。今天早上我親自在 Ubuntu 20.04 作業系統加裝了 wine 軟體的環境中,啟動了 ABC660ForthV61505.exe 執行出此範例程式後,才操作 Shift-PrintScreen 硬拷貝視窗物件,最後得到這張完成圖。
另外加貼一些於 2015 年採用各種同類型程式繪製出來的圖樣,供作參考。由於程式都不簡短,因此只刊圖形,不刊程式。

一畝三分地的程式人生就是這樣形成的,我自覺這些題材有點樂趣,創作也值得與大家分享。


 
\ Parametric Equation plot
\ Copyright (C)2015 Ching-Tang Tseng, All rights reserved

3 INTEGERS I XI YI
10 REALS S/D XYmax t dt lt ht x y x0 y0
4 reals a b d r

\ (1)Points setting
400 VALUE N

N ARRAY XX
N ARRAY YY

\ (2)Upper and Lower t limitation setting
: init
{{ lt = 0 }} {{ ht = 2 * fpi }} ;             

\ (3)Ellipse function
\ Axis: a = 8 b = 2 rotate: d = 79 degree
\ Positive d value makes rotate CCW
\ display curve has been normalized
\ original point coordinate is at the center of the screen
\ x0, y0 are the principle function value without rotation
\ x, y are the function value after rotate
: f(t)
{{ a = 8 }} {{ b = 2 }} {{ d = 79 }}
{{ x0 = a * cos ( t ) }} {{ y0 = b * sin ( t ) }}
{{ r = d * fpi / 180 }}
{{ x = x0 * cos r - y0 * sin r }} {{ y = y0 * cos r + x0 * sin r }}
;

\ For normalized plotting using
: xyEvaluate BASIC
10 REM xy evaluate
20 LET { t = lt }
30 LET { dt = ( ht - lt ) / I>R ( N ) }
40 FOR I = 0 TO N
50 LET { t = t + dt }
60 RUN f(t)
70 LET { XX ( I ) = x } :: { YY ( I ) = y }
80 NEXT I
90 END ;

\ For normalized plotting using
: XYmaxFind BASIC
10 REM XYmax find out
20 LET { XYmax = XX ( 0 ) }
30 FOR I = 0 TO N
40 LET { XYmax = MAX ( XYmax XX ( I ) ) }
50 LET { XYmax = MAX ( XYmax YY ( I ) ) }
50 NEXT I
60 END ;

\ Store floating point value into the array
: CartesianConvert BASIC
10 REM polar to cartesian convert
20 FOR I = 0 TO N
30 LET { XX ( I ) = 250 + ( XX ( I ) * 200 / XYmax ) }
    :: { YY ( I ) = 250 - ( YY ( I ) * 200 / XYmax ) }
40 NEXT I
50 END ;

: DataPreparation ( -- )
  init
  XYEvaluate
  XYmaxFind
  CartesianConvert
;

:OBJECT EllipsePlot

<SUPER WINDOW

:M STARTPOS:  400 100 ;M
:M STARTSIZE: 500 500 ;M
:M WINDOWTITLE:
 Z" ABC FORTH Designer: Ching Tang Tseng (C)2015 " ;M 

:M ON_PAINT:

BASIC

300 REM text print

330 RUN 490 251 S" x" TEXTOUT: DC
340 RUN 251   1 S" y" TEXTOUT: DC
350 RUN   1 460 S" R(max)    = " TEXTOUT: DC
360 RUN   1 480 S" scale/div = " TEXTOUT: DC

400 REM XYmax, scale/division floating point values print
410 RUN 80 460 XYmax (FG.) TEXTOUT: DC
420 LET { S/D = XYmax / 4 }
430 RUN 80 480   S/D (FG.) TEXTOUT: DC


500 REM x axis and y axis plot
510 RUN BLACK LINECOLOR: DC
520 RUN 250   0  MOVETO: DC
530 RUN 250 500  LINETO: DC
540 RUN   0 250  MOVETO: DC
550 RUN 500 250  LINETO: DC

600 REM axis scale marks plot
610 FOR I = 50 TO 450 STEP 50
620 RUN 250  I MOVETO: DC
630 RUN 260  I LINETO: DC
640 RUN  I 250 MOVETO: DC
650 RUN  I 240 LINETO: DC
660 NEXT I

\ Change the value type from floating point to integer while plotting
900 REM function curve plot
910 RUN LTRED LINECOLOR: DC
920 LET XI = INT ( XX ( 0 ) ) :: YI = INT ( YY ( 0 ) )
930 RUN XI YI MOVETO: DC
940 FOR I = 1 TO N
950 LET XI = INT ( XX ( I ) ) :: YI = INT ( YY ( I ) )
960 IF ( XI < 49 ) OR ( XI > 451 ) OR ( YI < 49 ) OR ( YI > 451 ) THEN 980

\ For line curve
\ 970 RUN XI YI LINETO: DC
\ 970 RUN XI YI BLACK SETPIXEL: DC

\ For big dot curve
970 RUN LTRED BRUSHCOLOR: DC
972 RUN XI YI  2 FILLCIRCLE: DC

980 NEXT I

1000 END

[ CLASSES ]

;M

;OBJECT

: MAIN
  DataPreparation
  START: EllipsePlot ;

MAIN