Forth 语言简明教程 |
作者 Richard E. Haskell |
| 编译 赵宇 张文翠 |
第七课 CODE 字和 DOS I/O
7.1 CODE 字
当我们需要最大的执行速度或者需要直接访问计算机硬件时,可以用汇编语言来定义 Forth 字。这需要使用 Forth 字 CODE 来完成。 CODE 的一般格式是:
CODE <name>
<assembly commands>
<return command>
END-CODE
字 CODE 代替冒号定义的冒号,并建立一个 Forth 字的头,END-CODE 代替分号结束 CODE 字的定义。
<assembly commands> 可以用 POSTFIX (后缀)也可以用 PREFIX 前缀格式来编写。我们建议使用 PREFIX 前缀格式,这样汇编语言就和标准的 8086/8088 汇编语言相似了,于是在 CODE 编译之前,需要给出字 PREFIX 。
<return command> 可以是下面这些指令中的任何一个:
NEXT JMP >NEXT ( jumps to the inner interpreter >NEXT )
1PUSH PUSH AX
JMP >NEXT ( pushes ax on the stack and jumps to >NEXT )
2PUSH PUSH DX
PUSH AX ( pushes dx and ax on the stack
JMP >NEXT and then jumps to >NEXT )
调试 CODE 字时可以使用 8088 Tutor monitor, 它包含在本教程中。 Tutor monitor 使用 8086 汇编语言,学习 8088/8086 汇编语言的书可以参看
"IBM PC - 8088 Assembly Language Programming" by Richard E. Haskell.
作为一个使用 Tutor monitor 反汇编和单步执行 CODE 字的例子,可以用 F-PC 3.5 提供的字 CMOVE ,它从地址 <source> 移动 <count> 字节到地址 <dest>,并假设状态寄存器的方向标志是 0 (通过执行 CLD 指令) 所以字符串原语 MOVSB 将自动增量 SI 和 DI.
CODE CMOVE ( source dest count -- )
MOV BX, SI \ save SI (IP)
MOV AX, DS \ copy DS for setting ES
POP CX \ cx = count
POP DI \ di = destination address
POP SI \ si = source address
MOV DX, ES \ save es in dx
MOV ES, AX \ point es to code segment
REPNZ \ repeat until count is zero
MOVSB \ copy DS:SI to ES:DI
MOV SI, BX \ restore si
MOV ES, DX \ restore es
NEXT \ done, jmp to >NEXT
END-CODE
当你装入这个代码后, 16 进制值 11 22 33 44 55 在偏移地址在source.addr的代码段,代码段的实际值由 Forth 字 ?CS: 给出,使用字 show.addrs 可以打印到屏幕上。
在偏移量dest.addr" 地址处保留 5 个字节的空间,当你打入字 show.addrs 后,偏移地址 source.addr, dest.addr, 栈顶元素, CMOCE 的 CFA 也被印到屏幕上。
HEX
CREATE source.addr 11 C, 22 C, 33 C, 44 C, 55 C,
CREATE dest.addr 5 ALLOT
5 CONSTANT #bytes
: test ( -- )
source.addr dest.addr #bytes CMOVE ;
: show.addrs ( -- )
HEX
CR ." code segment = " ?cs: u.
CR ." source addr = " source.addr u.
CR ." dest addr = " dest.addr u.
CR ." top of stack = " SP0 @ U.
CR ." address of CMOVE = " [ ' CMOVE ] LITERAL U.
CR DECIMAL ;
字 [, ] 和 LITERAL 将在第九课讨论。
假设被 "show.addrs" 打印的值如下
code segment = 1091
source addr = 74E0
dest addr = 74E8
top of stack = FFE2
address of CMOVE = 477
你的值可能不同,如果不同,则在下面的练习中你应该使用对应的实际值。
Type debug test.
Type HEX
Type test.
单步通过前三个字,它们将打印下面的堆栈值:
74E0 74E8 5
Press F to go to Forth.
Type SYS TUTOR – 将执行 TUTOR 程序
通过 TUTOR 存储器显示
Type >S1091 to display the code segment.
Type /GS1091 to display the data segment = code segment.
Type /GOFEDC to display the stack starting at the top of the
stack (FEE2) minus 6 in the data segment region. The
value 5 (05 00) should be on top of the stack, followed
by the "source addr" 74E0 (E0 74) and the "dest addr"
74E8 (E8 74).
Type /GO74E0 to display the "source addr" in the data segment.
Note that 11 22 33 44 55 is displayed.
Type >O477 to go to the start of the CMOVE code.
再次按 F1 单步执行前两个指令。注意 SI 的值被移到 BX 而 DS 的值被移到 AX 。下一个指令是 POP CX ,它假设从栈顶弹出了 #bytes (5) 值到 CX 。然而, Tutor 的堆栈指针和堆栈段寄存器并没有指向这些值,我们实际看它们在 1091:FEDC. 你可以改变 SS 和 SP 的值,通过打入 /RSS1091 使堆栈段和代码段相同,打入 /RPSFEDC 使堆栈指针等于栈顶 (FFE2) 减 6 。
接着按 F1 执行 POP CX,不过,你又会遇到一个问题,这就是就如何回到 F-PC 。当你退出 Tutor 时你也许退到了 DOS ,或者也可能计算机挂起了。一个变通的办法是用手工方法装入适当的值 5 到 CX 。输入 /RGC5 ,然后按右光标键跳过指令 POP CX 。
使用同样方法跳过指令 POP DI,通过手工输入 /RID74E8 装入dest addr 。
使用同样方法跳过指令 POP SI,通过手工输入 /RIS74E0 装入source addr。
你可以按两次 F1 执行下面两个指令。
你现在位于 REP 指令,按 F1 。注意到值 11 从数据段地址 74E0 复制到扩展段(它实际上与数据段一致)地址 74E8 ,并且 SI 和 DI 都增加了1. 这是指令 MOVSB 的工作 – 它也只做这些。同时 CX 从 5 减量到 4 。
再按 F1 。注意 22 从 SI 所指示的数据段位置( (74E1) 移动到 DI 所指示的扩展段位置 (74E9) , CX 的值 减量到 3
按 F1 三次则值 33 44 和 55 被移动,注意当 CX 为 0 时, REP 循环终止,下一条指令准备执行。
按 F1 两次,执行下面两条指令。下面的指令是一个 JMP 指令,它跳转到 >NEXT.
要退出 TUTOR, 批入 /QD. 这会返回到你在 Forth 中你打入 sys tutor 命令的地方。打入 <Enter> 返回到调试模式,打一个空格键你就可以回到 Forth 。
Forth 字 CMOVE> ( source dest count -- ) 与 CMOVE 类似,差异是字节按相反的方向移动。也就是说,最高地址的字节先移动。在向上移动字符串时这个功能很有用,因为字符串可能重叠,如果这时使用CMOVE则可能导致源串还没有移动之前就已经被破坏了。
7.2 CODE 条件
当我们使用 Forth 汇编的跳转指令时, Forth 字 IF ... ELSE ... THEN 、 BEGIN ... WHILE ... REPEAT 和 BEGIN ... UNTIL 可以有下列的代码条件
0= JNE/JNZ
0<> JE/JZ
0< JNS
0>= JS
< JNL/JGE
>= JL/JNGE
<= JNLE/JG
> JLE/JNG
U< JNB/JAE/JNC
U>= JB/JNAE/JC
U<= JNBE/JA
U> JBE/JNA
OV JNO
CX<>0 JCX0
作为一个例子,考虑 Forth 字 ?DUP 的定义,它只在堆栈上的值为非 0 时才复制栈顶:
CODE ?DUP ( n -- n n | 0 )
MOV DI, SP
MOV CX, 0 [DI]
CX<>0
IF
PUSH CX
THEN
NEXT
END-CODE
注意当这个定义汇编后,语句 CX<>0 被汇编成 JCX0 放在 THEN.
7.3 长存储器地址字
下面这些长存储器地址字对于访问不在代码段的数据非常有用:
CODE @L ( seg off -- n ) \ Fetch 16-bit value from seg:off
POP BX \ BX = offset address
POP DS \ DS = segment address
MOV AX, 0 [BX] \ AX = data at DS:BX
MOV BX, CS \ Restore DS to CS value
MOV DS, BX
1PUSH \ push value on stack
END-CODE
CODE !L ( n seg off -- ) \ Store 16-bit value at seg:off
POP BX \ BX = offset address
POP DS \ DS = segment address
POP AX \ AX = n
MOV 0 [BX],AX \ Store n at DS:BX
MOV BX, CS \ Restore DS to CS value
MOV DS, BX
NEXT
END-CODE
下面是一些有用的长存储器字:
C@L ( seg off -- byte ) \ Fetch 8-bit byte from seg:off
C!L ( byte seg off -- ) \ Store 8-bit byte at seg:off
CMOVEL ( sseg soff dseg doff count )
\ move a block of count bytes from sseg:soff to dseg:doff
CMOVEL> ( sseg soff dseg doff count )
\ move a block of count bytes from sseg:soff to dseg:doff
\ moves last byte first to avoid overwriting moved data
7.4 DOS 字
F-PC 拥有大量的 Forth 字用于处理 DOS 文件 I/O ,这些字都在源文件 HANDLES.SEQ 和 SEQREAD.SEQ 中定义。本节和下面一节将开发一系列的文件 I/O 字,它们可以让你使用并扩展处理各种文件 I/O 、进行其它 DOS 操作。这些字可以替代或者与 F-PC DOS 和文件 I/O 字联合使用。
VARIABLE ITEMS \ used to record stack depth
VARIABLE handl \ file handle
VARIABLE eof \ TRUE if end-of-file was read
CREATE fname 80 ALLOT \ 80 byte buffer containing ASCII filename
: {{ ( -- )
DEPTH ITEMS ! ;
: }} ( -- c )
DEPTH ITEMS @ - ;
{{ . . . }} 使用追踪放置到堆栈上的元素的数目,例如:
{{ 5 2 8 }}
将把下列值留在堆栈上
5 2 8 3
堆栈上的3是在 {{ 和 }} 之间的元素的数目。
: $>asciiz ( addr1 -- addr2 ) \ change counted string to ASCIIZ string
DUP C@ SWAP 1+
TUCK +
0 SWAP C! ;
DOS 2.0+ 磁盘 I/O 功能
2fdos 调用 DOS INT 21H 功能,并使用堆栈上的 ax =ah:al, bx, cx 和 dx 。它在堆栈上返回 ax, dx 和一个错误标志。如果错误标志为真,则错误代码在 ax 中(堆栈上的第三个元素)。如果错误标志为假,则 ax 和 dx 的值依赖于所调用的功能。
fdos 与 2fdos 相似,但是不返回错误标志,它被用于不使用进位标志来指示错误的功能调用。
PREFIX
HEX
CODE 2fdos ( ax bx cx dx -- ax dx f )
POP DX
POP CX
POP BX
POP AX
INT 21 \ DOS function call
U>=
IF \ if carry = 0
MOV BX, # FALSE \ set error flag to false
ELSE \ else
MOV BX, # TRUE \ set error flag to true
THEN
PUSH AX
PUSH DX
PUSH BX
NEXT
END-CODE
CODE fdos ( ax bx cx dx -- ax dx )
POP DX
POP CX
POP BX
POP AX
INT 21 \ DOS function call
PUSH AX
PUSH DX
NEXT
END-CODE
DECIMAL
7.5 基本的文件 I/O
下面这些字可以用于基本的文件 I/O 操作,比如打开、创建、关闭和删除文件,以及从磁盘文件中读写字节。
open.file ( addr -- handle ff | error.code tf )
打开一个文件。在栈顶的假标志下返回一个句柄,在真标志下返回一个错误代码。 addr 指向一个 asciiz 串,访问码设为 2 用于读写方式读写。
HEX
: open.file ( addr -- handle ff | error.code tf )
3D02 \ ah = 3D; al = access.code=2
0 ROT 0 SWAP \ 3D02 0 0 addr
2fdos \ DOS function call
NIP ; \ nip dx
close.file 关闭一个文件,文件句柄在栈顶,如果不能关闭则打印错误信息。
: close.file ( handle -- )
3E00 \ ah = 3E
SWAP 0 0 \ bx = handle
2fdos
NIP \ nip dx
IF
." Close error number " . ABORT
THEN
DROP ;
create.file 创建文件 – 返回值与 open.file 一样
addr 指向一个 asciiz 串
attr 是文件属性
0 - normal file
01H - read only
02H - hidden
04H - system
08H - volume label
10H - subdirectory
20H – archive
: create.file ( addr attr -- handle ff | error.code tf )
3C00 \ ah = 3C
0 2SWAP SWAP \ 3C00 0 attr addr
2fdos
NIP ; \ nip dx
open/create 在文件存在就打开它,不存在时则创建一个新的一般文件
addr 指向一个 asciiz 串,返回一个打开文件的句柄,如果不能打开则打印一个错误信息。
: open/create ( addr -- handle )
DUP open.file
IF
DUP 2 =
IF
DROP 0 create.file
IF ." Create error no. " . ABORT
THEN
ELSE
." Open error no. " . DROP ABORT
THEN
ELSE
NIP
THEN ;
: delete.file ( addr -- ax ff | error.code tf )
4100
0 ROT 0 SWAP
2fdos
NIP ;
: erase.file ( $addr -- )
$>asciiz
delete.file
IF
CR ." Delete file error no. " .
ELSE
DROP
THEN ;
read.file 从文件 handle 中读出 #bytes 个字节到 buff.addr 缓冲区,返回读入的字节数 #bytes ,如果返回 0 ,则读到了文件尾,如果不成功则打印错误信息。
: read.file ( handle #bytes buff.addr -- #bytes )
>R 3F00 \ handle #bytes 3F00
-ROT R> \ 3F00 handle #bytes addr
2fdos
NIP \ nip dx
IF
." Read error no. " . ABORT
THEN ;
write.file 将 buff.addr' 绘缓冲区的 '#bytes' 个字节写入文件 'handle'. 如果不成功则打印一个错误信息。
: write.file ( handle #bytes buff.addr -- )
>R 4000 \ handle #bytes 4000
-ROT R> \ 4000 handle #bytes addr
2fdos
NIP \ nip dx
IF
." Write error no. " . ABORT
ELSE
DROP
THEN ;
mov.ptr 移动文件 handle 的文件读写指针,doffset 是一个双精度 32 位偏移量,code 是方式代码,其意义如下:
0 – 移动文件指针到文件开始 + offset 处
1 – 用 offset 增量指针
2 - 移动文件指针到文件尾 + offset 处
: mov.ptr ( handle doffset code -- dptr )
42 FLIP + \ hndl offL offH 42cd
ROT >R \ hndl offH 42cd
-ROT R> \ 42cd hndl offH offL
2fdos
IF
DROP ." Move pointer error no. " . ABORT
THEN ;
rewind.file 移动文件 handle 的读写指针到文件开始处
: rewind.file ( handle -- )
0 0 0 mov.ptr 2DROP ;
get.length 返回文件 handle 的 32 位字节长度
: get.length ( handle -- dlength )
0 0 2 mov.ptr ;
read.file.L 从已经打开的文件 handle 中读出 #bytes 字节到扩展存储器 seg:offset 处
CODE read.file.L ( handle #bytes seg offset -- ax f )
POP DX
POP DS
POP CX
POP BX
MOV AH, # 3F
INT 21
U>=
IF
MOV BX, # FALSE
ELSE
MOV BX, # TRUE
THEN
MOV CX, CS \ restore DS
MOV DS, CX
PUSH AX
PUSH BX
NEXT
END-CODE
write.file.L 写 #bytes 个字节到一个打开的文件 handle 中,要写入的数据在扩展存储器 seg:offset 处。
CODE write.file.L ( handle #bytes seg offset -- ax f )
POP DX
POP DS
POP CX
POP BX
MOV AH, # 40
INT 21
U>=
IF
MOV BX, # FALSE
ELSE
MOV BX, # TRUE
THEN
MOV CX, CS \ restore DS
MOV DS, CX
PUSH AX
PUSH BX
NEXT
END-CODE
findfirst.dir 查找文件目录的第一个匹配,文件指示符位于 addr 的 asciiz 串。
CODE findfirst.dir ( addr -- f ) \ search directory for first match
POP DX \ dx = addr of asciiz string
PUSH DS \ save ds
MOV AX, CS
MOV DS, AX \ ds = cs
MOV CX, # 10 \ attr includes subdirectories
MOV AX, # 4E00 \ ah = 4E
INT 21 \ DOS function call
JC 1 $ \ if no error
MOV AX, # FF \ flag = TRUE
JMP 2 $ \ else
1 $: MOV AX, # 0 \ flag = FALSE
2 $: POP DS \ restore ds
PUSH AX \ push flag on stack
NEXT
END-CODE
findnext.dir 查找文件目录的下一个匹配,文件描述在 addr 处
CODE findnext.dir ( -- f ) \ search directory for next match
PUSH DS \ save ds
MOV AX, CS
MOV DS, AX \ ds = cs
MOV AX, # 4F00 \ ah = 4F
INT 21 \ DOS function call
JC 1 $ \ if no error
MOV AX, # FF \ flag = TRUE
JMP 2 $ \ else
1 $: MOV AX, # 0 \ flag = FALSE
2 $: POP DS \ restore ds
PUSH AX \ push flag on stack
NEXT
END-CODE
set-dta.dir 设置磁盘传输区 DTA 地址
CODE set-dta.dir ( addr -- ) \ set disk transfer area address
POP DX \ dx = dta address
PUSH DS \ save ds
MOV AX, CS
MOV DS, AX \ ds = cs
MOV AX, # 1A00 \ ah = 1A
INT 21 \ DOS function call
POP DS \ restore ds
NEXT
END-CODE
DECIMAL
7.6 读入数和字符串
下面的字可以用于从磁盘文件中读入字节、数和串。
get.fn 从键盘输入一个文件名并作为一个 asciiz 串存放 fname 中。
: get.fn ( -- )
QUERY BL WORD \ addr
DUP C@ 1+ \ addr cnt+1
2DUP + \ addr len addr.end
0 SWAP C! \ make asciiz string
SWAP 1+ SWAP \ addr+1 len
fname SWAP \ from to len
CMOVE ;
open.filename 输入一个文件名,打开这个文件,将文件句柄存入变量 handl 中。
: open.filename ( -- )
get.fn
fname open/create
handl ! ;
eof? 如果读到了一个文件结束符(eof = true),则退出包含 eof? 的这个字。
: eof? ( -- )
eof @
IF
2R> 2DROP EXIT
THEN ;
get.next.byte 从磁盘文件中得下一个字节,文件句柄在 handl 中,如果是 eof 则设置变量 eof 为真。
: get.next.byte ( -- byte )
handl @ 1 PAD read.file
IF
FALSE eof ! PAD C@
ELSE
TRUE eof !
THEN ;
get.next.val 从文件中读出下一个字的值(2 字节),文件句柄在 handl 中,如果到达文件尾则设置变量 eof 为真,如果文件中存储的不是 ASCII 码而是实际的数则这个字就非常有用。
: get.next.val ( -- n )
handl @ 2 PAD read.file
IF
FALSE eof ! PAD @
ELSE
TRUE eof !
THEN ;
get.next.dval 从磁盘文件中读入 32 位的值(4 字节),文件句柄在 handl 中。如果文件结束则则设置 eof 变量为真,如果文件中存储的不是 ASCII 码而是实际的数则这个字就非常有用。
: get.next.dval ( -- d )
handl @ 4 PAD read.file
IF
FALSE eof ! PAD 2@
ELSE
TRUE eof !
THEN ;
parenchk 如果栈上是一个 '(' 则读文件直到字符 ')' 被读入。如果 eof 则退出。
: parenchk ( byte -- byte )
DUP ASCII ( =
IF
DROP
BEGIN
get.next.byte eof?
ASCII ) =
UNTIL
get.next.byte eof?
THEN ;
quotechk 如果堆栈上的字节是引号 (") ,读入文件直到字节 " 被读入。如果读到 eof 则退出。
: quotechk ( byte -- byte )
DUP ASCII " =
IF
DROP
BEGIN
get.next.byte eof?
ASCII " =
UNTIL
get.next.byte eof?
THEN ;
?digit 检查堆栈上的字节是不是一个对应当前数基的 ASCII 码。
: ?digit ( byte -- byte f )
DUP BASE @ DIGIT NIP ;
get.next.digit 从磁盘文件中得到一个合法的 ASCII 数字,如果读到 eof 则退出。
: get.next.digit ( -- digit )
BEGIN
get.next.byte eof?
parenchk eof?
quotechk eof?
?digit NOT
WHILE
DROP
REPEAT ;
get.digit/minus 从磁盘文件中得到一个合法的 ASCII 数字或者一个减号,如果读到 eof 则退出。
: get.digit/minus ( -- digit or - )
BEGIN
get.next.byte eof?
parenchk eof?
quotechk eof?
DUP ASCII - =
SWAP ?digit ROT OR NOT
WHILE
DROP
REPEAT ;
get.next.number 从磁盘文件中读入一个以 ASCII 串存储的有符号数,并把它转换成一个有符号的 16 位整数,如果读到 eof 则退出。
: get.next.number ( -- n )
{{ get.digit/minus eof? \ uses {{ }} to store
BEGIN \ consecutive digits
get.next.byte eof? \ on the stack.
parenchk eof? \ ignore (...)
quotechk eof? \ and "..."
?digit NOT
UNTIL
DROP }}
DUP PAD C!
DUP PAD + BL OVER 1+ C!
SWAP 0 DO \ move digits on stack
SWAP OVER C! 1- \ to counted string as PAD
LOOP
NUMBER DROP ; \ convert to number
?period 测试一个字节是不是一个小数点。注意标志作为为次栈顶元素。
: ?period ( byte -- f byte )
DUP ASCII . = SWAP ;
get.next.dnumber 从磁盘文件中读入一个以 ASCII 串存储的有符号实数,并把它转换成一个有符号双精度数放到堆栈上,小数点之后的数字数目放到变量 DPL 中,如果读到 eof 则退出。
: get.next.dnumber ( -- dn )
{{ get.digit/minus eof?
BEGIN
get.next.byte eof?
parenchk eof? \ similar to
quotechk eof? \ get.next.number
?period \ but include period
?digit ROT OR NOT \ in number string
UNTIL
DROP }}
DUP PAD C!
DUP PAD + BL OVER 1+ C!
SWAP 0 DO
SWAP OVER C! 1-
LOOP
NUMBER ; \ convert to double number
get.next.string 从磁盘文件中读入包含在引号中的字符串,并把它存储成位于 addr 地址处的一个计数串。
: get.next.string ( -- addr ) \ counted string
BEGIN
get.next.byte eof?
ASCII " =
UNTIL
0 PAD 1+
BEGIN \ cnt addr
get.next.byte eof?
DUP ASCII " <>
WHILE
OVER C!
SWAP 1+ SWAP
1+
REPEAT
2DROP PAD C! PAD ;
7.7 数字和串
send.byte 输入一个字节到打开的文件中,文件的句柄在 handl 中。
: send.byte ( byte -- )
PAD C!
handl @
1 PAD write.file ;
send.number 把一个有符号的 16 位数字作为一个 ASCII 串写入打开的文件中,文件的句柄在 handl 中。
: send.number ( n -- )
(.) 0
DO
DUP C@ send.byte
1+
LOOP
DROP ;
send.number.r 把一个有符号 16 位数作为一个 ASCII 串写入一个打开的文件中,这个数字将被右对齐到一个宽度为 len 的字段中,并用 ASCII 空格填充。
: send.number.r ( n l -- )
>R (.) R>
OVER -
0 DO
BL send.byte
LOOP
0 DO
DUP C@ send.byte 1+
LOOP
DROP ;
send.dnumber 把一个有符号的 32 位数作为一个 ASCII 串写入打开的文件中,文件的句柄在 handl,小数点的位置由 DPL 的内容决定。
: send.dnumber ( d -- ) \ DPL = #digits after dec. point
TUCK DABS <# DPL @ ?DUP
IF
0 DO # LOOP
ASCII . HOLD
THEN
#S ROT SIGN #>
0 DO
DUP C@ send.byte 1+
LOOP DROP ;
: send.val ( n -- ) \ send 16-bit value
PAD ! handl @
2 PAD write.file ;
: send.dval ( d -- ) \ send 32-bit value
PAD 2! handl @
4 PAD write.file ;
: send.string ( addr -- ) \ addr of counted string
DUP C@
SWAP 1+ SWAP
0 DO
DUP I + C@
send.byte
LOOP
DROP ;
: send.crlf ( -- )
13 send.byte
10 send.byte ;
: send.lf ( -- )
10 send.byte ;
: send.cr ( -- )
13 send.byte ;
: send.tab ( -- )
9 send.byte ;
: send.( ( -- )
ASCII ( send.byte ;
: send.) ( -- )
ASCII ) send.byte ;
: send., ( -- )
ASCII , send.byte ;
: send." ( -- )
ASCII " send.byte ;
: send."string" ( addr -- )
send."
send.string
send." ;
|