The force: Forth 语言

The force: Forth 语言

  • MD Document: 2/24/2016 8:49:24 AM by Jimbowhy
  • CSDN发布:

这是一个可以靠美色搞钱的时代,这是一个可以靠权力搞钱的时代,可是我不乎,因为我都没有,还因为我知道这一切都不保鲜,TA们当然也更清楚,所以要趁早卖! - by Jimbowhy 3/3/2016 9:38:49 AM

本文是《VirtualNES虚拟机》关于 Forth 语言部分的延伸,是对原文的扩展。

计算机语言应该是易理解,因为它是人机交流的管道,如果人理解语言都成问题,那么还怎么能期待计算机很好地执行我们交待的程序呢?传统高组语言如C,特别是C++隐藏了太多细枝末节的东西,所以异常问题常常登门造访,让人防不胜防。

Forth 语言对我来说是一个全新的概念,初次接触它时,很容易就打破了我以往建立的计算机编程语言的观念。每一种计算机语言基本上都会有一个基本模型,即把程序运行的环境当作什么。传统的C语言看到的是 CPU 与内存的连接,还有在内存建立的堆栈结构。而 Forth 把 CPU 当作是 多个堆栈和内存之间的连接通道。

+------------+    +-------------+    +--------------+       +--------------+ 
| data-stack |    | float-point |    | return stack |       |    memory    | 
+------+-----+    +------+------+    +------+-------+       +--------------+ 
       |                 |                  |               | stack-layout | 
       |                 |                  |               +--------------+ 
       |          +------+------+           |               |     ....     | 
       |          |             |           |               +--------------+ 
       +----------+    CPU      +-----------+               | stack-layout | 
                  |             |                           +-------+------+ 
                  +------+------+                                   |        
     Forth               |                      C           +-------+------+ 
 Virtual Machines        |                Virtual Machines  |              | 
                  +------+------+                           |     CPU      | 
                  |    memory   |                           |              | 
                  +-------------+                           +--------------+ 

然而开发属于自己计算机语言编译器确是件非常有趣的事,试想,将整个电脑系统当作一个经济体,那么操作系统就像是政府机构,编译工具则像是重工业,而应用软件则像是轻工业或服务业。David A. Wheeler 在他的一篇文章中陈述了自己实现一个 6502 平台的语言中的经过及经验 A-Lang: WAYS TO IMPLEMENT COMPUTER LANGUAGES ON 6502s。在他的另一篇文章中提到许多 6502 Language Implementation Approaches 语言实现,提到6502平台的两个Java虚拟机实现 VM02 和 NanoVM。还提到一本特别适合编译入门的教材《Crafting a Compiler》,已经添加目录好书签上传方便阅读。 其中特别介绍了 Forth 语言,特别强调它编译的程序运行快,Thinking Forth 项目提供了一本教材,可以下载彩色版的PDF文件。Brad Rodriguez 的 Moving Forth 讲述如何移植 Forth 到其它硬件平台,阅读时会提到“穿线”技术 the Threading Technique,请不要和线程混淆,Forth 是一种全新视角的计算机语言。

Forth 语言是 Charles Moore 在 20 世纪 60 年代发明的基于堆栈、交互式、具有简单性哲学思想的计算机编程语言和环境。实践中证明它特别适合千行代码数量级的嵌入式系统,能有效降低开发成本和增强系统可靠性,被广泛应用于各行业领域,国际天文学会于 1976 年接受了 Forth 作为标准语言。由于 Forth 的高可用性,CPU厂商也开始为 Forth 体系结构设计优化的堆栈计算机处理器芯片。

Forth 是一个交互式的程序设计环境,最初是为程序员在小型和微型计算机上开发应用程序而设计的,主要优点是软件开发快速、交互式、计算机硬件的高效使用等。与传统的高级语言工具不同,Forth 把编译器、编辑器、汇编器等等融为一体,这是优化的设计。这也使得它的内部结构对于新用户来说很特殊,但是 Forth 的简单性、高度模块化和交互式特性可以弥补初学者的陌生感,使得 Forth 非常易于学习和使用。在 Startgin Forth 中,是这样描述 Forth 的:

a high-level language
an assembly language
an operating system
a set of development tools
a software design philosophy

为了在PC机上一探 Forth 真容,可以使用 Tom Zimmer 的 Win32Forth,它可以在Win32平台上运行。在这 A Beginner’s Guide to Forth by J.V. Noble 下载 w32for42.exe。很不巧的是 Win32Forth 4.2 这个安装包程序是16位的,不能在Win7 x64平台运行,只能通过DOSBOX运行Win3.1来解包了。在本文发布时,Win32Forth 6.15 是最新版本,可以到 sourceforge 下载。使用 SwiftForth 也是一个不错的选择,还附带 Application Techniques 和 Programmer’s Handbook 电子书,评估版只是不含原代码。

The force: Forth 语言_第1张图片

Forth也被戏称为“只写”语言,因为它的语法风格非常怪异,常常代码写完后边作者本人都读不懂。其中一个原因是 Forth 将许多高级语言的功能分解了,以更精细的进行控制,所以在写 Forth 程序时,需要很好的语言组织能力,不仅写代码同时还要将自我解释的信息写进去,这才是一个优秀的程序人。

对于新手来说《Starting Forth by by Leo Brodie》 是非常棒的入门教材,作者也很用心在写作,对于那些稍为难懂的机器运行过程都以漫画的形式化简表达,Starting Forth 离线版已经制作打包,修改了眼睛保护背景色,可以在此下载。作者在书中还用了一种我之前从未见过的方法来解析负数的补码是什么,设想一个展馆,每天到访客流都有个限额,假设最大的额度就是255,刚好是一个字节可以存储的最大值。那么每有一个访客额度就会减一,即表示负一:

 1111 1111 - 1 = 1111 1110

实际上使用的补码是2’补码,即在1’补码的基础上加一,也就是取反加一的补码形式,这样计算机可以用加法器就可以进行减法运算。

The force: Forth 语言_第2张图片
来自17世纪 G.W.Leibniz 莱布尼茨二进制算法手稿

史蒂芬·沃尔夫勒姆(Stephen Wolfram)即数学软件 Mathematica 和知识型计算引擎 Wolfram Alpha 的开发者。他的博客文章《探访莱布尼茨:与大师穿越时空的碰撞》(Dropping In on Gottfried Leibniz)引用了许多原始的手稿图片,上面这张图就是其中之一。莱布尼茨之后的数个世纪里,几乎没人用2进制做出些许成果,直到电子数字计算机的兴起。

掀起她盖头

Forth语言一个特点就是用词汇来描述程序,所以执行程序就可以理解为给计算机下达具有指令功能的单词。解释器接收到输入时,会进行检测,看输入内容是不是一个定义的词,是不是一个变量,是不是常量等等,如果是词汇定义,那么就执行它。为了更直观题解这一过程,可以先来了解秒号 ’ 即双引号按键上的那个符号,它就是用来查询词汇定义的内存地址的,知道词汇地址后,就可以通过执行指令来运行词汇程序,如下执行一个称为 GREET 的词,如果它定义的话:

' GREET EXECUTE

Forth系统的词汇其实是一个链表结构,每一个词汇定义都记录了可以执行代码的地址,和前后两个相邻的词汇,解释器通过线性查找来定位词汇代码在内存的地址,因此一个单词定义的典型结构包含如下内容:

name field
link field
code pointer field
data field

link field 就是存储相邻单词定义的数据单元,Code Pointer 则包含了单词定义的属性,是变量定义还是常量定义,还是可以执行词,都是这一块内容记录的。在解释器接收输入和完成执行指令的过程可以用以下的过程表达,:

: QUIT  BEGIN  (clear return stack)
               (accept input)
               INTERPRET
               ." ok " CR
               AGAIN ;

这是一个典型的单词定义格式,INTERPRET 指示了解释器所在的位置。 BEGIN … AGAIN 构成了一个循环体,在循环体内不断地对返回堆栈进行清理,然后接收输入,然后将输入交给解释器进行处理,QUIT 这个单词则是 Forth 系统执行的。当前的位置可以用 HERE 这个词来取得,其它和解释器相关的单词功能如下:

' xxx   ( -- addr ) Attempts to find the execution token of xxx in the dictionary.   
['] compile time:( -- ) run time:( -- addr )   
        Compiles the execution token of the next word in the definition as a literal.
EXECUTE ( xt -- )   Executes the dictionary entry whose execution token is on the stack.   
EXIT       ( -- )   When compiled within a colon definition, terminates execution at that point.   
QUIT       ( -- )   Clears all stacks and returns control to the terminal. No message is given.   
HERE       ( -- addr )   Returns the next available dictionary location.   
PAD        ( -- addr )   Returns the beginning address of a scratchpad area.   
SCR        ( -- addr )   User variable. A pointer to the current block number (set by LIST). 
BASE       ( -- addr )   User variable. Number conversion base.   
SP@        ( -- addr )   Return the address of the top of the stack before SP@ is executed. 
TIB   ( -- addr )   User variable. Contains the address of the start of the terminal input buffer.  
#TIB  ( -- addr )   User variable. Contains the size of the terminal input buffer.
SP0   ( -- addr )   User variable. Contains the address of the bottom of the parameter stack.
>IN   ( -- addr )   User variable. A pointer to the current position in the input stream. 
BLK   ( -- addr )   User variable. If non-zero, a pointer to the block being interpreted by LOAD. 

Forth 同时又是一个更接近汇编语言的工具,可以通过 see 来查看单词对应的汇编代码:

see execute 
EXECUTE IS CODE 
  ( $4010AC 8BC3 )            mov     eax, ebx 
  ( $4010AE 5B )              pop     ebx 
  ( $4010AF FF20 )            jmp     [eax] 
  ( $4010B1 0000 )            add     [eax], al 
  ( $4010B3 00B81040008B )    add     $8B004010 ( >WIN32FORTHIDE+$8AA7A0B8 ) [eax] bh 
  ( $4010B9 035BFF )          add     ebx, $FF ( $400000+$FFC000FB ) [ebx] 
  ( $4010BC 2000 )            and     [eax], al 
  ( $4010BE 0000 )            add     [eax], al 
  ( $4010C0 C410 )            les     dl, [eax] 
  ( $4010C2 40 )              inc     eax 
  ( $4010C3 008B0683C604 )    add     $4C68306 ( >WIN32FORTHIDE+$46DE3AE ) [ebx] cl 
  ( $4010C9 FF20 )            jmp     [eax] 

在Win32Forth上还可以通过 CODE 来直接编写汇编语言:

CODE *+             ( a b c -- b*c+a) \ stack: before -- after
  mov ecx, edx    \ protect edx because mul alters it
  pop eax         \ get item b; item c (TOS) is already in ebx
  mul ebx         \ integer multiply-- c*b -> eax (accumulator)
  pop ebx         \ get item a
  add ebx, eax    \ add c*b to a -- result in ebx (TOS) --done
  mov edx, ecx    \ restore edx
  next,           \ terminating code for Forth interpreter
END-CODE  ok

基本语法

Forth 是编程思想是目标导向,告诉它做什么比告诉它怎么做更便捷。比如说做一个洗衣机的嵌入系统程序,直接定义洗衣功能,并进行功能分解,逐步实现这些功能需要的子项目,程序编写的格式如下,提示三种注解的写法:

/ comments
( and comments )
: WASHER  WASH SPIN RINSE SPIN ; Another comments
: RINSE  FAUCETS OPEN  TILL-FULL  FAUCETS CLOSE ;

冒号表示一个过程定义的开始,后面跟着一个名称,它在 Forth 系统中称为字 Word,之所以使用字这个称谓,是因为 Forth 内部以字典的形式来组织它们。名称后面可以跟接数个子过程,如RINSE,它的定义就在第二行,而分号表示定义结束。当一个 Word 定义好后,可以通过 Forth 的界面呼叫它,这样就可以当程序一样执行,这就是程序的交互功能。完成定义后,Forth 将定义编译保存在内部,以一个称为字典 Directory 的形式组织这些已定义的字,Vocabularies 词汇空间则和C++的命名空间一样用来管理字,避免冲突。每个系统都有一组预定义的 Word,这就像C语言的运行库一样,你可以调用它,比如说输入 page、cls 可以清屏, print 可以执行打印的功能,执行下面的命令就可以打印 Win32Forth 的版本号:

print .version [ENTER]

为了查询系统定义的字,可以使用 Class-browser 字打开词汇树,也可以通过词汇字查询方式,如下,words 表示查询,forth 表示系统的词汇空间,结果摘要显示系统定义了5212个字,真丰盛:

forth words ; forth constants or forth f
...
Displayed 2350 of the 5212 words in the system.
** Use: WORDS  to limit the list **
** Use: CONSTANTS  to display Windows Constants **
See also .loaded .deferred .file .fonts .free and more ok

例如查询到一个新字 “web-link,可以通过帮助系统 help-system 来学习这个字。

help-system 
help "web-link

Forth 编程风格和数学软件 Mathmatica 与非常相似。前面已经点明 Forth 是编程思想是告诉它做什么比告诉它怎么做更便捷,以 3+4=7 这条算术为列,传统的语言中执行过程大概这样:

1. 读取数值 3;
2. 读取算法 +;
3. 读取数值 4;
4. 计算两个数的和。

这是典型的告诉它怎么做的思维,而在 Forth 中,可以以更简捷,只要告诉它做什么:

3 4 + .↵[ENTER] =7
3 9 +  4 6 +  * .↵[ENTER] =120
17 20 132 3 9 + + + + .↵[ENTER] =181
1 10 SWAP . .↵[ENTER] (1 10)
3 DUP * .↵[ENTER] =9

Forth 在接收到输入时,通过解释器检测到数值,就会通过堆栈来存放这些值,并进行算术计算,点号就是从堆栈中取出结果并显示,也可以显示无符号数 U.。堆栈是CPU内部硬件定义的数据结构,它的入栈出栈操作是非常有执行效率的,只需要一条指令 push 或 pop 就可以完成。统语言的函数调用过程需要一系列指令来完成堆栈帖 Stack Layer 的管理,而 Forth 由于节省了传统语言的函数调用过程,CPU指令执行流程大大优化,所以 Forth 程序速度上有明显的提升。

在开始学习 Forth 的各种字之前,有必要来了解一下注解中可能出现的各种操作数的意义:

n1,n2..   signed numbers (integers)
d1,d2..   double precision signed numbers
u1,u2..   unsigned numbers (integers)
ud1,ud2.. double precision unsigned numbers
addr1..   address
b1,b2..   bytes
c1,c2..   ASCII characters
char1,... ASCII characters
t/f,t,f   boolean flag, true, false 0=false, nz=true,
flag      0=false, -1 =true

可以用以下几个字来查询其它字的定义:

DIS 
SEE 
VIEW 

以下是一些内置的堆栈操作定义。

: xxxx yyy ;    ( -- )      Definition with the name xxx, consisting of word or words yyy. 
CR              ( -- )      Performs a carriage return and line feed at your terminal. 
SPACES        ( n -- )      Prints the given number of blank spaces at your terminal. 
SPACE           ( -- )      Prints one blank space at your terminal. 
EMIT          ( c -- )      Transmits a character to the output device. 

ABS           ( n -- |n| )      Returns the absolute value. 
NEGATE        ( n -- -n )       Changes the sign. 
MIN       ( n1 n2 -- n-min )    Returns the minimum. 
MAX       ( n1 n2 -- n-max )    Returns the maximum.
AND       ( x1 x2 -- x3 )       the bit-wise logical and of x1 with x2.
OR        ( x1 x2 -- x3 )       the bit-wise inclusive or of x1 with x2.
XOR       ( x1 x2 -- x3 )       the bit-wise exclusive or of x1 with x2.
INVERT       ; n1 -- n2         Returns bitwise inverse of n1.
LSHIFT     ; x1 u -- x2         Logically shift x1 by u bits left.
RSHIFT     ; x1 u -- x2         Logically shift x1 by u bits right.

HEX             ( -- )          Sets the base to sixteen.
OCTAL           ( -- )          Sets the base to eight (available on some systems).
DECIMAL         ( -- )          Returns the base to ten.
n BASE !        ( -- )          Sets the Base to n. 

括号部分为注解,解释堆栈的状态,( before – after ),– 表示当前位置,n1 n2 这些数字序号和堆栈的数字的序号无关,只和它们的位置相关, – 左边的数字就是栈顶的所在,在执行后,括号结束前的那个数就是新的栈顶位置。

通过设置 BASE,可以很方便地查询负数补码的实际数据值:

2 BASE !  ok
-1  ok.
U. 11111111111111111111111111111111  ok
2 -1 *  ok.
U. 11111111111111111111111111111110  ok
3 -1 *  ok.
U. 11111111111111111111111111111101  ok

以下堆栈编程例子,定义了各种罪的刑期,CONVICTED-OF 用来下达审判结果,注意 – 表示的堆栈状态还有算术计算过程:

( problem 2-6 )
\ Code from Starting Forth Chapter 2
\ ANSized by Benjamin Hoyt in 1997

: CONVICTED-OF  ( -- no-sentence )  0 ;
: ARSON         ( sentence -- sentence+10 )  10 + ;
: HOMICIDE      ( sentence -- sentence+20 )  20 + ;
: BOOKMAKING    ( sentence -- sentence+2 )  2 + ;
: TAX-EVASION   ( sentence -- sentence+5 )  5 + ;
: WILL-SERVE    ( sentence -- )  . ." years" ;

上面的程序中使用了字符串的输出语法 .” years “,现在就可以执行审判程序:

CONVICTED-OF ARSON TAX-EVASION↵[ENTER] =15 years

字符与字符串

Forth 与字符字符串相关的字不多,有以下这些:

." xxx"     ( -- )        Prints the character string xxx use inside definition. 
.( xxx)     ( -- )        Prints text xxx right away use outside definition.
CHAR C      ( -- ascii )  Put ascii value of C into stack.
[CHAR] C    ( -- ascii )  Put ascii value of C into stack use inside definition.
EMIT      ( C -- )        Type character C on terminal screen.

S" xxx"         ( -- addr len ) Define string content.
C" xxx"         ( -- addr )     Define string content.

ACCEPT ( addr len -- len )      Input string and put it in buffer addr.
CREATE BUFFER$ 80 ALLOT ( -- )  Define a buffer word name BUFFER$ and allocate 80 bytes.
COUNT      ( addr -- addr len ) Count the string and leave len to the top of stack.
TYPE   ( addr len -- )          Type string on terminal screen.

数值:单长/双长与符号数

Forth 处理数值时,默认的数据长度为单长 sigle-length,它所占用的内存就称为一个单元 cell。一般 Forth 中数值和 + , - . / : 这些符号连用时,如 1+ 1- 2* 2/ */ 等,会转换为双长数值 double-length,它则要占用两个单元,在使用的 Win32Forth 6.15中它是32-bit数值。可以使用 D. 来显示,双长数值的运算符也有D前缀。在交互输入会通过点号提示,下面提示在 ok 后面的点就表示了堆栈中数值占用的格数:

10  ok.
10.  ok...
D. 10  ok.
. 10  ok

相关的操作字如下,注解中 d 这样的字符表示 double-cell,n 则表示 single-cell,u 表示 unsinged,f 表示 flag 用于条件判断:

+         ( n1 n2 -- sum )      Adds.
-         ( n1 n2 -- diff )     Subtracts (n1-n2).
*         ( n1 n2 -- prod )     Multiplies.
/         ( n1 n2 -- quot )     Divides (n1/n2).
/MOD      ( n1 n2 -- rem quot ) Divides. Returns the remainder and quotient.
MOD       ( n1 n2 -- rem )      Returns the remainder from division.

D.            ( d -- )          Prints the signed double-length number, followed by a space.
D.R     ( d width -- )          Prints like D. and right-justified within the field width.
D+        ( d1 d2 -- d-sum )    Adds two double-length numbers. 
D-        ( d1 d2 -- d-diff)    Subtracts two double-length numbers (d1-d2). 
DNEGATE       ( d -- -d)        Changes the sign of a double-length number. 
DMAX      ( d1 d2 -- d-max )    Returns the maximum of two double-length numbers (d1-d2). 
DMIN      ( d1 d2 -- d-min )    Returns the minimum of two double-length numbers (d1-d2).
D=        ( d1 d2 -- f )        Returns true if d1 and d2 are equal. 
D0=           ( d -- f )        Returns true if d is zero. 
D<        ( d1 d2 -- f )        Returns true if d1 is less than d2. 
DU<     ( ud1 ud2 -- f )        Returns true if ud1 is less than ud2. Both numbers are unsigned.

U.            ( u -- )          Prints the unsigned single-length number, followed by a space.
U.R     ( u width -- )          Prints like U. and right-justified within the field width.
UM*       ( u1 u2 -- ud )       Multiplies two single-length numbers. Returns a double-length result. 
UM/MOD    ( ud u1 -- u2 u3 )    Divides a double-length by a single-length number.
                                Returns a single-length quotient u2 and remainder u3. 
U<        ( u1 u2 -- f )        Leaves true if u1 < u2, where both are treated as single-length unsigned integers.

M+          ( d n -- d-sum )    double- & single-length number adding.
SM/REM     ( d n1 -- n2 n3 )    double- divide by single-number, d1/n1, 
            giving the symmetric quotient n3 and the remainder n2. Input and output are signed.
FM/MOD     ( d n1 -- n2 n3 )    double- divide by single-number, d1/n1, 
            giving the floored quotient n3 and the remainder n2. Input and output are signed.
M*        ( n1 n2 -- d-prod )   Multiplies, both n1 and n2 are single-length numbers. 
                                Returns a double-length result. All values are signed. 
M*/    ( d +n1 n2 -- d-result ) Multiplies a double-length number by a single-length number and 
                                divides the triple-length result by a single-length number (d*n/n). 
                                Returns a double-length result. All values are signed. 

数值格式化

格式化操作通过 <# 来输入双长数据,然后结合 # #S HOLD 等等来规格化内容,如果需要使用数值的符号就用 SIGN,最后用 #> 结束格式化并使用 TYPE 来输出结果,如一个转换秒数的时间格式化程序:

: SEXTAL  6 BASE ! ;
: :SS  # SEXTAL # DECIMAL [CHAR] S HOLD ;
: :MM  # SEXTAL # DECIMAL [CHAR] M HOLD ;
: SEC  <# :SS :MM #S [CHAR] H HOLD ROT SIGN #>  TYPE SPACE ;

600. SEC H0M10S00  ok

分值和秒值的十位是6进的,所以定义了一个 SEXTAL 字来修改基数变量 BASE,语法参考后面变量部分。注意,<# 使用的是双长无符号数,所以 600 后面跟了一个点将其扩展为双长数值。对于有符号单长数值,可以先复制一分出来,求其绝对值,在原始数值中保留符号标记。而无符号单长数值则直接在后面再补一个零,就可以组成一个新的无符号双长数值,下面的运行结果和前面是一样的:

-600 DUP ABS 0 SEC
600 0 SEC

而 HOLD 这个字用来将字符 S/M/H 的值保存到堆栈,等待 TYPE 输出,最后一个 SPACE 用来在交互界面的提示信息之间插入一个空格。

<#       Begins the number conversion process. Expects the unsigned double-length number. 
#        Converts one digit and puts it into an output character string. 
#S       Converts the number until the result is zero. Always produces at least one digit. 
c        HOLD Inserts a ASCII value of a character c. HOLD must be used between <# and #>.
SIGN     Inserts a minus sign for negative. Usually used with ROT immediately before #>.
#>       Completes number conversion by leaving the character count and address on the stack.
TUCK    ( d --  )
TYPE     ( addr u -- ) Print text content amount u at addr
<# ... #> ( ud -- addr u ) double-length unsigned
<# ... ROT SIGN #> ( n |d| -- addr u ) double-length signed

系统内置的字 D. 可以简化表达为:

: D.  TUCK DABS <#  #S ROT SIGN  #>  TYPE SPACE ;

TUCK的作用是拷贝栈顶数据到第二个单元,即将数据中含符号位的高半部分拷贝一份,以供后面的 ROT 调用,可以参考后面堆栈操作部分。ROT SIGN 配合使用时,如果堆栈第三个值是个负数时,就在输出字符串中添加负号标记。

常量、变量、数组与内存分配

定义常量的语法如下,对于双长数值 则用 2CONSTANT 代替:

220 CONSTANT AC_INPUT
AC_INPUT (Get value directtly)

用过 Forth 的变量功能后,再和C语言的变量相比,发现C语言的变量真LOW,和指针什么的一混杂,就是一锅粥!看看 Forth 的变量相关的语法,week 为变量名:

VARIABLE week   (Define variable week)
7 week !        (Asign value)
1 week +!       (Add value)
week .      (Print value)
week ?          (Print value way two)

变量的初始化语法中使用了 VALUE 关键字,更改值除了上面的赋值表达式外,还可以用 TO 关键字:

VARIABLE voltage
110 VALUE voltage
220 TO voltage

相关的操作符号功能,前缀2表示双长数值的功能:

VARIABLE xxx ( -- )     Creates a variable named xxx; the word xxx returns its address when executed.
!     ( n addr -- )     Stores a single-length number into the address. 
@       ( addr -- n )   Replaces the address with its contents.
?       ( addr -- )     Prints the contents of the address, followed by one space. 
+!    ( n addr -- )     Adds a single-length number to the contents of the address. 

2VARIABLE xxx( -- )     Creates a double-length variable named xxx; 
2!    ( d addr -- )     Stores a double-length number into the address.
2@      ( addr -- d )   Returns the double-length contents of the address.

定义变量时,Forth 会在字典中生成对应的字,和变量的功能代码,变量可以按内容类型而适应。定义变量时,Forth 在字典中生成对应的字,并分配一个单元 cell 给变量来存储数据。数组的定义就是在变量的基础上,再添加更多的 cell 以保存更多的数据,这就是 Forth 数组,如下面 LIMITS 就是一个有四个单元的数组:

VARIABLE limits 4 CELLS ALLOT

注意这里的 4 CELLS ALLOT 是额外分配的,算上定义变量时分配的一个单元,合计5个单元空间。

220 limits              ; set limits[0]=220
480 1 CELLS LIMITS + !  ; set limits[1]=480  
1 CELLS limits + ?      ; query limits[1]
limits ?                ; query limits[0]
limits @ .              ; (Print value)

还可以使用 CREATE 来直接创建数组,ERASE 用来清零:

CREATE data 100 ALLOT
data 100 ERASE
CREATE data 100 CHARS ALLOT
CREATE cell-data 100 CELLS ALLOT

在 Forth 系统中,逗号也是一个字,它将栈顶的数据保存到数组的单元格上,下面定义了 10 的0次方到4次方的数值,TENS为这几个数的初始地址,在32位系统上,每个数相隔4个字节:

CREATE TENS 1 , 10 , 100 , 1000 , 10000 ,
TENS 8 + @ . (read the value 10^2)

在使用字节数据时可以结合 C 来定义数组,通常还会使用 ALIGN 来进行内存对齐:

CREATE TEST 123 C, ALIGN 1234 ,
TEST CELL+ @     ( properly return 1234 )

内存的操作有以下功能字:

@       ( addr -- n )   Fetch and return the cell at memory address addr.
!     ( n addr -- )     Store the cell quantity n at memory address addr.
+!    ( n addr -- )     Add n to the cell at memory address addr.
c@      ( addr -- char) Fetch and zero extend the character at memory address addr.
c! ( char addr -- )     Store the character char at memory address addr.
CELLS     ( n1 -- n2 )  Returns n2, the memory size required to hold n1 cells. 
CHARS     ( n1 -- n2 )  Returns n2, the memory size required to hold n1 characters.
ALIGN        ( -- )     reserve enough space to align data pointer.
ALIGNED ( addr -- a-addr ) Return a-addr, the first aligned address.
ALLOT      ( u -- )     Allocate u bytes of data space
BUFFER:  ( n -- ) Create a dictionary entry with n bytes of data space.
C,       ( char -- )    Reserve one byte of data space and store char in the byte. 
CELL+ ( a-addr1 -- a-addr2 ) Add the size in bytes of a cell to a-addr1, giving a-addr2.
CELLS      ( n1 -- n2 ) Return n2, the size in bytes of n1 cells.
CHAR+ ( c-addr1 -- c-addr2 ) Add the size of a character to c-addr1, giving c-addr2.
CHARS      ( n1 -- n2 ) Return n2, the size in bytes of n1 characters.
CREATE  ( -- )    Create a dictionary entry
DUMP   ( addr u -- )    Displays u bytes of memory, starting at the address.
ERASE  ( addr n -- )    Stores zeroes into n bytes of memory, beginning at the address.
FILL ( addr n b -- )    Fills n bytes of memory, beginning at the address, with value b.

w@      \ addr -- val   Fetch and zero extend the 16 bit item at memory address addr. 
w!  \ val addr --       Store the 16 bit item val at memory address addr.

堆栈操作

前面提到的堆栈操作在 Forth 系统编程中是一个重要角色,在 Forth 系统中使用了不止一个堆栈,而是好几个。有返回堆栈 Return Stack、浮点堆栈 Floating Point Stack、参数堆栈 Parameter Stack,上面使用的就是参数堆栈,在意思明确的情况下称之为堆栈。参数堆栈可以用来在字执行过程中传递参数,不像C语言那样传递参数要通过形参实参形式,Forth 中传递数就是通过堆栈实现的,当字执行完如果有返回的值,就会留堆栈实现参数的返回。以下是几个堆栈间的操作字:

>R  ( n -- )     Takes a value off the parameter stack and pushes it onto the return stack. 
R>    ( -- n )   Takes a value off the return stack and pushes it onto the parameter stack. 
I     ( -- n )   Copies the top of the return stack without affecting it. 
R@    ( -- n )   Copies the top of the return stack without affecting it.
J     ( -- n )   Copies the third item of the return stack without affecting it.
2>R ( x1 x2 -- ) Push the current top cell pair from the data stack onto the return stack.
2R> ( -- x1 x2 ) Pop the top cell pair from the return stack and place on the data stack.
2R@ ( -- x1 x2 ) Copy the top cell pair of the return stack and place on the data stack. 
DEPTH ( -- n )   Leave a number of stack deepth.

TUCK        ( a b -- b a b)     SWAP OVER
ROT    ( n1 n2 n3 -- n2 n3 n1 ) Rotates the third item to the top.
SWAP      ( n1 n2 -- n2 n1 )    Reverses the top two stack items.
DUP           ( n -- n n )      Duplicates the top stack item.
OVER      ( n1 n2 -- n1 n2 n1 ) Makes a copy of the second item and pushes it on top.
DROP          ( n -- )          Discards the top stack item.
2SWAP     ( d1 d2 -- d2 d1 )    Reverses the top two pairs of numbers.
2DUP          ( d -- d d )      Duplicates the top pair of numbers.
2OVER ( d1-d4 --  d1-d4 d1 d2 ) Makes a copy of and pushes the second pair of numbers on top.
2DROP         ( d -- )          Discards the top pair of numbers.
ROLL  ( xu .. x0 u -- .. x0 xu) Pick up and move u-th item and put it to the top of statck.
PICK  ( xu .. x0 u -- xu .. x0 xu) Pick up u-th item in stack and put it to the top of stack.
                0 PICK is equivalent to DUP, 1 PICK to OVER and so on.

为了理解堆栈间的操作意义,下面用一个三个数字的排序程序来做演示:

( 2 3 1 -- 3 2 1 )

>R SWAP R>

初始状态下,参考堆栈的三个值是 2、3、1,排序后变为 3、2、1,程序中 >R 将栈顶的值 1 移动到返回堆栈中,余下两个数做位置交换,最后 R> 将 1 从返回堆栈中取回来完成排序。 又以二次方程 ax2 + bx + c 计算为例:

: QUADRATIC  ( a b c x -- n )
>R SWAP ROT R@ *  + R> *  + ;

2 7 9 3 QUADRATIC .↵48 ok 

上面的程序已经开始有点“只写语言”的味道了,不是吗!符号:到;之间定义了一个字 QUADRATIC,使用它来计算一元二次方程时,依次输入a b c x 等参数。从执行指令上来讲,这是非常简洁的,没有多余的指令来消耗系统硬件资源,这就是 Forth 的追求目标。可以将程序执行的流程列表:

Operator    parameter stack   return stack 
            a b c x
>R          a b c             x
SWAP ROT    c b a             x
R@          c b a x           x
*           c b ax            x
+           c ax+b            x
R> *        c x(ax+b)          
+             x(ax+b)+c

定义字的时候也可以使用堆栈来保存参数,在定义结束之前就需要确保堆栈平衡,不要留下额外的数据,这会占用内存导致内存泄漏。深度的堆栈操作有 PICK 和 ROLL,下面这个表以另一种方式来对应一些堆栈操作字的功能差异:

cell| initial | DROP | SWAP |  ROT |  DUP | OVER | TUCK | 3 PICK | 3 ROLL |
0   |     -16 |   73 |   73 |    5 |  -16 |  -16 |   73 |      2 |     2  |
1   |      73 |    5 |  -16 |  -16 |  -16 |   73 |  -16 |    -16 |   -16  |
2   |       5 |    2 |    5 |   73 |   73 |    5 |   73 |     73 |    73  |
3   |       2 |      |    2 |    2 |    5 |    2 |    5 |      5 |     5  |
4   |         |      |      |      |    2 |    2 |    2 |      2 |     2  |

逻辑运算与流程控制

IF-THEN 条件判断,基本格式 condition IF xxx ELSE yyy THEN zzz,其中 ELSE yyy 部分是可选的,条件为真是执行 xxx 部分。

= ( n1 n2 -- f ) Returns true if n1 and n2 are equal. 
- ( n1 n2 -- n-diff ) Returns true if n1 and n2 are not equal. 
< ( n1 n2 -- f ) Returns true if n1 is less than n2. 
> ( n1 n2 -- f ) Returns true if n1 is greater than n2. 
0=    ( n -- f ) Returns true if n is zero. 
0<    ( n -- f ) Returns true if n is negative. 
0>    ( n -- f ) Returns true if n is positive. 

AND ( n1 n2 -- and ) Returns the logical AND.
OR  ( n1 n2 -- or ) Returns the logical OR.

?DUP ( n -- n n ) or( 0 -- 0 ) Duplicates only if n is non-zero. 
ABORT" xx" ( f -- ) If the flag is true, types out an error message, followed by the text. 
                    Also clears the stacks and returns control to the terminal. 
?STACK     ( -- f ) Returns true if a stack underflow condition has occurred.

条件运算是对栈顶的值进行比较,这个值可以是算术运算产生的,也可以是逻辑运算产生的,也可以是位运算产生的:

AND   \ n1 n2 -- n3   Returns n3 = n1 AND n2
OR    \ n1 n2 -- n3   Returns n3 = n1 OR n2
XOR   \ n1 n2 -- n3   Returns n3 = n1 XOR n2
INVERT   \ n1 -- n2   Returns bitwise inverse of n1.
LSHIFT \ x1 u -- x2   Logically shift x1 by u bits left.
RSHIFT \ x1 u -- x2   Logically shift x1 by u bits right.

DO循环:

: CountDown 0 -10 do I abs . loop ;
: AnotherCountDown 0 10 DO  I . -1 +LOOP ;

注意 Forth 系统中 I 和 J 都是内置的字,I 是从 Return Stack 中拷贝栈顶的值,J 则可以在嵌套循环中使用,切勿和C语言中常用的变量混淆。对于内置的编译的字,可以使用参数调用的形式:

: CountDown do I . Loop drop ;  ok...
10 1 CountDown 1 2 3 4 5 6 7 8 9  ok..

UNTIL循环:

: STAR   [CHAR] * EMIT ;
: STARS  ( #stars -- )  0 ?DO  STAR  LOOP ;

: A/STARS  ( height -- )
   BEGIN  CR  1- DUP SPACES DUP STARS  DUP 0= UNTIL  DROP ;

运行它:

10 a/stars 
         *********
        ********
       *******
      ******
     *****
    ****
   ***
  **
 *

各种循环的语法格式如下:

DO ... LOOP     DO: ( limit index -- ) LOOP: ( -- ) 
DO ... n +LOOP  DO: ( limit index -- )+LOOP: ( -- )

BEGIN ... UNTIL             UNTIL: ( f -- ) Sets up an indefinite loop which ends when f is true.
BEGIN xxx WHILE yyy REPEAT  WHILE: ( f -- ) Sets up an indefinite loop which 
                                            always executes xxx (condition) and also yyy if f is true.
LEAVE           ( -- ) Terminate the loop immediately.
QUIT            ( -- ) Terminates execution for the current task and returns control to the terminal.

CASE选择:

CASE key
    value1 OF  ENDOF
    value2 OF  ENDOF
    ...
     ( otherwise clause )
ENDCASE

浮点与定点

什么是浮点数呢?请参考前不久发布的文章 那年声明理解不了定义与初始化(一) - 浮点数 部分。简单来说就是通过指数运算的形式来实现浮点的移动,如 3 可以表示为 3.0x101。也可以表示为 0.3x1010,小数点位置就移动了,而这种情况下整数部分为 0,小数部分称为尾数。如果在计算机上,也如此处理,只是需要将基数 10 转换成 2,数值 300 转换成二进制来表示。在不同的CPU上,具体浮点实现是不同,在PC机上基本都是按 IEEE754 标准来实现,即单精度、双精度分别使用 8-bit、11-bit 来表示指数,使用 23-bit、52-bit 来表示尾数,使用1-bit来表示符号位,这样单精度、双精度分别为 16-bit 和 64-bit 的值。这样前面的数值 3 就可以表示为二进制的 11,以浮点表示为 1.1x21,在IEEE754标准中,约定整数1、和底数2不保存在二进制数据中,这样可以节省比特位,在二进制数据中只保存符号位、指数、和尾数。同时指数还要计数偏移值127、1023,使单精度、双精度指数的范围分别在 -127~128、-1023~1024,这样就可以表示负指数的数据。转换为单精度的二进制表示:

3 => 11 => 100 0000 0000 0000 0000 0000 (23-bit Mantissa)
0 (1-bit sign)
0000 0001 => 1000 0000 (8-bit exponent,add offset 127)
0100 0000 0100 0000 0000 0000 0000 0000 => 0x40400000 (final data)

由于表达浮点数尾数位数是有限的,即精度有限,在浮点运算过程中就会存在误差问题,比如在VC中运算 34.6f-34.0f,在将十进制数转换为浮点数值是误差就出现了:

CString ah;
al.Format(""34.6f-34.0f=%f",34.6f-34.0f) ; //0.599998

以下是Intel x86 CPU支持浮点数:

Data Type           Length  Precision(Bits)
Half Precision          16      11
Single Precision        32      24
Double Precision        64      53
Double Extended         80      64

而对于定点数,则约定小数点的位置,比如说8-bit的数值,用高4-bit表示整数部分,余下的4-bit作为小数部分,这样小数点就定下了。为了处理方便,一般情况下将定点数分为定点纯整数和纯小数。对于同样的8-bit,使用定点数表示的数值范围比浮点数表示的范围要小,但是精度却是定点数高,因为所有比特位都用来表示有效值。浮点数的计算是比较复杂的,而在微控制系统中,并不是所有的CPU都提供浮点数的硬件支持,而通过软件来模拟的浮点运算比硬件的速度上要慢上4-15倍速。

Forth 的浮点部分的内容几乎是另一套独立的系统,如浮点变量定义用 FVARIABLE,赋值用 F!,定义浮点常量用 FCONSTANT,加减乘除用 F+ F- F* F/等等,例如下面的浮点变量计算:

FVARIABLE co
FVARIABLE cor
FVARIABLE cog
FVARIABLE cob
2.99e-1 cor f!
5.87e-1 cog f!
1.14e-1 cob f!
4.92E-1 co F!
co f@ fe.           ; 492.099E-3  ok
co f@ cor f@ f* fe. ; 147.138E-3  ok
co f@ cog f@ f* fe. ; 288.862E-3  ok
co f@ cob f@ f* fe. ; 56.0993E-3  ok

前面所提到的所有字,基本上前缀一个F就是对应的浮点版本。下面为浮点系统定义的字,注意注解的表达,f: 这样的字符提示对应的堆栈是浮点堆栈:

FEXP    ( f: x -- e^x)
FLN     ( f: x -- ln[x])
FSQRT   ( f: x -- x^0.5)
FDROP   ( F: r -- ) or ( r -- )
FDUP    ( F: r -- r r ) or ( r -- r r )
FDEPTH  ( -- +n )

>FLOAT  ( c-addr u -- true | false ) ( F: -- r | ) or ( c-addr u -- r true | false )
        An attempt is made to convert the string
D>F     ( d -- ) ( F: -- r ) or ( d -- r )
F0<     ( -- flag ) ( F: r -- ) or ( r -- flag )
F0=     ( -- flag ) ( F: r -- ) or ( r -- flag )
F<      ( -- flag ) ( F: r1 r2 -- ) or ( r1 r2 -- flag )
F>D     ( -- d ) ( F: r -- ) or ( r -- d )
F@      ( f-addr -- ) ( F: -- r ) or ( f-addr -- r )

IO与文件

前面已经讲到通过 EMIT 来打印一个ASCII码对应的字符,还有使用 .” 来定义字符串,通过 TYPE 来显示。而 KEY 则可以将按键输入的字符转换成ASCII码值,和EMIT相反。而 ACCEPT 则可以用来保存输入的字符串,如例子,PAD 返回一个缓存地址,40是ACCEPT的参数,用来接收最长40个字符输入:

: greetings \ --
cr ." Enter your name : " pad 40 accept
cr ." Hi there " pad swap type
;

EMIT        char --   Print the specified character at the current output position.
TYPE addr length --   Print characters starting from the given address.
COUNT     ( addr -- addr len ) Count the string and leave len to the top of stack.
CR               --   Perform a carriage return, line feed operation.
PAGE             --   Clear the screen. Often called CLS.

." xxx"     ( -- )        Prints the character string xxx use inside definition. 
S" xxx"         ( -- addr len ) Define string content.
C" xxx"         ( -- addr )     Define string content.

KEY      -- char   Waits until a key is pressed and return its ASCII value
KEY?     -- t/f    Returns a true flag if a character is available at the keyboard;
ACCEPT addr +n -- len

DIGIT c base -- n t/f  DIGIT converts the ASCII character to a number.
NUMBER? addr -- nl..nn n2  Parsing numerical string input.

文件操作单词需要系统提供的文件ID,当操作系统打开一个文件时就会返回一个关联的ID,在不需要使用文件时,还需要告诉系统关闭和ID关联的文件。大多数文件操作单词都会产生一个状态标志 “ior”,值为 0 时表示操作是成功的。下面是一个创建二进制文件的例子:

0 VALUE fileid
: TextFile
R/W CREATE-FILE 0= IF
TO fileid
fileid WRITE-FILE
fileid FLUSH-FILE
fileid CLOSE-FILE
ELSE ." ERROR!"
THEN ;

CREATE bytes   61 c, 8A c, 63 c, D2 c, FB c,
bytes 5 S" C:\writing\t.txt" TextFile

以下是文件操作的相关词汇:

R/O     ( -- fmode ) read/only mode for OPEN-FILE and CREATE-FILE
W/O     ( -- fmode ) write/only mode for OPEN-FILE and CREATE-FILE
R/W     ( -- fmode ) read/write mode for OPEN-FILE and CREATE-FILE

OPEN-FILE     ( adr slen fmode -- fileid ior ) opens a file - error if it doesn't exist
CREATE-FILE   ( adr slen fmode -- fileid ior ) create a file - always overwrite any existing file
CLOSE-FILE    ( fileid -- ior ) close a file
FILE-POSITION ( fileid -- len-ud ior ) get current file position
ADVANCE-FILE  ( len-ud fileid -- ior ) set file position RELATIVE to current position, not ANS
REPOSITION-FILE ( len-ud fileid -- ior ) set file position, absolute from beginning of file
FILE-APPEND   ( fileid -- ior ) set file position to end of file, not ANS
FLUSH-FILE    ( fileid -- ior ) force write to disc of file's OS buffers
READ-FILE     ( b-adr b-len fileid -- len ior ) read binary data from a file.
WRITE-FILE    ( adr slen fileid -- ior ) write to a file. ior = 0 = success
READ-LINE     ( adr len fileid -- len eof ior ) read a line from file, delimited by cr+lf
WRITE-LINE    ( adr len fileid -- ior )  write a line to file - automatically append cr+lf
FILE-SIZE     ( fileid -- len-ud ior ) get file total size
RESIZE-FILE   ( len-ud fileid -- ior ) resize a file
FILE-STATUS   ( adr len -- x ior ) get current status flags of a file
DELETE-FILE   ( adr len -- ior ) delete a file - must be closed -
RENAME-FILE   ( adr1 len adr2 len -- ior ) rename file from name1 to name2

例子:RC4对称加密

在WIKI上找到一个非常赞的示例代码,展示了RC4加密算法是如何工作的,RC4是对称加密算法,和RSA同出一人之手,他就是纳德·李维斯特。1977年罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的公匙加密算法,取三人姓氏首字母命名该算法。可以说在对称加密算法,非对称加密算法和散列 Hash 三类常用加密算法中,RSA是当世界上最为重要的加密算法之一。RSA算法基于一个十分简单的数论事实:将两个大素数 P、Q 相乘得到 N 十分容易,但想要对乘积 N 进行因式分解得到 P、Q 却极其困难,因此可以将乘积公开作为密钥。

1987年,Ron Rivest 为他的公司 RSA Data Security, Inc. 发明了 RC4 加密系统,加密过程十分简洁明了,以致可以用大多数据语言重新编写。

设有一个256字节的数组,用它来加密明文 plaintext,每使用一次,数组的就要交换其中两个字节。被交换的两个字节通过变量 i j 来指定,它们初始值为 0。计算 i 的新值时,直接加一,计算 j 的新值时,将 i 数值对应的数组字节值和密钥字节值相加得到。要得到密文 ciphertext,将明文和 i j 求和后指示的字节相异或 XOR,加密 encrypt 和解密 decrypt 的过程一样。然后交换 i j 指示的数组字节,所有操作都对256求模,数组使用前经过初始化,值依次为 0-255。密钥长度在 1-256字节,以下为 Forth 语言的实现代码:

0 value ii        0 value jj
0 value KeyAddr   0 value KeyLen
create SArray   256 allot   \ state array of 256 bytes
: KeyArray      KeyLen mod   KeyAddr ;

: get_byte      + c@ ;
: set_byte      + c! ;
: as_byte       255 and ;
: reset_ij      0 TO ii   0 TO jj ;
: i_update      1 +   as_byte TO ii ;
: j_update      ii SArray get_byte +   as_byte TO jj ;
: swap_s_ij
    jj SArray get_byte
       ii SArray get_byte  jj SArray set_byte
    ii SArray set_byte
;

: rc4_init ( KeyAddr KeyLen -- )
    256 min TO KeyLen   TO KeyAddr
    256 0 DO   i i SArray set_byte   LOOP
    reset_ij
    BEGIN
        ii KeyArray get_byte   jj +  j_update
        swap_s_ij
        ii 255 < WHILE
        ii i_update
    REPEAT
    reset_ij
;
: rc4_byte
    ii i_update   jj j_update
    swap_s_ij
    ii SArray get_byte   jj SArray get_byte +   as_byte SArray get_byte  xor
;

测试代码如下,输出结果应为 F1 38 29 C9 DE:

hex
create AKey   61 c, 8A c, 63 c, D2 c, FB c,
: test   cr   0 DO  rc4_byte . LOOP  cr ;
AKey 5 rc4_init
2C F9 4C EE DC  5 test   \ output should be: F1 38 29 C9 DE

不过WIKI上的这段代码运行虽然是正确的,但它在表达RC4算法上有逻辑错误,因为无法进行解码。

资源参考

  • VirtualNES 官网下载
  • Nintendo MMC3
  • NES Developement Wiki
  • MCS6500 PROGRAMMING MANUAL JANUARY 1976
  • Visual Transistor-level Simulation of the 6502 CPU
  • Intel 4004 35th anniversary project
  • NES Cart Database
  • Magicengine MagicKit v2.51
  • 3ML EDITOR v2.0.3 Build 783 (2008/06/22)
  • FC平台开发工具下载
  • VirtuaNSF 1.0.4.3 NSF播放器
  • CC65 - the 6502 C Compiler
  • David A. Wheeler’s 6502 Language Implementation Approaches
  • David A. Wheeler - WAYS TO IMPLEMENT COMPUTER LANGUAGES ON 6502s
  • Thinking Forth Project
  • Moving Forth Part I: Design Decisions in the Forth Kernel by Brad Rodriguez
  • Starting FORTH by Leo Brodie
  • A Beginner’s Guide to Forth by J.V. Noble
  • Win32Forth 6.15.04 at SourceForge
  • VirtualNES虚拟机
  • Crafting a Compiler by Charles N. Fisher & Richard J. LeBlanc, Jr
  • forth32 : a 32 bit MSDOS excerpt of fig-Forth
  • Wiki - Forth Programming language

你可能感兴趣的:(Forth)