[2022考事业单位e类看什么书]汉化理查德琼斯写的forth教程(二)

事业单位考试网(sydw.cn)最新考试信息:[2022考事业单位e类看什么书]汉化理查德琼斯写的forth教程(二),包含报名时间及入口、考试时间、笔试内容等信息,更多事业单位考试内容请查看:事业单位考试内容

  汉化理查德琼斯写的forth教程(二)

由于这次汉化有两个文件, 所以将其分为两篇文章阐述,

这个文件主要是基于前面在汇编基础上建造的原语,来构建稍微复杂一点的编程语法(句法)

或者说构建更为适合人阅读和理解的代码块, 我说过这是使用分治算法思维的一种体现,

这种利用分治进行递归的过程是没有极限的,不会受到C语族那样的语法信息茧房的限制,

栈语言没有lisp这种通过宏造DSL的说法, 因为forth这样的栈语言根本不需要用DSL来证明自己能屈能伸!

为啥?

因为栈语言可以从机器语言开始建起, 直到超越自然语言.

我认为中文汉字编程的春天可以来临啦, 建生态繁荣就可以, 因为栈语言实在太简单了, 业余人和热衷中文编程人,自不必多说, 那些非专业编程的朋友, 只是想用编程解决一些日常生活工作琐碎问题, 又不想学专业C语族那复杂编程的朋友, 这次真的可以实现啦,

为啥栈语言简单, 我一句话就可以描述完它的语法(句法); 就是栈这个字本身的含义, 一个装东西的器皿.

咋样,够简单吧, 你在使用它的时候,只要脑子里一直保持一个栈的形象, 你想象出一个装东西的器皿即可熟练使用,

为此,我还做了3D视频来讲解和演示, 有兴趣的朋友, 可以翻看我的视频.

现在栈语言的生态还非常孱弱, 所以让不知其为何物的广大陌生朋友觉得它神秘 难用, 又因为现在栈语言只有forth PostScript 这类基于拉丁字母的英文实现, 但这不要紧, 只要丰富其用中文汉字写的代码生态即可, 新蓝海 新赛道,需要大家共同建设.

不说多的, 贴第二个文件的代码;

\ -*- 文本 -*-
\	一个有时是最小的FORTH编译器和Linux / i386系统的教程。 -*- asm -*-
\	作者:Richard W.M. Jones <rich@annexia.org> http://annexia.org/forth
\	这是公共领域(见下面的公共领域发布声明)。
\	$Id: jonesforth.f,v 1.18 2009-09-11 08:32:33 rich Exp $
\
\	本教程的第一部分在jonesforth.S.中,得到它http://annexia.org/forth。
\
\	公共领域 --------------------------------
\	我,本作品的版权持有人,特此将其释放到公共领域。这适用于全世界。

\	如果这在法律上是不可能的,我授予任何实体为任何目的使用这一作品的权利。
\   除非法律要求有这样的条件。

\	设定 ----------------------------------

\	让我们先把几件家务事做完。 
\   首先,因为我需要绘制大量的ASCII-art图表来解释概念,
\   最好的方法是使用一个固定宽度字体的窗口,
\   并且至少有这么宽:

\<------------------------------------------------------------------------------------------------------------------------>

\		第二,确保TABS被设置为8个字符。 
\   下面应该是一条垂直线。 
\   如果不是,请整理好你的标签。

\		    |
\	        |
\	    	|

\	第三,我假设你的屏幕至少有50个字符高。

\	FORTH 代码的开始 -------------------------

\
\现在我们已经达到了FORTH系统运行和自我托管的阶段。 
\所有进一步的字都可以写成FORTH本身,包括像IF、THEN、. "等在大多数语言中被认为是相当基本的字。
\

\ 关于代码的一些说明:

\我使用缩进来显示结构。 
\然而,空格的多少对FORTH来说没有任何意义,
\只是你必须在字之间至少使用一个空格字符,
\而且字本身不能包含空格。

\	FORTH是区分大小写的。 使用capslock!

\ 原语字/MOD(DIVMOD)将商和余数都留在栈中。 (在i386上,idivl指令无论如何都会给出两者)。 
\ 现在我们可以用/MOD和其他一些原语来定义 / 和 MOD 。

: / /MOD SWAP DROP ;
: MOD /MOD DROP ;

\ 定义一些字符常量

: '\n' 10 ;
: BL   32 ; \ BL(BLank)是一个标准的FORTH字,表示空格。

\ CR prints a carriage return CR打印一个回车键
: CR '\n' EMIT ;

\ SPACE 打印一个空格
: SPACE BL EMIT ;

\ NEGATE在栈中留下一个数字的负数。
: NEGATE 0 SWAP - ;

\ 布尔运算的标准字。
: TRUE  1 ;
: FALSE 0 ;
: NOT   0= ;

\ LITERAL接收栈上的任何东西并编译LIT <foo>。
: LITERAL IMMEDIATE
	' LIT ,		\ 编译LIT
	,		\ 编译字面本身(来自栈)。
	;

\现在我们可以使用[ 和 ] 来插入在编译时计算的字面。 
\(回想一下,[ 和 ] 是 FORTH 的字,用于切换到立即模式和非立即模式)。
\在定义中,在"...... "是一个常量表达式的地方使用[ ......] LITERAL,
\你希望只计算一次(在编译时,而不是在每次运行字时计算它)。

: ':'
	[		\ 进入立即模式(临时)。
	CHAR :		\ 将数字58(冒号的ASCII码)推到参数栈上
	]		\ 回到编译模式
	LITERAL		\ 将LIT 58编译为':'字的定义
;

\ 还有一些字符常量的定义方式与上面相同。
: ';' [ CHAR ; ] LITERAL ;
: '(' [ CHAR ( ] LITERAL ;
: ')' [ CHAR ) ] LITERAL ;
: '"' [ CHAR " ] LITERAL ;
: 'A' [ CHAR A ] LITERAL ;
: '0' [ CHAR 0 ] LITERAL ;
: '-' [ CHAR - ] LITERAL ;
: '.' [ CHAR . ] LITERAL ;

\ 在编译时,'[COMPILE] word'会编译'word',如果它本来是IMMEDIATE。
: [COMPILE] IMMEDIATE
	WORD		\ 获取下一个字
	FIND		\ 在字典中找到它
	>CFA		\ 获取其代码字
	,		\ 并将其编译为
;

\RECURSE对正在编译的当前字进行递归调用。

\通常情况下,当一个字正在被编译时,它被标记为HIDDEN,
\所以对同一字的引用是对该字的前一个定义的调用。 
\但是我们仍然可以通过LATEST指针访问我们目前正在编译的字,
\所以我们可以使用它来编译一个递归调用。

: RECURSE IMMEDIATE
	LATEST @	\ LATEST 指向目前正在编译的字
	>CFA		\ 获取代码字
	,		\ 编译它
;

\	控制结构 ---------------------

\到目前为止,我们只定义了非常简单的定义。 
\在我们能更进一步之前,我们确实需要制作一些控制结构,如IF ... THEN和循环。 
\幸运的是,我们可以在FORTH中直接定义任意的控制结构。

\请注意,我在这里定义的控制结构只能在编译后的字中工作。 
\如果你试图在立即模式下使用IF等输入表达式,那么它们将无法工作。
\让这些东西在立即模式下工作是留给读者的一个练习。

\ condition IF true-part THEN rest
\ 编译为: --> condition 0BRANCH OFFSET true-part rest
\ 其中OFFSET是'rest'的偏移量
\ condition IF true-part ELSE false-part THEN
\ 编译为: --> condition 0BRANCH OFFSET true-part BRANCH OFFSET2 false-part rest
\ 其中OFFSET是false-part的偏移,OFFSET2是rest的偏移。

\IF是一个IMMEDIATE字,它编译了0BRANCH,
\后面是一个假动作偏移量,并把0BRANCH的地址放在栈上。 
\之后当我们看到THEN时,我们从栈中弹出该地址,
\计算偏移量,并回填偏移量。

: IF IMMEDIATE
	' 0BRANCH ,	\ 编译0BRANCH
	HERE @		\ 保存栈上的偏移量位置
	0 ,		\ 编译一个假动作偏移量
;

: THEN IMMEDIATE
	DUP
	HERE @ SWAP -	\ 计算出保存在栈中的地址的偏移量
	SWAP !		\ 将偏移量存储在回填的位置
;

: ELSE IMMEDIATE
	' BRANCH ,	\ 明确的分支到刚刚超过false-part位置
	HERE @		\ 保存栈上的偏移量位置
	0 ,		\ 编译一个假动作偏移量
	SWAP		\ 现在回填原来的(IF)偏移量
	DUP		\ 与上面的THEN字相同
	HERE @ SWAP -
	SWAP !
;

\ BEGIN 循环部分 条件 UNTIL
\	-- 编译为: --> 循环部分 条件 0BRANCH OFFSET
\	其中OFFSET指向循环部分
\ 这就像C语言中的do { loop-part } while (condition)。

: BEGIN IMMEDIATE
	HERE @		\ 在栈上保存位置
;

: UNTIL IMMEDIATE
	' 0BRANCH ,	\ 编译0BRANCH
	HERE @ -	\ 计算出保存在栈上的地址的偏移量
	,		\ 在这里编译偏移量
;

\ BEGIN 循环部分 AGAIN
\	-- 编译为: --> 循环部分 BRANCH OFFSET
\	其中OFFSET指向循环部分
\ 换句话说,一个无限循环,只能用EXIT来返回。
: AGAIN IMMEDIATE
	' BRANCH ,	\ 编译BRANCH
	HERE @ -	\ 计算回来的偏移量
	,		\ 在这里编译偏移量
;

\ BEGIN condition WHILE loop-part REPEAT
\	-- 编译为: --> condition 0BRANCH OFFSET2 loop-part BRANCH OFFSET
\	其中 OFFSET 指向条件(起始),OFFSET2 指向整段代码的后面
\  所以这就像C语言中的 while (condition) { loop-part } 循环。
: WHILE IMMEDIATE
	' 0BRANCH ,	\ 编译0BRANCH
	HERE @		\ 保存栈中offset2的位置
	0 ,		\ 编译一个假动作offset2
;

: REPEAT IMMEDIATE
	' BRANCH ,	\ 编译BRANCH
	SWAP		\ 获取原始偏移量(从BEGIN开始)。
	HERE @ - ,	\ 并在BRANCH之后编译它
	DUP
	HERE @ SWAP -	\ 计算 offset2
	SWAP !		\ 并将其回填到原来的位置
;

\UNLESS与IF相同,但 test 内容相反.

\注意 [COMPILE] 的使用。由于 IF 是IMMEDIATE的,我们不希望它在 UNLESS 编译时被执行,
\而是在 UNLESS 运行时被执行(这恰好是使用 UNLESS 的字被编译的时候 -- whew!)。 
\所以我们用[COMPILE]来扭转将IF标记为立即执行的效果。
\当我们想编写自己的控制字时,通常会使用这个技巧,而不必全部用原语0BRANCH和BRANCH来实现,
\而是重复使用更简单的控制字,比如(在这个例子中)IF。

: UNLESS IMMEDIATE
	' NOT ,		\ 编译NOT(反转 test)
	[COMPILE] IF	\ 通过调用正常的IF继续
;

\	注释 -------------------------------------

\FORTH允许(...)作为函数定义的注释。 
\这是由一个叫做()的IMMEDIATE字来实现的,它只是将输入的字符丢掉,直到碰到相应的)。
: ( IMMEDIATE
	1		\ 通过跟踪深度,允许嵌套的括弧。
	BEGIN
		KEY		\ 读下一个字符
		DUP '(' = IF	\ 开括弧?
			DROP		\ 丢掉开括弧
			1+		\ 深度增加
		ELSE
			')' = IF	\ 关括弧?
				1-		\ 深度减少
			THEN
		THEN
	DUP 0= UNTIL		\ 继续,直到我们到达匹配的关括弧,深度为0
	DROP		\ 丢掉深度计数器
;

(
	从现在开始,我们可以使用 ( ... )作为注释.

	栈备注 ----------------------------------------------------------------------

	在FORTH风格中,我们也可以用( ... -- ... )来显示一个字对参数栈的影响。 
	比如说:

	( n -- )	表示该字从参数栈中消耗一个整数(n)。
	( b a -- c )	表示该字使用了两个整数(a和b,其中a在栈顶)
				并返回一个单一的整数(c)。
	( -- )		表示该字对栈没有影响
)

( 一些更复杂的栈例子,显示了栈的备注. )
: NIP ( x y -- y ) SWAP DROP ;
: TUCK ( x y -- y x y ) SWAP OVER ;
: PICK ( x_u ... x_1 x_0 u -- x_u ... x_1 x_0 x_u )
	1+		( 因为'u'在栈中,所以增加一个 )
	4 *		( 乘以字的大小 )
	DSP@ +		( 加到栈指针上 )
	@    		( 并拾取 )
;

( 有了循环结构,我们现在可以写SPACES,将n个空格写到stdout上. )
: SPACES	( n -- )
	BEGIN
		DUP 0>		( while n > 0 )
	WHILE
		SPACE		( 打印一个空格 )
		1-		( 直到我们倒数至0 )
	REPEAT
	DROP
;

( 操纵BASE的标准字. )
: DECIMAL ( -- ) 10 BASE ! ;
: HEX ( -- ) 16 BASE ! ;

(
	打印号码 ----------------------------------------------------------------------

	FORTH的标准字.(DOT)是非常重要的。 
	它将栈顶的数字取出并打印出来。 
	然而,我首先要实现一些低级别的FORTH字:

	U.R	( u width -- )	打印一个无符号的数字,垫起到一定的宽度。
	U.	( u -- )	它打印出一个无符号的数字
	.R	( n width -- )	它打印一个有符号的数字,垫起到一定的宽度。

	例如:
		-123 6 .R
	将会打印出这些字符:
		<space> <space> - 1 2 3

	换句话说,数字向左填充到一定数量的字符。

	即使宽度更宽广,也会打印出完整的数字,
	这就是我们能够定义普通函数 U. 和 . 的原因
	(我们只是将宽度设置为0,因为知道无论如何都会打印完整的数字)。

	. 和 朋友 的另一个问题是,他们服从变量BASE中的当前基数。
	BASE可以是2到36范围内的任何东西

	当我们定义 .&c 的时候,我们也可以定义 .S ,这是一个有用的调试工具。 
	这个字可以从顶到底打印出当前的栈(非破坏性的)。
)

( 这是U的潜在递归定义。)
: U.		( u -- )
	BASE @ /MOD	( 宽度为 REM quot )
	?DUP IF			( 如果商数<>0,那么 )
		RECURSE		( 打印商数 )
	THEN

	( 打印余数 )
	DUP 10 < IF
		'0'		( 十进制数字 0..9 )
	ELSE
		10 -		( 十六进制及以上的数字 A..Z )
		'A'
	THEN
	+
	EMIT
;

(
	FORTH字 .S 打印出栈的内容。 
	它并不改变栈的内容。
	在调试时非常有用。
)

: .S		( -- )
	DSP@		( 获取当前栈指针 )
	BEGIN
		DUP S0 @ <
	WHILE
		DUP @ U.	( 打印栈元素 )
		SPACE
		4+		( 上移 )
	REPEAT
	DROP
;

( 这个字返回一个无符号数字在当前基数下的宽度(以字符为单位))
: UWIDTH	( u -- 宽度 )
	BASE @ /	( rem quot )
	?DUP IF		( 如果商数<>0,那么 )
		RECURSE 1+	( 返回 1 + 递归 调用 )
	ELSE
		1		( 返回 1 )
	THEN
;

: U.R		( u 宽度 -- )
	SWAP		( 宽度 u )
	DUP		( 宽度 u u )
	UWIDTH		( 宽度 u u宽度 )
	ROT		( u u宽度 宽度 )
	SWAP -		( u 宽度-u宽度 )
	( 在这一点上,如果要求的宽度更窄,我们在栈上会有一个负数。
	否则栈上的数字就是要打印的空格数。 
	但无论如何,SPACES不会打印负数的空格,所以现在调用SPACES是安全的 ... )
	SPACES
	( ... 然后再调用U.的潜在实现。 )
	U.
;

(
	.R 打印一个有符号的数字,填充到一定的宽度。 
	我们不能只打印符号并调用U.R,因为我们希望符号在数字的旁边('-123'而不是'- 123')。
)
: .R		( n 宽度 -- )
	SWAP		( 宽度 n )
	DUP 0< IF
		NEGATE		( 宽度 u )
		1		( 保存一个标志,以记住它是 负数 | 宽度 n 1 )
		SWAP		( 宽度 1 u )
		ROT		( 1 u 宽度 )
		1-		( 1 u 宽度-1 )
	ELSE
		0		( 宽度 u 0 )
		SWAP		( 宽度 0 u )
		ROT		( 0 u 宽度 )
	THEN
	SWAP		( flag 宽度 u )
	DUP		( flag 宽度 u u )
	UWIDTH		( flag 宽度 u u宽度 )
	ROT		( flag u u宽度 宽度 )
	SWAP -		( flag u 宽度-u宽度 )

	SPACES		( flag u )
	SWAP		( u flag )

	IF			( 是负数的吗? 打印"-"字符 )
		'-' EMIT
	THEN

	U.
;

( 最后,我们可以用 .R 来定义字 . ,并在后面加一个空格。 )
: . 0 .R SPACE ;

( 真实的 U. ,注意后面的空格. )
: U. U. SPACE ;

( ? 拾取一个地址的整数并打印出来。)
: ? ( addr -- ) @ . ;

( c a b WITHIN 如果a <= c且c < b,则返回true。 )
(  或不含ifs的定义: OVER - >R - R>  U<  )
: WITHIN
	-ROT		( b c a )
	OVER		( b c a c )
	<= IF
		> IF		( b c -- )
			TRUE
		ELSE
			FALSE
		THEN
	ELSE
		2DROP		( b c -- )
		FALSE
	THEN
;

( DEPTH 返回栈的深度。 )
: DEPTH		( -- n )
	S0 @ DSP@ -
	4-			( 调整,因为当我们推DSP时,S0在栈上。 )
;

(
	ALIGNED接收一个地址并将其四舍五入(对齐)到下一个4字节边界。
)
: ALIGNED	( addr -- addr )
	3 + 3 INVERT AND	( (addr+3) & ~3 )
;

(
	ALIGN使HERE指针对齐,所以下一个被附加的字将被正确对齐。
)
: ALIGN HERE @ ALIGNED HERE ! ;

(
	字符串 ----------------------------------------------------------------------

	S "string "在FORTH中用来定义字符串。 
它把字符串的地址和它的长度留在栈中,(长度在栈顶)。 
S "后面的空格是FORTH字之间的正常空格,不是字符串的一部分。

	这个定义很棘手,因为它必须根据我们是在编译还是在立即模式下做不同的事情。 
(因此这个字被标记为IMMEDIATE,所以它可以检测到这一点并做不同的事情)。

	在编译模式下,我们将LITSTRING <string length> <string rounded up 4 bytes>附加到当前字。 
原语的LITSTRING在执行当前字的时候会做正确的事情。

	在立即模式下,没有一个特别好的地方来放置字符串,但在这种情况下,我们把字符串放在这里(但我们_不_改变这里)。 
这是一个临时位置,可能很快就会被覆盖。
)

( C,将一个字节附加到当前编译的字上。 )
: C,
	HERE @ C!	( 在编译后的镜像中存储该字符 )
	1 HERE +!	( 将HERE的指针增加1个字节 )
;

: S" IMMEDIATE		( -- addr len )
	STATE @ IF	( 编译? )
		' LITSTRING ,	( 编译 LITSTRING )
		HERE @		( 在栈上保存长度字的地址 )
		0 ,		( 假动作长度 - 我们还不知道它是什么 )
		BEGIN
			KEY 		( 获取字符串的下一个字符 )
			DUP '"' <>
		WHILE
			C,		( 拷贝字符 )
		REPEAT
		DROP		( 丢掉末尾的双引号字符 )
		DUP		( 获取长度字的保存地址 )
		HERE @ SWAP -	( 计算出长度 )
		4-		( 减去4(因为我们是从长度字的开始测量的)。)
		SWAP !		( 并回填长度位置 )
		ALIGN		( 对于剩余的代码,四舍五入到下一个4字节的倍数。 )
	ELSE		( 立即模式 )
		HERE @		( 获取临时空间的起始地址 )
		BEGIN
			KEY
			DUP '"' <>
		WHILE
			OVER C!		( 保存下一个字符 )
			1+		( 递增地址 )
		REPEAT
		DROP		( 丢掉最后的 " 字符 )
		HERE @ -	( 计算出长度 )
		HERE @		( 推开始的地址 )
		SWAP 		( addr len )
	THEN
;

(
	. "是FORTH中的打印字符串操作符。 例如:." 要打印的东西" 操作符后面的空格是字与字之间需要的普通空格,不属于打印的内容。

	在立即模式下,我们只是不断地读取字符并打印它们,直到我们到达下一个双引号。

	在编译模式下,我们使用 S" 来存储字符串,然后在之后添加TELL。:
		LITSTRING <string length> <string rounded up to 4 bytes> TELL

	值得注意的是,使用[COMPILE]将对立即字 S" 的调用变成对该字的编译。 
	它把它编译到 ." 的定义中,而不是编译到这个字运行时的定义中(对你来说够复杂了?)
)

: ." IMMEDIATE		( -- )
	STATE @ IF	( 编译? )
		[COMPILE] S"	( 读取字符串,并编译LITSTRING,等等。 )
		' TELL ,	( 编译最终的TELL )
	ELSE
		( 在立即模式下,只需读取字符并打印,直到我们到达结束的双引号。 )
		BEGIN
			KEY
			DUP '"' = IF
				DROP	( 丢掉双引号字符 )
				EXIT	( 从该函数返回 )
			THEN
			EMIT
		AGAIN
	THEN
;

(
	常量与变量 ----------------------------------------------------------------------

	在FORTH中,全局常量和变量的定义是这样的:

	10 CONSTANT TEN		当TEN被执行时,它将整数10留在栈上。
	VARIABLE VAR		当VAR被执行时,它将VAR的地址留在栈上。

	常量可以被读取但不能被写入,例如:

	TEN . CR		prints 10

	你可以通过执行以下操作来读取一个变量(在本例中称为VAR):

	VAR @			在栈中留下VAR的值
	VAR @ . CR		打印VAR的值
	VAR ? CR		与上述相同,因为 ? 与 @ 相同。

	并通过以下操作更新该变量:

	20 VAR !		sets VAR to 20

	请注意,变量是未初始化的(但请看后面的VALUE,它提供了初始化的变量,句法稍显简单)。

	我们如何定义CONSTANT和VARIABLE这两个字?

	诀窍是为变量本身定义一个新字(例如,如果变量被称为 "VAR",那么我们将定义一个新字,称为VAR)。 
	这很容易做到,因为我们通过CREATE字暴露了字典条目的创建(上面是对 :的定义的一部分)。
	调用WORD [TEN] CREATE(其中[TEN]表示 "TEN "是输入的下一个字)会留下字典条目:

				   +--- HERE
				   |
				   V
	+---------+---+---+---+---+
	| LINK    | 3 | T | E | N |
	+---------+---+---+---+---+
                   len

	对于CONSTANT,我们可以继续添加DOCOL(代码字),
	然后是LIT,后面是常量本身,然后是EXIT,形成一个小的单词定义,返回常量:

	+---------+---+---+---+---+------------+------------+------------+------------+
	| LINK    | 3 | T | E | N | DOCOL      | LIT        | 10         | EXIT       |
	+---------+---+---+---+---+------------+------------+------------+------------+
                   len              codeword

	请注意,这个字的定义与你写的完全相同,
	如果你写的是 : TEN 10 ;

	请阅读下面代码的人注意:
	DOCOL是我们在汇编部分定义的一个常量字,它返回同名的汇编符号的值。
)

: CONSTANT
	WORD		( 获取名称(名称跟随CONSTANT) )
	CREATE		( 做字典条目 )
	DOCOL ,		( 追加DOCOL(这个字的代码字字段) )
	' LIT ,		( 追加代码字LIT )
	,		( 在栈顶追加值 )
	' EXIT ,	( 追加代码字EXIT )
;

(
	VARIABLE有点难,因为我们需要一个地方来放置这个变量。 
用户内存(由HERE指向的内存区域,我们之前只是在那里存储了新的字定义)没有什么特别的地方。 
我们可以从这个内存区域中切下一些位来存储任何我们想要的东西,
所以VARIABLE的一个可能的定义可能是这样的:

	   +--------------------------------------------------------------+
	   |								  |
	   V								  |
	+---------+---------+---+---+---+---+------------+------------+---|--------+------------+
	| <var>   | LINK    | 3 | V | A | R | DOCOL      | LIT        | <addr var> | EXIT       |
	+---------+---------+---+---+---+---+------------+------------+------------+------------+
        		     len              codeword

	其中<var>是存储变量的地方,而<addr var>则指向它。

	为了使其更加通用,让我们定义几个字,我们可以用它们来从用户内存中分配任意的内存。

	首先是ALLOT,其中n ALLOT分配了n个字节的内存。 
(注意在调用这个时,最好确保n是4的倍数,或者至少在下次编译字时,HERE已经被留成4的倍数)。
)

: ALLOT		( n -- addr )
	HERE @ SWAP	( here n )
	HERE +!		( adds n to HERE, 在这之后,HERE的旧值仍在栈上。 )
;

(
	第二,cells。 在FORTH中,"n CELLS ALLOT "这句话的意思是分配n个整数,不管这个机器架构上整数的自然大小。 
在这台32位机器上,CELLS只是将栈顶乘以4。
)
: CELLS ( n -- n ) 4 * ;

(
	因此,现在我们可以用与上面的CONSTANT相同的方式轻松定义VARIABLE。 
请参考上图,看看这样创建的字是什么样子的。
)
: VARIABLE
	1 CELLS ALLOT	( 分配1个cell的内存,把指针推到这个内存上 )
	WORD CREATE	( 编写字典中的条目(名称跟随VARIABLE) )
	DOCOL ,		( 追加DOCOL(这个字的代码字字段))
	' LIT ,		( 追加代码字LIT )
	,		( 将指针追加到新的内存中 )
	' EXIT ,	( 追加代码字EXIT )
;

(
	值 ----------------------------------------------------------------------

	VALUEs 与 VARIABLEs 类似,但句法更简单。 
一般来说,当你想要一个经常被读取,而不经常被写入的变量时,你会使用它们。

	20 VALUE VAL 	创建VAL,初始值为20
	VAL		直接将值(20)推到栈上。
	30 TO VAL	更新VAL,将其设置为30
	VAL		直接将值(30)推到栈上

	请注意,'VAL'本身并不返回值的地址,而是返回值本身,这使得值的使用比变量更简单、更明显(没有通过'@'的转接)。
其代价是一个更复杂的实现,尽管有这样的复杂性,但在运行时没有性能上的损失。

	一个天真的 "TO "的实现将是相当缓慢的,每次都要进行字典搜索。
但是因为这是FORTH,我们可以完全控制编译器,所以我们可以更有效地编译TO,
	把:
		TO VAL
	变成:
		LIT <addr> !
	并计算出 <addr> (值的地址)在编译的时候.

	现在,这就是聪明之处。 我们将像这样编译我们的值:

	+---------+---+---+---+---+------------+------------+------------+------------+
	| LINK    | 3 | V | A | L | DOCOL      | LIT        | <value>    | EXIT       |
	+---------+---+---+---+---+------------+------------+------------+------------+
                   len              codeword

	其中<value>是实际值本身。 
注意,当VAL执行时,它将把值推到栈上,这就是我们想要的结果.

	但TO将用什么来表示地址<addr>? 为什么当然是一个指向<value>的指针?:

		code compiled	- - - - --+------------+------------+------------+-- - - - -
		by TO VAL		  | LIT        | <addr>     | !          |
				- - - - --+------------+-----|------+------------+-- - - - -
							     |
							     V
	+---------+---+---+---+---+------------+------------+------------+------------+
	| LINK    | 3 | V | A | L | DOCOL      | LIT        | <value>    | EXIT       |
	+---------+---+---+---+---+------------+------------+------------+------------+
                   len              codeword

	换句话说,这是一种自我修改的代码。

	(请注意那些想修改这个FORTH以增加内联的人:以这种方式定义的值不能被内联).
)
: VALUE		( n -- )
	WORD CREATE	( 编写字典条目(名称跟随VALUE)。 )
	DOCOL ,		( 追加DOCOL )
	' LIT ,		( 追加 the 代码字 LIT )
	,		( 追加初始值 )
	' EXIT ,	( 追加 the 代码字 EXIT )
;

: TO IMMEDIATE	( n -- )
	WORD		( 获取值的名称 )
	FIND		( 查字典吧 )
	>DFA		( 获取第一个数据字段的指针("LIT") )
	4+		( 递增到指向值 )
	STATE @ IF	( 编译吗?)
		' LIT ,		( 编译 LIT )
		,		( 编译该值的地址 )
		' ! ,		( 编译 ! )
	ELSE		( 立即模式 )
		!		( 直接更新它 )
	THEN
;

( x +TO VAL 将x添加到VAL中 )
: +TO IMMEDIATE
	WORD		( 获取值的名称 )
	FIND		( 查字典吧 )
	>DFA		( 获取第一个数据字段的指针("LIT") )
	4+		( 递增到指向值 )
	STATE @ IF	( 编译吗? )
		' LIT ,		( 编译 LIT )
		,		( 编译该值的地址 )
		' +! ,		( 编译 +! )
	ELSE		( 立即模式 )
		+!		( 直接更新它 )
	THEN
;

(
	打印字典 ----------------------------------------------------------------------

	ID.接收一个字典条目的地址,并打印出该字的名称。

	比如说。LATEST @ ID.将打印最后一个被定义的字的名称。
)
: ID.
	4+		( 跳过链接指针 )
	DUP C@		( 获取标志/长度字节 )
	F_LENMASK AND	( 屏蔽掉标志--只想要长度 )

	BEGIN
		DUP 0>		( 长度 > 0? )
	WHILE
		SWAP 1+		( addr len -- len addr+1 )
		DUP C@		( len addr -- len addr char | 获取下一个字符)
		EMIT		( len addr char -- len addr | 并打印它)
		SWAP 1-		( len addr -- addr len-1    | 从长度上减去1 )
	REPEAT
	2DROP		( len addr -- )
;

(
	'WORD word FIND ?HIDDEN'如果'word'被标记为隐藏,则返回真。

	'WORD word FIND ?IMMEDIATE'如果'WORD'被标记为立即,则返回真。
)
: ?HIDDEN
	4+		( 跳过链接指针 )
	C@		( 获取标志/长度字节 )
	F_HIDDEN AND	( 屏蔽F_HIDDEN标志并返回它(作为一个真值) )
;
: ?IMMEDIATE
	4+		( 跳过链接指针 )
	C@		( 获取标志/长度字节 )
	F_IMMED AND	( 屏蔽F_IMMED标志并返回它(作为一个真值) )
;

(
	WORDS打印字典中定义的所有字,从最近定义的字开始。但是它不打印隐藏的字。

	该实现只是使用链接指针从LATEST向后迭代。
)
: WORDS
	LATEST @	( 从LATEST字典条目开始 )
	BEGIN
		?DUP		( 当链接指针不为空指针时 )
	WHILE
		DUP ?HIDDEN NOT IF	( 忽略隐藏的文字 )
			DUP ID.		( 但如果没有隐藏,则打印出字 )
			SPACE
		THEN
		@		( 解除对链接指针的引用 - 转到上一个字 )
	REPEAT
	CR
;

(
	遗忘 ----------------------------------------------------------------------

	到目前为止,我们只分配了字和内存。 FORTH提供了一个相当原始的方法来取消分配。

	FORGET word'从字典中删除了'word'的定义,以及在它之后定义的所有内容,包括之后分配的任何变量和其他内存。

	实现起来非常简单--我们查找字(会返回字典条目地址)。 
然后我们设置 HERE 指向该地址,因此实际上所有未来的分配和定义都将覆盖从该字开始的内存。 
我们还需要设置LATEST来指向前一个字。

	请注意,你不能忘记内建字(好吧,你可以尝试,但可能会导致故障)。

	XXX:因为我们写VARIABLE是为了将变量存储在字之前分配的内存中,在目前的实现中,VARIABLE FOO FORGET FOO将泄露1个cell 的内存。
)
: FORGET
	WORD FIND	( 找到这个字,获得字典中的条目地址 )
	DUP @ LATEST !	( 设置LATEST指向前一个字 )
	HERE !		( 并将HERE与字典中的地址存储在一起 )
;

(
	转储 ----------------------------------------------------------------------

	DUMP用于转储内存中的内容,采用'传统' hexdump格式。

	注意,DUMP的参数(地址、长度)与WORD和S"等字符串字兼容。

	您可以通过执行以下操作来转储您定义的最后一个字的原始代码:

		LATEST @ 128 DUMP
)
: DUMP		( addr len -- )
	BASE @ -ROT		( 将当前的BASE保存在栈底 )
	HEX			( 并切换到十六进制模式 )

	BEGIN
		?DUP		( while len > 0 )
	WHILE
		OVER 8 U.R	( 打印地址 )
		SPACE

		( 在这一行最多打印16个字 )
		2DUP		( addr len addr len )
		1- 15 AND 1+	( addr len addr linelen )
		BEGIN
			?DUP		( while linelen > 0 )
		WHILE
			SWAP		( addr len linelen addr )
			DUP C@		( addr len linelen addr byte )
			2 .R SPACE	( 打印字节 )
			1+ SWAP 1-	( addr len linelen addr -- addr len addr+1 linelen-1 )
		REPEAT
		DROP		( addr len )

		( 打印ASCII码的等价物 )
		2DUP 1- 15 AND 1+ ( addr len addr linelen )
		BEGIN
			?DUP		( while linelen > 0)
		WHILE
			SWAP		( addr len linelen addr )
			DUP C@		( addr len linelen addr byte )
			DUP 32 128 WITHIN IF	( 32 <= c < 128? )
				EMIT
			ELSE
				DROP '.' EMIT
			THEN
			1+ SWAP 1-	( addr len linelen addr -- addr len addr+1 linelen-1 )
		REPEAT
		DROP		( addr len )
		CR

		DUP 1- 15 AND 1+ ( addr len linelen )
		TUCK		( addr linelen len linelen )
		-		( addr linelen len-linelen )
		>R + R>		( addr+linelen len-linelen )
	REPEAT

	DROP			( 恢复栈 )
	BASE !			( 恢复保存的 BASE )
;

(
	情况 ----------------------------------------------------------------------

	CASE……ENDCASE是我们在FORTH中执行switch语句的方式。
这方面没有普遍一致的句法,所以我使用ISO标准FORTH (ANS-FORTH)强制使用的句法。

		( 栈上的某个值 )
		CASE
		test1 OF ... ENDOF
		test2 OF ... ENDOF
		testn OF ... ENDOF
		...2022考事业单位e类看什么书 ( default case 默认情况 )
		ENDCASE

	CASE语句通过对比栈中的值与test1, test2, ..., testn 是否相等来测试它,并在OF ... ENDOF中执行匹配的代码。
如果没有一个测试值匹配,那么就执行默认情况。 
在默认情况下的......里面,值仍然在栈顶(它被ENDCASE隐含地DROP了)。 
当ENDOF被执行时,它在ENDCASE之后跳转(即-没有 "落空",也不需要像C语言那样的break语句)。

	默认情况可以被省略。 
事实上,测试也可以省略,这样你就只有一个默认情况,尽管这可能不是很有用。

	一个例子(假设'q'等等 是将字母的ASCII值推到栈上的字):

		0 VALUE QUIT
		0 VALUE SLEEP
		KEY CASE
			'q' OF 1 TO QUIT ENDOF
			's' OF 1 TO SLEEP ENDOF
			( default case: )
			." Sorry, I didn't understand key <" DUP EMIT ." >, try again." CR
		ENDCASE

	(在某些版本的FORTH中,支持更高级的测试,如范围等。其他版本的FORTH需要你写OTHERWISE来表示默认情况。
正如我上面所说,这个FORTH试图遵循ANS FORTH的标准)。

	CASE...ENDCASE的实现有点非同小可。 
我是按照这里的实现来做的:
	http://www.uni-giessen.de/faq/archiv/forthfaq.case_endcase/msg00000.html

	一般的计划是将代码编译为一系列的IF语句:

	CASE				(在立即模式的参数栈中推0)
	test1 OF ... ENDOF		test1 OVER = IF DROP ... ELSE
	test2 OF ... ENDOF		test2 OVER = IF DROP ... ELSE
	testn OF ... ENDOF		testn OVER = IF DROP ... ELSE
	... ( default case )		...
	ENDCASE				DROP THEN [THEN [THEN ...]]

	CASE语句将0推到立即模式的参数栈中,
	这个数字被用来计算当我们到达ENDCASE时需要多少个THEN语句,以便每个IF都有一个匹配的THEN。 
	这个计数是隐式完成的。 
	如果你还记得上面IF的实现,每个IF都会在立即模式栈中推送一个代码地址,
	这些地址是非零的,所以当我们到达ENDCASE时,栈中包含了一些非零的数字,后面是一个零。 
	非零的数量就是IF被调用了多少次,所以我们需要用THEN来匹配它的次数。

	这段代码使用了[COMPILE],这样我们在编译下面的字时,
	就会编译对IF、ELSE、THEN的调用,而不是实际调用它们。

	就像我们所有的控制结构一样,
	它们只在字的定义中起作用,而不是在立即模式下。
)

: CASE IMMEDIATE
	0		( 推0来标记栈底 )
;

: OF IMMEDIATE
	' OVER ,	( 编译OVER )
	' = ,		( 编译 = )
	[COMPILE] IF	( 编译 IF )
	' DROP ,  	( 编译 DROP )
;

: ENDOF IMMEDIATE
	[COMPILE] ELSE	( ENDOF与ELSE相同 )
;

: ENDCASE IMMEDIATE
	' DROP ,	( 编译 DROP )

	( 继续编译THEN,直到我们到达我们的零标记 )
	BEGIN
		?DUP
	WHILE
		[COMPILE] THEN
	REPEAT
;

(
	反编译 ----------------------------------------------------------------------

	CFA>是>CFA的对立面。 它接收一个代码字并试图找到匹配的字典定义。 
(事实上,它对任何进入一个字的指针都有效,而不仅仅是代码字指针,这是做栈跟踪所需要的)。

	在这个FORTH中,这就不那么容易了。 
事实上,我们必须在字典中搜索,因为我们没有一个方便的后置指针(在其他版本的FORTH中经常如此)。 
由于这种搜索,CFA>不应该在对性能要求很高的时候使用,所以它只用于调试工具,如反编译器和打印栈跟踪。

	若未找到匹配,这个字会返回0。
)
: CFA>
	LATEST @	( 从LATEST 字典条目开始 )
	BEGIN
		?DUP		( 当链接指针不为空指针时 )
	WHILE
		2DUP SWAP	( cfa curr curr cfa )
		< IF		( 当前的字典条目 < cfa? )
			NIP		( 将当前的字典条目留在栈上 )
			EXIT
		THEN
		@		( 跟随链接指针返回 )
	REPEAT
	DROP		( 恢复栈 )
	0		( 对不起,没有找到 )
;

(
	SEE反编译一个FORTH字.

	我们搜索该字的字典条目,然后再搜索下一个字(实际上就是编译后的字的结尾)。 
	这就产生了两个指针:

	+---------+---+---+---+---+------------+------------+------------+------------+
	| LINK    | 3 | T | E | N | DOCOL      | LIT        | 10         | EXIT       |
	+---------+---+---+---+---+------------+------------+------------+------------+
	 ^									       ^
	 |									       |
	Start of word							      End of word

	有了这些信息,我们就可以对这个字进行反编译了。 
我们需要识别 "元字",如LIT、LITSTRING、BRANCH等,并分别处理这些字.
)
: SEE
	WORD FIND	( 找到要反编译的字典条目 )

	( 现在我们再次搜索,寻找字典中的下一个字。 
这就给我们提供了我们要反编译的字的长度。 (嗯,大部分情况下是这样)。 )
	HERE @		( 最后一个编译字的结束地址 )
	LATEST @	( word last curr )
	BEGIN
		2 PICK		( word last curr word )
		OVER		( word last curr word curr )
		<>		( word last curr word<>curr? )
	WHILE			( word last curr )
		NIP		( word curr )
		DUP @		( word curr prev (即成为: word last curr) )
	REPEAT

	DROP		( 在这一点上,栈是:字首字尾 )
	SWAP		( 字的末尾,字的开头 )

	( 用:NAME [IMMEDIATE] 开始定义。 )
	':' EMIT SPACE DUP ID. SPACE
	DUP ?IMMEDIATE IF ." IMMEDIATE " THEN

	>DFA		( 获取数据地址,即DOCOL之后的点|字尾数据首 )

	( 现在我们开始反编译,直到我们碰到字尾。 )
	BEGIN		( 末端开始 )
		2DUP >
	WHILE
		DUP @		( 结束时开始的代码字  2022考事业单位e类看什么书)

		CASE
		' LIT OF		( 它是LIT吗? ? )
			4 + DUP @		( 获取下一个字,它是整数常数 )
			.			( 并打印它 )
		ENDOF
		' LITSTRING OF		( 它是LITSTRING吗 ? )
			[ CHAR S ] LITERAL EMIT '"' EMIT SPACE ( 打印 S"<space> )
			4 + DUP @		( 获取长度字 )
			SWAP 4 + SWAP		( 末端 开始+4 长度 )
			2DUP TELL		( 打印该字符串 )
			'"' EMIT SPACE		( 用最后的引号来结束这个字符串 )
			+ ALIGNED		( 末端 开始+4+len,已对齐 )
			4 -			( 因为我们要在下面增加4个 )
		ENDOF
		' 0BRANCH OF		( 是0BRANCH吗 ? )
			." 0BRANCH ( "
			4 + DUP @		( 打印偏移量 )
			.
			." ) "
		ENDOF
		' BRANCH OF		( 是否是BRANCH ? )
			." BRANCH ( "
			4 + DUP @		( 打印偏移量 )
			.
			." ) "
		ENDOF
		' ' OF			( 是 '(TICK)吗 ? )
			[ CHAR ' ] LITERAL EMIT SPACE
			4 + DUP @		( 获取下一个代码字 )
			CFA>			( 并强制将其作为字典条目打印出来 )
			ID. SPACE
		ENDOF
		' EXIT OF		( 是EXIT吗? )
			( 我们希望最后一个字是EXIT,如果是的话,我们就不打印它,因为EXIT通常是由 ; 暗示的。
			 EXIT也可能出现在字的中间,这时需要打印出来。 )
			2DUP			( end start end start )
			4 +			( end start end start+4 )
			<> IF			( end start | 我们还没有到最后 )
				." EXIT "
			THEN
		ENDOF
					( default case: 默认情况: )
			DUP			( 在默认情况下,我们总是需要在使用前DUP一下 )
			CFA>			( 查阅代码字,获取字典中的条目 )
			ID. SPACE		( 并打印它 )
		ENDCASE

		4 +		( end start+4 )
	REPEAT

	';' EMIT CR

	2DROP		( 恢复栈 )
;

(
	执行牌 ----------------------------------------------------------------------

	标准FORTH定义了一个叫做 "执行牌"(或 "xt")的概念,与C语言中的函数指针非常相似。 
我们将执行牌映射到一个代码字地址。

			DOUBLE的执行牌是这个代码字的地址。
						    |
						    V
	+---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
	| LINK    | 6 | D | O | U | B | L | E | 0 | DOCOL      | DUP        | +          | EXIT       |
	+---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
                   len                         pad  codeword					       ^

	有一个用于执行牌的汇编原语,EXECUTE ( xt -- ) ,它可以运行这些牌.

	你可以用>CFA为一个现有的字做一个执行牌,即:WORD [foo] FIND >CFA会把foo的xt推到栈中,其中foo是输入的下一个字。 
所以一个非常慢的运行DOUBLE的方法可能是:

		: DOUBLE DUP + ;
		: SLOW WORD FIND >CFA EXECUTE ;
		5 SLOW DOUBLE . CR	\ 打印 10

	我们还提供了一个更简单、更快速的方法来获取任何字FOO的执行牌:

		['] FOO

	(读者练习:(1)['] FOO 和 ' FOO 之间有什么不同? 
		(2) ' 、 ['] 和 LIT 之间有什么关系?)

	更有用的是定义匿名字 和/或 将xt赋给变量。

	要定义一个匿名字(并把它的xt推到栈上),使用 :NONAME ... ;
	如本例中:

		:NONAME ." anon word was called" CR ;	\ 将xt推到栈上
		DUP EXECUTE EXECUTE			\ 执行了两次Anon字

	栈参数按预期工作:

		:NONAME ." called with parameter " . CR ;
		DUP
		10 SWAP EXECUTE		\ 打印 '用参数10调用'
		20 SWAP EXECUTE		\ 打印 '用参数20调用'

	请注意,上面的代码有一个内存泄漏:匿名字仍然被编译到数据段中,所以即使你失去了对xt的跟踪,这个字仍然继续占用内存。 
	追踪xt从而避免内存泄漏的一个好方法是将其赋给一个CONSTANT、VARIABLE或VALUE:

		0 VALUE ANON
		:NONAME ." anon word was called" CR ; TO ANON
		ANON EXECUTE
		ANON EXECUTE

	 :NONAME 的另一个用途是创建一个可以快速调用的函数数组(想想:快速切换语句)。 
	这个例子是根据ANS FORTH标准改编的:

		10 CELLS ALLOT CONSTANT CMD-TABLE
		: SET-CMD CELLS CMD-TABLE + ! ;
		: CALL-CMD CELLS CMD-TABLE + @ EXECUTE ;

		:NONAME ." alternate 0 was called" CR ;	 0 SET-CMD
		:NONAME ." alternate 1 was called" CR ;	 1 SET-CMD
			\ etc...
		:NONAME ." alternate 9 was called" CR ;	 9 SET-CMD

		0 CALL-CMD
		1 CALL-CMD
)
: :NONAME
	0 0 CREATE	( 创建一个没有名称的字--我们需要一个字典头,因为 ; 期望它。 )
	HERE @		( 当前的HERE值是代码字的地址,即xt )
	DOCOL ,		( 编译DOCOL (the 代码字) )
	]		( 进入编译模式 )
;

: ['] IMMEDIATE
	' LIT ,		( 编译 LIT )
;

(
	异常 ----------------------------------------------------------------------

	令人惊讶的是,异常可以直接在FORTH中实现,事实上相当容易。

	一般的用法如下:

		: FOO ( n -- ) THROW ;

		: TEST-EXCEPTIONS
			25 ['] FOO CATCH	\ 执行25个FOO,捕捉任何异常
			?DUP IF
				." called FOO and it threw exception number: "
				. CR
				DROP		\ 我们必须放弃FOO(25)的论证。
			THEN
		;
		\ 打印:调用FOO,它抛出了异常号:25

	CATCH运行一个执行牌并检测它是否抛出任何异常。 
	CATCH的栈签名是相当复杂的:

		( a_n-1 ... a_1 a_0 xt -- r_m-1 ... r_1 r_0 0 )		如果xt没有抛出一个异常
		( a_n-1 ... a_1 a_0 xt -- ?_n-1 ... ?_1 ?_0 e )		如果xt DID抛出异常'e'的话

	其中 a_i 和 r_i 是xt被EXECUTEd之前和之后的(任意数量的)实参和返回栈内容。 
请注意,特别是在抛出异常的情况下,栈指针被恢复,所以栈上有n个_something_,位于实参 a_i 原来所在的位置。 
我们并不能保证栈上有什么--也许是原来的实参,也许是其他乱七八糟的东西--这主要取决于被执行的字的实现。

	THROW、ABORT和其他一些抛出的异常。

	异常数字是非零的整数。 
按照惯例,正数可用于特定应用程序的异常,而负数则具有ANS FORTH标准中定义的某些含义。 
(例如,-1是由ABORT抛出的异常)。

	0 THROW不做任何事情。 这是THROW的栈签名:

		( 0 -- )
		( * e -- ?_n-1 ... ?_1 ?_0 e )	栈被恢复到相应的CATCH的状态。

	实现取决于CATCH和THROW的定义以及它们之间共享的状态。

	到此为止,返回栈仅仅包括一个返回地址的列表,返回栈的顶部是返回地址,在当前字退出时,我们将在这里继续执行。 
	然而CATCH将在返回栈上推送一个更复杂的 "异常栈帧"。 
	异常栈帧记录了一些关于调用CATCH时的执行状态的东西。

	当被调用时,THROW沿着返回栈向上走(这个过程被称为 "松开"),直到它找到异常栈帧。 
然后它使用异常栈帧中的数据来恢复状态,允许在匹配的CATCH之后继续执行。 
(如果它松开栈,没有找到异常栈帧,那么它就会打印一条信息,然后返回到提示符,这也是所谓的 "未捕获的异常 "的正常行为)。

	这就是异常栈帧的样子。 
(按照惯例,返回栈显示为从高到低的内存地址向下增长)。

		+------------------------------+
		| 来自CATCH的返回地址    		|   注意,当调用CATCH时,这已经在返回栈上了。
		|                              |   
		+------------------------------+
		| 原始参数栈指针    			|
		|                      		   |
		+------------------------------+  ^
		|    异常栈标记        			|  |
		| (EXCEPTION-MARKER)           |  |   通过THROW松开栈的方向。
		+------------------------------+  |   
						  |
						  |

	EXCEPTION-MARKER标志着这个条目是一个异常栈帧,而不是一个普通的返回地址,这就是THROW在展开栈时 "注意到 "的东西。 
(如果你想实现更先进的异常,比如 TRY...WITH,那么你需要使用不同的标记值,如果你想让新旧异常栈帧的布局共存的话)。

	如果执行的字没有抛出一个异常,会发生什么? 它最终会返回并调用EXCEPTION-MARKER,所以EXCEPTION-MARKER最好做一些合理的事情,而不需要我们修改EXIT。 
这很好地给了我们一个合适的EXCEPTION-MARKER的定义,即一个只是丢弃栈帧并自己返回的函数(因此从原来的CATCH中 "返回")。

	从中可以看出,在FORTH中,异常是一种相对轻量级的机制。
)
: EXCEPTION-MARKER
	RDROP			( 丢掉原来的参数栈指针 )
	0			( 没有异常,这就是常规的返回路径。 )
;

: CATCH		( xt -- exn? )
	DSP@ 4+ >R		( 在返回栈上保存参数栈指针(由于xt的存在,+4) )
	' EXCEPTION-MARKER 4+	( 在EXCEPTION-MARKER内推RDROP的地址 ... )
	>R			( ...到返回栈中,所以它的作用就像一个返回地址一样 )
	EXECUTE			( 执行嵌套函数 )
;

: THROW		( n -- )
	?DUP IF			( 只有在异常代码<>0时才采取行动 )
		RSP@ 			( 获取返回栈指针 )
		BEGIN
			DUP R0 4- <		( RSP < R0 )
		WHILE
			DUP @			( 获取返回栈的条目 )
			' EXCEPTION-MARKER 4+ = IF	( 在返回栈中发现了EXCEPTION-MARKER。 )
				4+			( 跳过返回栈上的EXCEPTION-MARKER。 )
				RSP!			( 恢复返回栈的指针 )

				( 恢复参数栈. )
				DUP DUP DUP		( 保留一些工作空间,以便这个字的栈不与被恢复的栈部分重合。 )
				R>			( 获取保存的参数栈指针 | n dsp )
				4-			( 在栈中预留空间以存储n )
				SWAP OVER		( dsp n dsp )
				!			( 在栈上写n )
				DSP! EXIT		( 恢复参数栈指针,立即退出 )
			THEN
			4+
		REPEAT

		( 没有匹配的捕获--打印一个消息并重新启动INTERPRETER。 )
		DROP

		CASE
		0 1- OF	( 中止 )
			." ABORTED" CR
		ENDOF
			( default case 默认情况 )
			." UNCAUGHT THROW "
			DUP . CR
		ENDCASE
		QUIT
	THEN
;

: ABORT		( -- )
	0 1- THROW
;

( 通过在返回栈上行走来打印一个栈跟踪。 )
: PRINT-STACK-TRACE
	RSP@				( 从该函数的调用者开始 )
	BEGIN
		DUP R0 4- <		( RSP < R0 )
	WHILE
		DUP @			( 获取返回栈的条目 )
		CASE
		' EXCEPTION-MARKER 4+ OF	( 是异常栈帧吗? )
			." CATCH ( DSP="
			4+ DUP @ U.		( 打印已保存的栈指针 )
			." ) "
		ENDOF
						( default case )
			DUP
			CFA>			( 查找代码字,获取字典条目 )
			?DUP IF			( 并打印它 )
				2DUP			( dea addr dea )
				ID.			( 打印字典条目中的字 )
				[ CHAR + ] LITERAL EMIT
				SWAP >DFA 4+ - .	( 打印偏移 )
			THEN
		ENDCASE
		4+			( 向上移动栈 )
	REPEAT
	DROP
	CR
;

(
	C 字符串 ----------------------------------------------------------------------

	FORTH字符串由栈或内存中保存的起始地址和长度表示。

	大多数FORTH都不处理C语言字符串,
	但我们需要它们来访问Linux内核留在栈中的进程参数和环境,并进行一些系统调用。

	操作	      输入		 输出		  FORTH字	   说明
	----------------------------------------------------------------------

	Create FORTH string		addr len	S" ..."

	Create C string			c-addr		Z" ..."

	C -> FORTH	c-addr		addr len	DUP STRLEN

	FORTH -> C	addr len	c-addr		CSTRING		分配在一个临时缓冲区中,所以应该立即消耗/复制。FORTH字符串不应包含NULs。
									
									

	例如,DUP STRLEN TELL打印出一个C语言字符串。
)

(
	Z" ... "与S" ... "类似,只是该字符串以ASCII NUL字符终结。

	为了使它更像一个C语言的字符串,在运行时Z "只是把字符串的地址留在栈上(而不是像S "那样的地址和长度)。 
为了实现这一点,我们需要在字符串中添加额外的NUL,并在之后添加一条DROP指令。 
除此以外,这个实现只是一个修改过的S"。
)
: Z" IMMEDIATE
	STATE @ IF	( 编译吗? )
		' LITSTRING ,	( 编译LITSTRING )
		HERE @		( 在栈中保存长度字的地址 )
		0 ,		( 假动作长度 - 我们还不知道它是什么 )
		BEGIN
			KEY 		( 获取字符串的下一个字符 )
			DUP '"' <>
		WHILE
			HERE @ C!	( 在编译后的镜像中存储该字符 )
			1 HERE +!	( 将HERE指针递增1个字节 )
		REPEAT
		0 HERE @ C!	( 增加ASCII NUL字节 )
		1 HERE +!
		DROP		( 丢掉末尾的双引号字符 )
		DUP		( 获取长度字的保存地址 )
		HERE @ SWAP -	( 计算出长度 )
		4-		( 减去4(因为我们是从长度字开始测量的)。 )
		SWAP !		( 并回填长度位置 )
		ALIGN		( 对于剩余的代码,四舍五入到下一个4字节的倍数。 )
		' DROP ,	( 编译DROP(丢掉长度) )
	ELSE		( 立即模式 )
		HERE @		( 获取临时空间的起始地址 )
		BEGIN
			KEY
			DUP '"' <>
		WHILE
			OVER C!		( 保存下一个字符 )
			1+		( 递增地址 )
		REPEAT
		DROP		( 丢掉最后的 " 字符 )
		0 SWAP C!	( 存储最后的ASCII NUL )
		HERE @		( 推开始地址 )
	THEN
;

: STRLEN 	( str -- len )
	DUP		( 保存起始地址 )
	BEGIN
		DUP C@ 0<>	( 找到零字节? )
	WHILE
		1+
	REPEAT

	SWAP -		( 计算出长度 )
;

: CSTRING	( addr len -- c-addr )
	SWAP OVER	( len saddr len )
	HERE @ SWAP	( len saddr daddr len )
	CMOVE		( len )

	HERE @ +	( daddr+len )
	0 SWAP C!	( 存储终结的NUL字符 )

	HERE @ 		( 推开始地址 )
;

(
	环境 ----------------------------------------------------------------------

	Linux将进程实参和环境在栈上提供给我们。

	当我们启动时,栈顶的指针被早期的汇编代码保存在FORTH变量S0中,从这个指针开始,我们可以读出命令行参数和环境。

	从S0开始,S0本身指向argc(命令行实参的数量)。

	S0+4指向argv[0],S0+8指向argv[1]等等,直到argv[argc-1]。

	argv[argc]是一个NULL指针。

	之后,栈包含环境变量,一组指向NAME=VALUE形式的字符串的指针,直到我们得到另一个NULL指针。

	我们定义的第一个字,ARGC,推动了命令行实参的数量(注意,与C语言的argc一样,这包括命令的名称)。
)
: ARGC
	S0 @ @
;

(
	n ARGV获取第n个命令行实参。

	例如,如果要打印命令名称,你应该这样做:
		0 ARGV TELL CR
)
: ARGV ( n -- str u )
	1+ CELLS S0 @ +	( 获取argv[n]条目的地址 )
	@		( 获取字符串的地址 )
	DUP STRLEN	( 并获取其长度/将其变成FORTH字符串 )
;

(
	ENVIRON返回第一个环境字符串的地址。 字符串的列表以一个NULL指针结束。

	例如,要打印环境中的第一个字符串,你可以这样做:
		ENVIRON @ DUP STRLEN TELL
)
: ENVIRON	( -- addr )
	ARGC		( 栈上要跳过的命令行形参的数目 )
	2 +		( 跳过命令行计数和命令行args后的NULL指针 )
	CELLS		( 转换为偏移量 )
	S0 @ +		( 加到基础栈地址 )
;

(
	系统调用和文件  ----------------------------------------------------------------------

	与系统调用有关的杂项字,以及对文件的标准访问。
)
( BYE通过调用Linux exit(2)系统调用而退出。)
: BYE		( -- )
	0		( 返回代码 (0) )
	SYS_EXIT	( 系统调用号 )
	SYSCALL1
;

(
	UNUSED返回用户内存(数据段)中剩余的cells 数。

	在我们的实现中,我们将使用Linux的brk(2)系统调用来找出数据段的末端,并从其中减去HERE。
)
: GET-BRK	( -- brkpoint )
	0 SYS_BRK SYSCALL1	( 调用 brk(0) )
;

: UNUSED	( -- n )
	GET-BRK		( 根据内核获取数据段的末端 )
	HERE @		( 获取数据段的当前位置 )
	-
	4 /		( 返回cells的数量 )
;

(
	MORECORE将数据段递增指定数量的(4字节)cells。

	NB. 要求的单元格数量通常应该是1024的倍数。 
原因是Linux不能将数据段扩展到小于一个页面(4096字节或1024 cells)。

	这个FORTH不会 "按需 "自动增加数据段的大小(即当使用Ⅳ(COMMA)、ALLOT、CREATE等)。 
相反,程序员需要意识到一个大的分配会占用多少空间,检查UNUSED,并在必要时调用MORECORE。 
一个简单的编程练习是改变数据段的实现,以便在程序需要更多内存时自动调用MORECORE。
)
: BRK		( brkpoint -- )
	SYS_BRK SYSCALL1
;

: MORECORE	( cells -- )
	CELLS GET-BRK + BRK
;

(
	标准FORTH提供了一些简单的文件访问原语,我们在Linux系统调用的基础上对其进行建模。

	主要的复杂问题是将FORTH字符串(地址和长度)转换为Linux内核的C字符串。

	注意在这个实现中没有缓冲。
)
: R/O ( -- fam ) O_RDONLY ;
: R/W ( -- fam ) O_RDWR ;

: OPEN-FILE	( addr u fam -- fd 0 (若成功) | c-addr u fam -- fd errno (如果有错误) )
	-ROT		( fam addr u )
	CSTRING		( fam cstring )
	SYS_OPEN SYSCALL2 ( open (filename, flags) )
	DUP		( fd fd )
	DUP 0< IF	( errno? )
		NEGATE		( fd errno )
	ELSE
		DROP 0		( fd 0 )
	THEN
;

: CREATE-FILE	( addr u fam -- fd 0 (若成功) | c-addr u fam -- fd errno (如果有错误) )
	O_CREAT OR
	O_TRUNC OR
	-ROT		( fam addr u )
	CSTRING		( fam cstring )
	420 -ROT	( 0644 fam cstring )
	SYS_OPEN SYSCALL3 ( open (filename, flags|O_TRUNC|O_CREAT, 0644) )
	DUP		( fd fd )
	DUP 0< IF	( errno? )
		NEGATE		( fd errno )
	ELSE
		DROP 0		( fd 0 )
	THEN
;

: CLOSE-FILE	( fd -- 0 (若成功) | fd -- errno (如果有错误) )
	SYS_CLOSE SYSCALL1
	NEGATE
;

: READ-FILE	( addr u fd -- u2 0 (若成功) | addr u fd -- 0 0 (if EOF) | addr u fd -- u2 errno (if error) )
	>R SWAP R>	( u addr fd )
	SYS_READ SYSCALL3

	DUP		( u2 u2 )
	DUP 0< IF	( errno? )
		NEGATE		( u2 errno )
	ELSE
		DROP 0		( u2 0 )
	THEN
;

(
	PERROR打印一个错误消息,类似于C的perror(3),但我们没有大量的字符串可用,所以我们能做的就是打印错误消息。
)
: PERROR	( errno addr u -- )
	TELL
	':' EMIT SPACE
	." ERRNO="
	. CR
;

(
	汇编代码 ----------------------------------------------------------------------

	这只是一个简单的汇编程序的概要,允许你用汇编语言编写FORTH原语。

	汇编原语以正常方式开始 ': NAME',但以 ;CODE 结束。
	 ;CODE 更新了头,因此代码字不是DOCOL,而是指向汇编后的代码(在字的DFA部分)。

	我们提供了一个方便的宏NEXT(你猜到了它的作用)。 
但是你不需要使用它,因为 ;CODE 会在你的字的末尾放一个NEXT。

	其余的由一些立即字组成,这些字扩展为机器码追加在字的定义上。 
i386的汇编空间只有很小的一部分被覆盖,只够编写下面几个汇编原语。
)

HEX 十六进制

( 相当于NEXT宏 )
: NEXT IMMEDIATE AD C, FF C, 20 C, ;

: ;CODE IMMEDIATE
	[COMPILE] NEXT		( 用NEXT宏来结束这个字 )
	ALIGN			( 机器码是以字节为单位进行汇编的,所以不一定在末端对齐。 )
	LATEST @ DUP
	HIDDEN			( 解除隐藏字 )
	DUP >DFA SWAP >CFA !	( 改变代码字以指向数据区 )
	[COMPILE] [		( 回到立即模式 )
;

( i386的寄存器 )
: EAX IMMEDIATE 0 ;
: ECX IMMEDIATE 1 ;
: EDX IMMEDIATE 2 ;
: EBX IMMEDIATE 3 ;
: ESP IMMEDIATE 4 ;
: EBP IMMEDIATE 5 ;
: ESI IMMEDIATE 6 ;
: EDI IMMEDIATE 7 ;

( i386栈指令 )
: PUSH IMMEDIATE 50 + C, ;
: POP IMMEDIATE 58 + C, ;

( RDTSC指令 )
: RDTSC IMMEDIATE 0F C, 31 C, ;

DECIMAL 十进制

(
	RDTSC是一个汇编器原语,它读取奔腾的时间戳计数器(一个非常细粒度的计数器,计算处理器的时钟周期)。 
因为TSC是64位宽,我们必须把它推到栈的两个槽中。
)
: RDTSC		( -- lsb msb )
	RDTSC		( 将结果写入%edx:%eax中 )
	EAX PUSH	( push lsb )
	EDX PUSH	( push msb )
;CODE

(
	INLINE可用于将一个汇编原语内联到当前(汇编)字中。

	例如:

		: 2DROP INLINE DROP INLINE DROP ;CODE

	将建立一个高效的汇编字2DROP,其中包含DROP的内联汇编代码,后面是DROP(例如,在这种情况下,两个'pop %eax'指令)。

	另一个例子。 考虑一下这个普通的FORTH定义:

		: C@++ ( addr -- addr+1 byte ) DUP 1+ SWAP C@ ;

	(它等同于C操作'*p++',其中p是char的指针)。 
如果我们注意到所有用于定义C@++的字实际上都是汇编器的原语,那么我们可以写一个更快的(但等价的)定义,比如说:

		: C@++ INLINE DUP INLINE 1+ INLINE SWAP INLINE C@ ;CODE

	值得注意的一点是,这种 "串联" 式的编程风格允许你可移植地编写汇编字。 
上述定义适用于任何CPU架构。

	要成功使用INLINE,必须满足几个条件:

	(1) 你目前必须定义一个汇编字(即 : ... ;CODE)。

	(2) 你所内联的字必须是已知的汇编字。 
如果你试图内联一个FORTH字,你会得到一个错误消息。

	(3) 汇编原语必须是与位置无关的代码,并且必须以一个NEXT宏结束。

	读者的练习:(a) 对INLINE进行泛化,使其在构建FORTH字时能够内联FORTH。
	(b) 进一步概括INLINE,使它在试图将FORTH内联到汇编程序中时能做一些合理的事情,反之亦然。

	INLINE的实现是非常简单的。 我们在字典中找到这个字,
	检查它是否是一个汇编字,然后逐个字节地把它复制到当前的定义中,直到我们到达NEXT宏(它没有被复制)。
)
HEX
: =NEXT		( addr -- next? )
	   DUP C@ AD <> IF DROP FALSE EXIT THEN
	1+ DUP C@ FF <> IF DROP FALSE EXIT THEN
	1+     C@ 20 <> IF      FALSE EXIT THEN
	TRUE
;

DECIMAL

( (INLINE)是低级别的内联函数。 )
: (INLINE)	( cfa -- )
	@			( 记住代码字指向的代码 )
	BEGIN			( 拷贝字节,直到我们碰到NEXT宏 )
		DUP =NEXT NOT
	WHILE
		DUP C@ C,
		1+
	REPEAT
	DROP
;

: INLINE IMMEDIATE
	WORD FIND		( 在字典里找到这个字 )
	>CFA			( 代码字 )

	DUP @ DOCOL = IF	( 检查代码字 <> DOCOL(即-不是FORTH字) )
		." Cannot INLINE FORTH words" CR ABORT
	THEN

	(INLINE)
;

HIDE =NEXT

(
	注意 ----------------------------------------------------------------------

	DOES>是不可能用这个FORTH实现的,因为我们没有一个单独的数据指针。
)

(
	欢迎消息 ----------------------------------------------------------------------

	打印版本和OK提示。
)

: WELCOME
	S" TEST-MODE" FIND NOT IF
		." JONESFORTH VERSION " VERSION . CR
		UNUSED . ." CELLS REMAINING" CR
		." OK "
	THEN
;

WELCOME
HIDE WELCOME

 

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.sydw.cn/sydwksm/64201.html