##第24章 编译器主体
###第1节 输入模块
把一个复杂的大问题分解成一个个简单的小问题,这个复杂的大问题也就容易解决了。这是软件工程里常见的思想,但却不是每个软件工程都能做得到的。
在forth里有两种开发方式,一种是自顶向下,另一种是自底向上。当你对全局把握得很清晰时,你可以使用自顶向下的开发方式;当你对全局把握得不是那么清晰时,你可以选择自底向上的方式,把肯定会用到的词先写出来。通常我们是两者兼用,从两头往中间编写。
forth编译器的全局结构
----------------------------------------------←不是就报错
↓ ↑ ↑
输入命令→冒号?→是则编译模式→定义新词名 | |
. ↓ ↓ | |
. 否则解释模式→ 分解单词→句尾?→是则解释或编译 |
. ↑ ↓ |
. | 否则搜索词典→没找到,是否数字?
. | ↓ ↓
. ---找到就编译单词 是则编译数值
. ↑ |
. ------------------------
$colon 'word',word_label
dw word1_label, word2_label, word3_label, exit
在汇编的源文件里是用上面这种方式写colon字,但是这样写起来比较麻烦,所以在下面我只用forth的语法来写,写完之后再转换格式。但是写的时候一定要记住,你是在写一个地址数值,甚至还会写出push push 这样的语句,其实就是dw push@,push@(push一个标签"push"的内存地址值)。
: cmd systeminfo do beginSign input doInput loop ;
这句子看起来真是别扭。好吧,我承认我不是一个优秀的鸟语者。所以,我决定用中文来写forth。
: 命令行 do 提示符 输入 编译执行 loop ;
嗯,现在看起来舒服多了,还是母语顺眼!
有人就问了:“用中文编程不会报错吗?”
这就涉及到ANSI编码的内容了,下面COPY一段:
ansi编码 为使计算机支持更多语言,通常使用 0x800xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。 当然对于ANSI编码而言,0x000x7F之间的字符,依旧是1个字节代表1个字符。这一点是ASNI编码与Unicode编码之间最大也最明显的区别。比如“A君是第131号”,在ANSI编码中,占用12个字节,而在Unicode编码中,占用16个字节。因为A和1、3、1这4个字符,在ANSI编码中只各占1位,而在Unicode编码中,是需要各占2位的。 不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
由此可知,ASCII码和ANSI码是两个没有交集的字码表,对计算机来说,它们也不过是0和1的数字而已,而汇编器中的关键字不属于ANSI码,不会对属于ANSI码的中文进行操作,所以我才能在这里用中文编程。大多数程序员之所以不用中文编程,一方面是传统习惯的问题,另一方面嫌中文输入慢,却忽略了易读性。
不过,在编译好的forth汇编器里,因为没有中文字库和中文输入法的支持,所以你无法输入并看到中文。
如果你是一名优秀的鸟语者, 不想用中文编程, 那就自己替换英文名吧
首先补上前一章的内容
;-------------------------code.inc-----------------------------
; if ( n -- )
$code 'if',if@
or tos,tos
pop tos ;mov push pop 不会影响到标志位
jnz if@1 ;如果不是0,就顺序执行
add si,celll*2 ;否则跳过两个词
if@1:
$next
; if0 ( n -- ) 与if相反
$code 'if0',if0@
or tos,tos
pop tos ;mov push pop 不会影响到标志位
jz if@1 ;如果是0,就顺序执行
add si,celll*2
$next
; endif ( n -- ) 空指令,只是用来填if的缺
$code 'endif',endif@
$next
;else ( - ) 直接跳过一个词,后面也无需跟一个endif
$code 'else',else@
add si,celll
$next
; next ( - ) ( addr num -RS- | addr num' )
$code 'next',next@
dec word [bp] ;计数
jz next@0 ;如果为0则退出循环
mov si,[bp+celll] ;否则循环
$next
next@0:
add bp,celll*2
$next
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
以及一些必要的算术函数,暂时不用深究
-------------------------colon.txt-----------------------------
( w w -- )
: 2drop drops drop ;
( n1 n2 -- t|0 )
: u> swap u< ;
( n1 n2 -- t|0 )
: > swap < ;
( n-- -n )
: negate not ++ ;
( dn-- -dn )
: dnegate not >r not push 1 um+ r> + ;
( n1 n2 -- t|0 )
: isDifSign xor isNegate ;
( n--|n| )
: abs dup isNegate if negate endif ;
( dl dh -- |dl dh| )
: dabs dup isNegate if dnegate endif ;
( n n -- ul uh )
: m* 2dup isDifSign >r abs swap abs um* r> if dnegate endif ;
( n n -- n )
: * m* drop ;
( dl dh n -- q m | ff ff )
: m/mod 2dup isDifSign >r abs >r dabs
r> um/mod r> if0 exit endif
>r negate r> ;
( n n -- q m )
: /mod over negate? swap m/mod ;
( n n -- m )
: mod /mod drops ;
( n n -- q )
: / /mod drop ;
然后,正式开始编写myforth,新建一个colon.txt的文本文件,先把forth代码写在里面,然后再转换格式。一般程序语言的写法是回车换行之后往下写,从顶而下的写法正好相反,是向上换行写,等到上面写无可写的时候,再向下写。为了方便阅读,这里是按思维顺序写的.
注意事项:最好有一款方便混合输入中英文的输入法, 而且切换到中文输入法的时候,要使用半角的标点符号,小心转化格式的时候不认中文的:和;
-------------------------colon.txt-----------------------------
( -RS- addr )
: do r@ >r ;
( addr -RS- addr )
: loop dropr r@ >r ;
( addrUp addrLoop -RS- )
: break dropr dropr ;
( n -- n n | 0 )
: dup? dup if dup endif ;
( n -- ) ( -RS- addr n )
: for dup? if0 dropr exit r@ swap >r >r ;
: 同义词 sameAs sameAs ;
: 如果是 同义词 if ;
: 如果不是 同义词 if0 ;
: 否则 同义词 else ;
: 完毕 ;
------------------------------------------------------------------
: 命令行 do 提示符 输入命令 编译执行 cr loop ;
: 提示符
: 输入命令
: 编译执行
: cr
------------------------------------------------------------------
: cr push $13 push $10 emit emit ;
cr是换行的意思,在MS-DOS里换行符是ASCII码的13和10的组合,而在Linux里只是一个10。另外, 为了以后的转换方便,所有的数字在前面必须加上一个#号,以表示这是一个数字。
------------------------------------------------------------------
: 提示符 ." ">>>" ;
: ."
------------------------------------------------------------------
(
">>>"开头的引号代表里面的内容编译成一个紧接着上个词的字符串 ,如:
$colon '提示符',提示符@
dw J"@
db '>>>',0
dw exit@
)
------------------------------------------------------------------
( 提取字串的头地址,打印,然后返回到字串之后的词 )
: ." r> .str >r ;
( 字串首地址 -- 字串地址尾0外 )
: .str
------------------------------------------------------------------
( 字串首地址 -- 字串地址尾0外 )
: .str do 读取字符 ++s
dup if0 drop break
emit loop ;
( addr -- addr c)
: 读取字符 dup c@ ;
------------------------------------------------------------------
: 输入命令 缓冲区定位 do key
换行符? 如果是 结束输入 break
退格符? 如果是 退格 loop
双引号? 如果是 双引号匹配 loop
dup emit 储存字符
loop ;
: 缓冲区定位
: 换行符?
: 结束输入
: 退格符?
: 退格
: 双引号?
: 双引号匹配
------------------------------------------------------------------
( -- addr )
: 缓冲区定位 词典目录尾 push #512 + ;
: 词典目录尾 push _coreEnd ;
------------------------------------------------------------------
( c -- T | c F )
: 换行符? CR? 如果是 TRUE exit
LF? ;
( -- T )
: TRUE push #0 not ;
( -- F )
: FALSE push #0 ;
( c -- T | c F )
: CR? push #13 对比字符 ;
( c -- T | c F )
: LF? push #10 对比字符 ;
( c1 c2 -- T | c1 F )
: 对比字符 over == if setT else FALSE ;
------------------------------------------------------------------
( addr -- )
: 结束输入 标记词条尾 cr ;
( addr -- ) ( 结尾加上" ;",0 )
: 标记词条尾 push #32 储存字符
push #59 储存字符
push #0 swap c! ;
( addr c -- addr )
: 储存字符 over c! ++ ;
------------------------------------------------------------------
( c -- T | c F )
: 退格符? push #8 对比字符 ;
------------------------------------------------------------------
( 退格符只是向左移动一格,并没有消去那一格显示的字符,所以需要打印一次空格 )
( addr -- addr | addr-- )
: 退格 缓冲区内? 如果不是 exit endif
push #8 dup emit space emit -- ;
( addr -- addr T | addr F )
: 缓冲区内? 缓冲区定位 over u< ;
: space push #32 emit ;
------------------------------------------------------------------
( c -- T | c F )
: 双引号? push #34 对比字符 ;
------------------------------------------------------------------
( addr c -- addr+3 )
: 双引号匹配 push #34 显示双引号 储存双引号 ;
( c -- c )
: 显示双引号 dup dup emit space emit ;
( addr c -- addr+3 )
: 储存双引号 >r
r@ 储存字符
push #32 储存字符
r> 储存字符 ;
------------------------------------------------------------------
输入模块到此结束, 现在就可以开始进行测试了, 不过一般的汇编调试器对此无用,因为DTC编码方式会让IP指令指针一直在code词里打转, 所以必须自己写测试代码, 根据运行的结果推测问题出在哪里
------------------------------------------------------------------
: 测试 输入命令 缓冲区定位 .str _deadloop ;
------------------------------------------------------------------
-------------------------myforth.asm--------------------------------
...
...
mov ax,测试@
jmp ax
_deadloop@:
jmp $
include 'include\code.inc'
include 'include\colon.inc'
dw _link
_coreEnd@:
------------------------------------------------------------------
这将显示你刚刚输入进去的字符串,看看跟你所设想的是否一致, 如果不一致, 逐步将适当的测试词插入各个词里再次编译运行