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


2026年1月15日 星期四

線性組合

線性組合


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


在大學三年級以上的高等數學課程中,經常可以聽到老師在教學課堂上強調:解方程式時『解的線性組合亦為其解』。
以前在學時,只是把他背下來,沒有去深入了解,後來走進了社會,經常看到『線性組合』的術語,就開始根據孔子所說的『思而不學則罔,學而不思則殆』之道理來想問題。
結果發現,運用幼年還沒有 x, y 代數知識時期的思考方式想問題,最容易了解,而且要把問題反過來表達,才更清楚。
那時,若問 3 乘以多少能得到 6 時,這個在等號左邊的『乘以多少』移到等號右邊,就應該是被 6『除以多少』了。

同理,我就以同樣的方式思考何謂『線性組合』?與可以運用在那裡?的問題。

根據定義,線性函數有兩個重要特點,以數學式表示則為:

cf(x)= f(cx)
f(x+y)=f(x)+f(y)

其中,『乘以 c 』也隱含了可以『除以 c 』的意義,『加上』也隱含了可以為『減掉』的意義。

思考的時候,就用幼學技巧來思考,例如:

假設已經知道 x = 5 。那麼,『線性組合』類比上的發揮就可以這樣的式子表示:


 

2 * x = 2 * 5 ==> 2x = 10
3 * x = 3 * 5 ==> 3x = 15
2x + 3x = 2 * 5 + 3 * 5 ==> 5x = 25



顯示的結果就是:『線性組合』攪來攪去,實質意義上,卻仍然只是 x = 5 而已 ,性質不變。


這樣子去了解『線性組合』,以後在用到時,就不會懷疑它所代表的實用意義了。
舉例而言:

1. 我們在解矩陣的線性聯立方程式時,也是憑此『線性組合』的意義來進行演算的:某列乘上一個常數加到另一列去,矩陣的性質不變,為什麼?意義就是上述內容。

2. 我們在解所有的方程式時,都要牢記解的線性組合仍為其解來寫通解,其意義也在這裡。韓信點兵的問題,求出第一個最小正整數解後,還得照線性組合的特性來寫出所有整個序列的通解,才是正確的解答。

3. 我們在將循環小數化為可用兩個最精簡整數相除來表示時,求解的方法,也是利用線性組合的意義來求解。

4. 傅立葉級數的展開式,尤其表現了『解的線性組合仍為其解』的物理意義,術語上稱為線性疊加效應。

5. 人臉辨識系統的數學運算,也就是將整個圖形的影像數據,進行線性組合式的運算,來獲得 zoom in/zoom out 的效果,以便與基本庫存圖檔的設定尺寸吻合後,才進行比對,落定結果。

6. 斷層掃描得到的訊息,都是以 180 度半圈掃描測量所得形成的數據,每一點的內容,都是沿著徑向相關點之訊息的線性組合。

7. 量子電腦儲存訊息的技術,在運用時,更發揮了線性組合的觀念,才更能以三度空間的方式,儲存大量的訊息。

8. 相控陣列感測系統,無論是聲納或雷達,也是全靠線性組合的原理處理訊號,將所有感測到的訊息,快速算出線性組合而成的輪廓花樣,就能得到放大了陣列維度倍數的精密訊息。

最原始武器的陣列感測器系統,純用類比硬體製造,解析度就難以提高。後來,改採數位電腦處裡訊號時,卻遇到非即時性處理,運算速度不夠快,難以即時得到結果的困擾。 以後,利用量子電腦,可以利用大位元數量操作的優點,與量子態的感應式光速運算能力,相控陣列雷達的性能,就能藉著量子電腦達到大躍進式的性能提升,用到的最基本原理,仍然就是線性組合。


『線性組合』運用在無數的地方,只是大家沒去注意而已。

我在了解線性組合的科學意義後,做過延伸性的思考,自問『乘以一個常數還能相加』的特性,在複數體系中是否照樣有效?若想深入探討這個問題,就必須擁有能夠很方便於操作出複數運算的系統,於是自行設計出它來。
我所設計的數學運算體系,都能辦到這樣的功能,運算程式可以直接寫成使用複數作為基本運算單元的格式。

學校課堂上的老師,不會採用複數運算教學的方式來驗證各種理論給學生看,否則整堂課,可能只能教一題複數運算的題目。

我在知名國際論壇上發表過這方面的研究成果,並公開展示,當時,沒有想要丑表自己先進的念頭。卻在不久之後,發現有人刻意趁機捐獻這種程式給公開的相關網站,博取名聲。我很不以為然,但是沒有生氣而指責對方的這種行為,因為他也是論壇中來往過的對象,自己知道他會這樣做,以後小心便可。


線性組合原理,其實是大自然運行的法則,包括人類必須順應天理的優良人文思想,也是如此。

中華文化,強調大家應該遵守儒家的人倫觀念,繁延我們的子孫。這就是最正常、最適當的線性組合理念,能讓整個社會與民族,健康、安定的發展下去。一個爸爸結合一個媽媽建立一個健康的家庭,養育出健康的下一代,是再美好不過的事情。如果有人硬是要搞基因工程,強將兩個爸爸與一個媽媽的基因組合在一起,結果可能會生出有兩個腦袋的畸形連體嬰來,很難養活。


這裡有部影片,拍攝到很難得的兩隻火鶴同時餵養幼鶴的寶貴鏡頭。
影片看起來很血腥,好像是火鶴爸爸,凶狠的啄穿了火鶴媽媽的腦袋,流出腥紅的血乳,餵養著還不能覓食的幼鶴,吃完一頓血乳,還弄得滿身都是血。
其實,火鶴不是哺乳動物,火鶴沒有那麼殘暴,幼鶴也沒有那麼狠毒,他門仍然是按照一般禽鳥類動物餵養幼兒的方式,採取反芻出大量已消化的營養物來餵食幼鶴。火鶴消化作物類食物來吸收養份的方式,有點特別,牠們喜吃作物類的植物,消化成帶有大量 beta 胡蘿菠素火紅色的養份後,才吸收。反芻出來的營養物,等同於是禽鳥的作物奶,跟人類可用米漿育嬰是同樣性質的東西,不是血奶。
一般而言,一隻單親成鶴,就能養育幼鶴。這部影片則攝得了寶貴的鏡頭,由兩隻火鶴以線性組合的方式,這樣子餵養幼鶴,幼鶴長大了必定更為健康,就是因為他曾享受過順應天理之『線性組合』式的栽培。

2026年1月2日 星期五

數字轉換成數值

數字轉換成數值


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


數學計算是電腦的主要功能之一,數學計算程式則處理數值問題。數值在被處理成電腦可以執行運算的格式前,僅只是人們可以認得的數字,例如:口語上的一百二十三,寫成人們可以認得的阿拉伯數字時是123,它只是一個數字,一般電腦系統,通常均只將其識別成『字』而不是可以用來運算的『值』。因此,凡涉及數學計算的程式系統,都必須具備將數字轉換成數值的指令,反之,為了顯示運算結果,也必須具備將數值轉換成數字的指令,電腦才能接受人們設計的數學計算程式,執行程式內指定之數學運算,最後,以數字表示方式顯示結果。

近代FORTH,特別是依循ANSI標準設計出來的系統,常令使用者在設計將數字轉換成數值的程式時感到困擾,問題的起因,就在一個設計轉換程式時,常會用到的關鍵性指令:>NUMBER,其標準規格,似乎是被訂定成強調數學計算功能者所不願見到的格式了。

首先,它不處理帶有負號的輸入數字,數學計算怎麼可能不管負數?這個標準原生指令則確實不管,因此,設計數學計算系統者,只好自行在系統中,增加設計補其功能不足的程式。

另外,傳統非ANSI標準的FORTH系統,在處理數字輸入的方式上,通常就將帶有小數點格式的輸入數字,列入等同於>NUMBER之指令亦應處理的對象之一,這種功能,甚至於被設計成隱性的處理方式,讓系統自動記錄,輸入數字有無小數點?有,則在第幾位位數之處,記錄會被留在一個常被稱為DPL的變數內。

更先進的FORTH系統,則將處理數字輸入方面的整個問題,進行了更具彈性的安排,也就是硬將等同於>NUMBER的基本指令,規劃成執行向量式的指令格式,其執行內容,可隨設計者任意調整。

當初,在議定>NUMBER指令的ANSI規格時,可以想像,必定發生過非常混亂的紛爭,因為,事隔十年之後,還能見到許多公開譴責這個指令標準格式的言論。事實上,想實現包括了上述傳統數字轉換成數值所有功能的指令規格,確實不易訂定,因此,不納入規格,反而更能配合後序的千變萬化程式設計。使用者當然會感到麻煩,負數、小數點都得另行處理,整個FORTH系統運行起來之後,有更多的數字輸入識別處理工作要做,而>NUMBER標準指令,只能提供非常有限的數字轉換成數值之功能。有時,系統乾脆不用,僅在系統之INTERPRET內單獨設計隱性處理程式,然後,只按ANSI規格設計>NUMBER顯性指令供使用者使用。

執行>NUMBER指令前後的堆疊上參數之表示說明為:

( ud1 addr1 n1 – ud2 addr2 n2 )

其中,ud表正的雙整數,恆為正數,不管負值。addr1 n1則表待轉換數字之起始位址與字長量,addr2 n2則表轉換到不能轉換時的位址與剩餘字長量,小數點將被視為不能轉換的對象。

傳統FORTH中,等同於>NUMBER的指令,就被稱為NUMBER,其對應規格為:

( addr – d )

addr表待轉換數字之起始位址,d為轉換出來的數值,可正可負。轉換到不能轉換的標界,是空白字元,因此,可以接受數字內有小數點,指令會將位數存入DPL變數。

雖然,ANSI中的>NUMBER指令規格,不太令人滿意,並不表示使用者絕對無法實現數字轉換成數值的程式,只是表示想達到目的時,較為麻煩而已。單憑此一ANSI標準規格的>NUMBER指令,我們仍然能以高階定義的方式,定義出傳統之NUMBER指令來。

下列這個程式就能達到目的,但執行內容則不管小數點的問題。


    
   
: NUMBER ( addr -- d )
  0 0 ROT DUP 1+ C@ [CHAR] - = >R
  COUNT R@
  IF 1 /STRING		\ 去除負號佔用過1個字元量
  THEN 				\ 否則不必去除1個字元之量
  >NUMBER 			\ ( ud1 addr1 n1 – ud2 addr2 n2 )
  NIP 0= 			\ drop addr2, no matter n2 is = 0 or <> 0 consume n2
  IF	R>			\ keep ud2, check sign?
  	IF DNEGATE THEN 	\ ud2 ==> -d
  ELSE	R> DROP		\ drop positive sign, ud2 ==> +d
  THEN ;

應用於一個精簡的立即式中算符程式,可以檢驗此NUMBER指令的性能。

forth definitions
vocabulary ikq
ikq definitions

: nexti bl word number drop ;
: + nexti + ;
: - nexti - ;
: * nexti * ;
: / nexti / ;
: = . ;
forth definitions

\ usage:
\ ikq
\ 123 + 456 * 3 / 2 =

上述NUMBER指令的獲得來源,係J. L. Bezemer所著之”And so Forth”中第3.28章節起所使用的範例程式,網頁網址為:

http://thebeez.home.xs4all.nl/ForthPrimer/Forth_primer.html

原始程式列示如下:

S" MAX-N" ENVIRONMENT? \ query environment [IF] \ if successful NEGATE 1- CONSTANT (ERROR) \ create constant (ERROR) [THEN] : number ( a -- n ) 0. Rot dup 1+ c@ [char] - = >r count r@ if 1 /string then >number nip 0= if d>s r> if negate then else r> drop 2drop (error) then ; 將其寫成較佳的具有層次結構格式則為: : number ( a -- n ) 0. Rot dup 1+ c@ [char] - = >r count r@ if 1 /string then >number nip 0= if d>s r> if negate then else r> drop 2drop (error) then ;


J. L. Bezemer可能是德國人,他們喜歡自成一格的將number指令設計成轉換出單整數的規格,因此,堆疊上參數表示方式的格式為:( addr – n ),與傳統的( addr – d )不同,他們有堅持使用這種number的理由。一方面係此number指令中不處理帶有小數點的數字,另方面係未來系統將以64位元為主,一個雙整數單元能放置的數字,實際上相當大,約為38位數,系統中已不再需要強調使用雙整數,18位數的單整數已相當夠用了。

我所修改出來的程式,與原程式比較,只是去掉d>s,並將2drop改為drop,便可得到( addr -- d )的規格。至於原程式中使用到的(error)常數,可以不用。我在測試Sod64 Forth與MinForth兩套系統時,均遭遇到系統只提供>NUMBER新ANSI規格指令,但不提供傳統FORTH中之NUMBER式規格指令的困擾,後來,從SwiftForth系統使用說明中,讀到遇此困擾時的處理辦法,建議使用者,將數字轉移到PAD緩衝區去,再當一般文字來慢慢地仔細處理,感到如此使用FORTH的技巧,非常拙劣。J. L. Bezemer在教材書籍中列示範例程式,採用以>NUMBER來設計出傳統NUMBER的技巧比較高明。

將數字轉換成數值程式,最常使用於有對談式要求的場合,程式要能讓系統停下來,等待使用者輸入訊息,系統獲得輸入訊息後才繼續執行,而輸入的訊息可為文字或數字,但對系統而言,都是『字』而非『值』。

發展系統時,很需要精簡的對談式功能,以利單個指令或簡明程式的測試。我在發展數學計算系統時,更有這種需求,尤其是面對新到手的系統,必須進行簡單指令測試時更甚。測試時,又以輸入為數字者居多,因此,經常要為新系統添加這方面的功能性指令,我常將其定名為GetNumber。

以SwiftForth為例,說明資料中建議的設計方法為將輸入數字全送到PAD緩衝區去處理:

: input ( -- n )
PAD 5 BLANK PAD 5 ACCEPT >R
0. PAD R> >NUMBER 2DROP DROP ;

其中兩個相關指令的堆疊參數表示方式規格為:

>NUMBER ( ud1 addr1 n1 – ud2 addr2 n2 )

ACCEPT ( addr n1 – n2 )

SwiftForth系統中仍提供NUMBER指令,但其規格為( addr c – n ) 而非 ( addr – d ),與一般FORTH系統不同。而且,其WORD的指令規格,亦與別的系統不同。一般FORTH系統,執行過WORD指令後,堆疊上會自動得到HERE之值,但SwiftForth之WORD不留HERE之值在堆疊上。

在Win32Forth系統中處理數字輸入的典型設計則為:

: GetNumber ( -- n )
CR QUERY BL WORD NUMBER D>S ; \ DROP會棄除負號,故只能用D>S。
: GetFloat ( -- float )
CR QUERY BL WORD COUNT >FLOAT DROP ;

我曾在Win32Forth中設計一次可以輸入兩個數字的程式,記錄如下:

\ : QUERY TIB 80 ACCEPT #TIB ! 0 >IN ! ;
: Get2Numbers ( -- n n )
CR QUERY
BL WORD NUMBER D>S
BL WORD NUMBER D>S CR ;

F83的設計格式則為:

: GetNumber ( -- n )
CR QUERY
BL WORD COUNT NUBER CR ;

在Sod64 Forth系統中,則有NUMBER?指令可用,其規格為( c-addr – d f ),c-addr表打包規格的字串,首個位元組(Byte)要放字長數量,轉換出雙整數d,並留下旗號f。因此,測試程式為:

: Get2d ( -- d d )
BL WORD NUMBER? DROP
BL WORD NUMBER? DROP ;
: Test ( -- )
Get2d D+ D. ;

類似Sod64 Forth或MinForth的系統,均為只能在文字模式( text mode)下操作的系統,其QUERY藉KEY指令設計而成之後,不能以一般系統之使用方式實現對談式輸入要求,必須改以類似SwiftForth說明中的建議,全搬到PAD緩衝區去後再行處理。

MinForth為純ANSI系統,只能在純文字模式狀況下使用,下例可用:

: GetNumber ( -- n )
query 0.
bl word count over c@ 45 =
if -1 >r 1- swap 1+ swap else 0 >r then \ sign > r
>number 2drop r> if dnegate then d>s ;

MinForth中亦有convert指令,顯然也只能操作出ud,而不管負數。指令若不具有處理帶有小數點數字字串的功能,一旦數字出現小數點時,這種數字便只能被處理到該小數點為止的位置,因此,這個系統的數字輸入格式,只能為純整數。

負號因恆在數字前首,尚易於設計處理帶有負號之輸入數字的程式,若想再添加也能處理帶有小數點之輸入數字的功能,將其納入程式,則程式必定複雜。

啟用MinForth系統時,進行各種數學計算所需單一指令之測試,確實被對談式輸入數字的問題困擾許久。後來,思考到所有FORTH系統自身運轉起來之後,豈不是都能自行接受任何輸入數字嗎?使用者何不借力使力,就用系統已經存在的功能,完成處理輸入數字的應有工作,使用者何須還得另外設計程式?於是測試出叫用系統天王指令INTERPRET來協助完成需求功能的程式,如下:

\ Naughty code for the purpose of interactive input numbers.
: GetNumbers ( -- n/d/f ..... )
POSTPONE [ QUERY INTERPRET POSTPONE ] ;

將這個GetNumbers指令應用在對談式輸入數字的程式中時,輸入就可以混合著輸入整數、雙整數、浮點數,輸入數字的個數及秩序均無限制,都能被系統自動轉換完成後,依序放在堆疊上,以供後續指令使用。

GetNumbers指令的設計原理,運用了FORTH系統能夠運轉起來的根本機制,程式中見不到惱人的NUMBER指令,數字自動轉換的工作,是隱性的潛伏在INTERPRET指令之中了。

從Julian V. Noble所著的A Beginner’s Guide to Forth書中可以見到下列用來描述FORTH系統內所謂之外執譯程式( outer interpreter)執行時的封閉流程,這個流程就是FORTH天王指令INTERPRET功能程式的主要內容,其中,將數字轉換成數值,並將其放置在堆疊上的部份,就是我利用來設計GetNumbers的根據。

The diagram below is a flow chart representing the actions performed
by the Forth outer interpreter during interpretation.


A continuous loop waits for input—from the keyboard, a disk file or
whatever— and acts on it according to its nature. Input consists of a sequence of words and numbers. If a name is recognized it is executed; if it is not in the dictionary (where else would you keep a list of words?) Forth tries to convert it to a number and push it on the stack. If this is impossible, Forth aborts execution, issues an error message and waits for more input.

以上從網上節錄下來的資料,明確的說明了我利用INTERPRET來設計出GetNumbers指令的根本原理,資料出處的網址為:

http://galileo.phys.virginia.edu/classes/551.jvn.fall01/primer.htm

INTERPRET雖不是一個標準指令,但所有的FORTH系統內都有,善加利用,可以設計出許多非凡的指令來。此前,我曾利用它來設計過斷點式的除錯程式,其功能被附加在ABC FORTH系統的PAUSE指令中了。後來,我又發現了利用這個INTERPRET指令便能精簡輕易的從BASIC式程式格式中叫用FORTH指令的特異用法,結果就設計出了ABC FORTH中的RUN指令。這次再度利用INTERPRET設計出GetNumbers指令,算是第三度的精彩運用了INTERPRET。

典型的使用範例如下:


 
\ 整數的質因數分解
: factors ( n -- )
2
begin  2dup dup * >=
while  2dup /mod swap
       if   drop  1+ 1 or             \ next odd number
       else -rot nip  dup . ." * "
       then
repeat
drop . ;

: main ( -- )
cr ." Please enter an integer number: "
cr GetNumbers
cr dup . ." = " Factors
cr ;

執行結果:

main 
Please enter an integer number: 
987654328
987654328 = 2 * 2 * 2 * 123456791 
ok



我在設計ABC FORTH系統的早期階段,便設計出系統在處理實數的環境中,能夠主動將三種數字自動處理成浮點數。純整數、帶小數點數、浮點數三種數字,是使用者可能用到之所有的表示數字,若欲令系統能將三者均自動轉換成浮點數,則必須動手修改天王指令INTERPRET的執行內容,才易於實現要求。幸好,Win32Forth系統中,已將INTERPRET內所需要之相關於處理數字指令(NUMBER),規劃成一個可以改變執行內容的執行向量式指令了,我只須在完成設計後,以轉換向量內容方式,切換指令指向之位址便可。

上述GetNumbers指令雖頗具使用彈性,但得注意其可用規格,它只適合在對談式應用場合,於系統停頓下來後,純粹等待接受使用者只能輸入數字時使用,不適合處理文字,或文字以外另有後續指令的場合。例如:上述以ikq字彙讓系統能執行中算符的應用簡例中,因使用者除了輸入數字外,還可能也要連續輸入指令,這樣的應用,便不適合採用GetNumbers。

相關於將數字轉換成數值的NUMBER指令,在可自動反編譯的FORTH系統中,或較有用的FORTH書籍中,均可找到此一指令的源程式,因ANSI的新規格性能欠佳,使用者便可能常需要自行設計將其改善的新功能程式。因此,所有可獲得之有關NUMBER指令的源程式,便成為可貴的資源,特將屬於書籍方面的資料來源彙整於此,留供日後參考,新資料亦可添加於此。

 

(1). Dr. C. H. Ting, “eForth and Zen”, Offete Enterprises, Inc. 1993, p.64e. NUMBER? ( a – n T, a F ) (2). Dr. C. H. Ting, “Technical reference manual”, Offete Enterprises, Inc. 1989, p.83 (NUMBER?) ( a – d f ) (3). Dr. C. H. Ting, “System guide to Fig-Forth”, Offete Enterprises, Inc. 1981, p.88 NUMBER ( a – d ) (4). Leo Brodie, “Starting FORTH”, FORTH, Inc. 1981, p.279 NUMBER ( a – n/d ) (5). Glen B. Haydon, “All About FORTH”, Haydon Enterprises, 1982, p.65 ( a – d ) (6). Mitch Derick & Linda Baker, “FORTH Encyclopedia”, Forth Interest Group, 1982, p.240 NUMBER ( a – d ) (7). Leo J. Scanlon, “Forth programming”, Howard W. Sams & Co., Inc. 1982, p.180 NUMBER ( a – d ) (8). Martin Tracy, Anita Anderson, Advanced MicroMotion, Inc, “Mastering FORTH”, 1989, Brady books, a division of Simon & Schuster, Inc. p.132 VAL ( a n – d f | 0 ) (9). Mahlon G. Kelly, Nicholas Spies, “FORTH: a text and reference”, Prentice-Hall Englewood Cliffs, N.J. 07632, 1986, p.195 NUMBER ( a – d ) NUMBER ( a – d or n )