
再一次,我高估了这篇文章的数量! 我计划这将是最后一篇文章,我们将在其中编写一个编译器并进行测试。 但是结果却很大,因此我决定将文章分成两部分。
在本文中,我们将完成编译器的几乎所有基本功能。 这将成为现实,并且可以编写,编译和执行相当严格的代码。 我们将在下一部分中进行测试。 (顺便说一下,前面的部分:
1、2、3 )。
我是第一次在哈布雷(Habré)写作;也许并非总是如此。 我认为,第2、3条相当干燥,代码很多,描述很少。 这次,我将尝试做一些不同的事情,着重于对想法本身的描述。 好吧,代码……代码当然会! 谁想彻底了解,这样的机会将是。 在许多情况下,我会将代码放在破坏者的下面。 而且,当然,您始终可以在github上查看完整的源代码。
编译器将继续在汇编器中写入一段时间,然后进入要塞,继续自己编写编译器。 这将类似于蒙克豪森男爵(Baron Munchausen),他从沼泽地的头发中拉了自己。 但是,对于初学者来说,我将概述要塞上的编译器的工作方式。 欢迎来到猫!
编译器如何工作?
堡垒中的内存由一个连续的片段组成,在该片段中顺序排列字典条目。 完成后,紧随其后的是空闲存储区。 第一个空闲字节由变量h指示。 这里还有一个经常使用的词,它会将第一个空闲字节的地址压入堆栈,它的确定非常简单:
: here h @ ;

值得一提的是分配字,它通过移动指针h保留了指定的字节数。 分配词可以定义如下:
: allot h +! ;
实际上,编译器使用特殊的解释器模式以及一些特殊的单词。 因此,只需一句话,您就可以在堡垒中描述编译器的整个原理。 解释器的工作模式由状态变量决定。 如果为零,则设置执行模式,否则为编译模式。 我们已经熟悉执行模式,其中输入缓冲区中的单词只是一个接一个地执行。 但是在编译模式下,它们不会执行,而是由指针h编译到内存中。 因此,指针向前移动。
在经典的堡垒中,单词“,”用于编译整数值,单词“ c,”用于编译字节。 我们的系统使用不同位深度(8、16、32、64)的值,因此,我们将另外加上单词“ w”和“ i”。 我们还制作了单词“ str”,它将编译字符串,并从堆栈中获取两个值-字符串的地址和长度。
特殊的编译器字用于形成控制结构。 这些是if,do,loop和其他词。 这些字甚至在编译模式下也被执行。 例如,单词if在执行时会编译条件分支字节命令(?Nbranch)。 为了使系统知道需要在编译模式下执行哪些字,而不是编译哪些字,请使用立即标记(符号)。 我们已经在字典条目的标志字段中拥有它。 在汇编器源代码中,它称为f_immediate。 要设置此标志,请使用单词立即。 它没有参数,立即标记设置在字典的最后一个单词上。
现在,让我们从理论转向实践!
准备工作
首先,我们需要使用所需的汇编语言来执行一些简单的字节命令。 它们是:移动(复制存储区),填充(填充存储区),位操作(和,或“异或”,“取反”),移位命令(rshift,lshift)。 让我们执行相同的rpick(这与pick相同,它仅适用于返回堆栈,不适用于数据堆栈)。
这些命令非常简单,这是它们的代码 b_move = 0x66 bcmd_move: pop rcx pop rdi pop rsi repz movsb jmp _next b_fill = 0x67 bcmd_fill: pop rax pop rcx pop rdi repz stosb jmp _next b_rpick = 0x63 bcmd_rpick: pop rcx push [rbp + rcx * 8] jmp _next b_and = 0x58 bcmd_and: pop rax and [rsp], rax jmp _next b_or = 0x59 bcmd_or: pop rax or [rsp], rax jmp _next b_xor = 0x5A bcmd_xor: pop rax xor [rsp], rax jmp _next b_invert = 0x5B bcmd_invert: notq [rsp] jmp _next b_rshift = 0x5C bcmd_rshift: pop rcx or rcx, rcx jz _next 1: shrq [rsp] dec rcx jnz 1b jmp _next b_lshift = 0x5D bcmd_lshift: pop rcx or rcx, rcx jz _next 1: shlq [rsp] dec rcx jnz 1b jmp _next
仍然需要使单词单词。 这与blword相同,但是在堆栈上指示了一个特定的定界符。 我没有提供代码,可以在源代码中找到。 我复制/粘贴单词blworld并替换了比较命令。
总之,我们将单词syscall命名。 有了它,就有可能进行丢失的系统操作,例如,处理文件。 如果需要平台独立性,则这种解决方案将不起作用。 但是此系统现在用于测试,所以现在就这样吧。 如有必要,所有操作都可以转换为字节命令,这并不困难。 syscall命令将接受6个用于系统调用的参数和来自堆栈的电话号码。 它将返回一个参数。 参数分配和返回值由系统调用号确定。
b_syscall = 0xFF bcmd_syscall: sub rbp, 8 mov [rbp], r8 pop rax pop r9 pop r8 pop r10 pop rdx pop rsi pop rdi syscall push rax mov r8, [rbp] add rbp, 8 jmp _next
现在让我们直接进入编译器。
编译器
让我们创建变量h,这里的一切都很简单。
item h h: .byte b_var0 .quad 0
我们将在开始行中编写其初始化:
# forth last_item context @ ! h dup 8 + swap ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word h - . - 2 .byte b_dup, b_num8, b_add, b_swap, b_set .byte b_quit
让我们在这里说一下:
item here .byte b_call8, h - . - 1 .byte b_get .byte b_exit
还有用于编译值的单词:“ allot”和“ c”,“ w”,“ i”,“,”,“ str” # : allot h +! ; item allot allot: .byte b_call8, h - . - 1, b_setp, b_exit # : , here ! 8 allot ; item "," .byte b_call8, here - . - 1, b_set, b_num8, b_call8, allot - . - 1, b_exit # : i, here i! 4 allot ; item "i," .byte b_call8, here - . - 1, b_set32, b_num4, b_call8, allot - . - 1, b_exit # : w, here w! 2 allot ; item "w," .byte b_call8, here - . - 1, b_set16, b_num2, b_call8, allot - . - 1, b_exit # : c, here c! 1 allot ; item "c," .byte b_call8, here - . - 1, b_set8, b_num1, b_call8, allot - . - 1, b_exit # : str, dup -rot dup c, here swap move 1+ h +!; item "str," c_str: .byte b_dup, b_mrot, b_dup callb c_8 callb here .byte b_swap, b_move callb h .byte b_setp .byte b_exit
现在,让状态变量和两个单词来控制其值:“ [”和“]”。 通常,这些词在编译时用于执行某些操作。 因此,单词“ [”关闭编译模式,单词“]”打开编译模式。 但是,在需要打开或关闭编译模式的其他情况下,没有什么可以阻止使用它们。 单词“ [”将是我们的第一个带有立即符号的单词。 否则,它将无法关闭编译模式,因为它将被编译而不执行。
item state .byte b_var0 .quad 0 item "]" .byte b_num1 callb state .byte b_set, b_exit item "[", f_immediate .byte b_num0 callb state .byte b_set, b_exit
转向$编译一词。 它将从堆栈中获取字典条目的地址,并编译指定的单词。 为了在普通Fort实现中编译一个单词,只需将单词“,”应用到执行地址即可。 这里的一切都更加复杂。 首先,有两种类型的单词-字节码和机器码。 前者按字节编译,后者按调用字节命令编译。 其次-我们有多达四个call命令的变体:call8,call16,call32和call64。 四个? 不行 当我编写编译器时,我在这四个中又增加了16个! :)
这是怎么发生的? 我们必须做一点题外话。
改善通话指令
当编译器开始工作时,我发现在许多情况下(但不是全部),call8命令就足够了。 这是当被调用字在128字节以内时。 我想-以及如何确保几乎在所有情况下都会发生这种情况? 如何在一个字节中放置超过256个值?
我注意到的第一点是,在要塞中,呼叫始终指向较低的地址。 这意味着您可以重做call命令,使其只能调用低位地址,但可以调用256个字节,而不能调用128个字节。更好。
但是,如果您在某些地方放一些东西……事实证明那里是那里! 我们有两个字节:一个字节是命令,第二个字节是偏移量。 但是,没有什么能阻止命令的低位放置参数的高位(偏移)。 对于字节机,似乎有多个而不是一个调用命令。 是的,通过这种方式,我们用一个命令占据了字节命令代码表的几个单元,但是有时值得这样做。 调用命令是最常用的命令之一,因此我决定在命令中放入4个偏移量位。 因此,您可以拨打多达4095字节的距离! 这意味着几乎总是会使用这样的短呼叫命令。 我将这些命令放置为代码0xA0,并且以下行出现在命令表中:
.quad bcmd_call8b0, bcmd_call8b1, bcmd_call8b2, bcmd_call8b3, bcmd_call8b4, bcmd_call8b5, bcmd_call8b6, bcmd_call8b7 # 0xA0 .quad bcmd_call8b8, bcmd_call8b9, bcmd_call8b10, bcmd_call8b11, bcmd_call8b12, bcmd_call8b13, bcmd_call8b14, bcmd_call8b15
这些字节命令中的第一个简单地以参数中指定的偏移量(最多为255)向低地址方向进行调用。 其余的将相应的偏移量添加到参数。 bcmd_call8b1添加256,bcmd_call8b2添加512,依此类推。 我分别发出了第一个调用命令,其余的则使用了宏。
第一条命令:
b_call8b0 = 0xA0 bcmd_call8b0: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 sub r8, rax jmp _next
宏并创建其余的调用命令:
.macro call8b N b_call8b\N = 0xA\N bcmd_call8b\N: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 add rax, \N * 256 mov [rbp], r8 sub r8, rax jmp _next .endm call8b 1 call8b 2 call8b 3 call8b 4 call8b 5 call8b 6 call8b 7 call8b 8 call8b 9 call8b 10 call8b 11 call8b 12 call8b 13 call8b 14 call8b 15
好吧,我重做了旧的call8命令以进行转接,因为我们已经有16个团队进行了回调。 无论有什么困惑,我都将其重命名为b_call8f:
b_call8f = 0x0C bcmd_call8f: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next
顺便说一句,为方便起见,我制作了一个宏,该宏在汇编器中自动在4095内编译相应的回调。然后,我就不需要再进行:)
.macro callb adr .if \adr > . .error "callb do not for forward!" .endif .byte b_call8b0 + (. - \adr + 1) >> 8 .byte (. - \adr + 1) & 255 .endm
现在...
团队编制
因此,我们得到了一个相当复杂的命令编译算法。 如果这是字节命令,则仅编译一个字节(字节命令代码)。 并且,如果此字已用字节码编写,则需要使用call命令(从20个中选择一个)来编译其调用。 更准确地说是19,因此我们没有呼叫转移,因此call8f不会用作堡垒。
所以选择就是这个。 如果偏移量在0到4095之间,请选择代码为0xA0的bcmd_call8b命令,将四个最高有效偏移量位放在命令的最低有效位中。 同时,对于字节机,bcmd_call8b0命令之一的代码为bcmd_call8b15。
如果后向偏移量大于或等于4095,则我们确定偏移量放置在哪个尺寸上,并使用来自call16 / 32/64的适当命令。 应该记住的是,这些球队的补偿金额是有符号的。 它们可能导致前进和后退。 例如,call16可以在两个方向上调用32767的距离。
结果是实现:
$编译编译一个单词。 作为参数,采用已编译单词的字典条目的地址。 实际上,它检查f_code标志,计算代码地址(cfa),并调用compile_b或compile_c(如果设置了标志)。
compile_c编译一个字节命令。 这里最简单的单词在堡垒上的描述如下:
: compile_c c@ c, ;
compile_b它在堆栈上使用字节码地址并编译其调用。
test_bv它从堆栈中偏移(带符号),并确定要使用的位深度(1、2、4或8个字节)。 返回值0、1、2或3。使用此字,您可以从call16 / 32/64命令中确定要使用哪个值。 该单词在编译数字时会派上用场(从lit8 / 16/32/64中选择)。
顺便说一句,您可以启动系统并使用以下任何一个单词在堡垒控制台中“玩转”。 例如:
$ ./forth ( 0 ): > 222 test_bv ( 2 ): 222 1 > drop drop ( 0 ): > 1000000 test_bv ( 2 ): 1000000 2 > drop drop ( 0 ): > -33 test_bv ( 2 ): -33 0 >
test_bvc它从堆栈中获取一个偏移量(带有符号),并确定要使用的调用命令。 实际上,它会检查偏移量是否在0 ... -4095内,并返回0。在这种情况下,如果在此间隔内没有命中,它将调用test_bv。
这就是编译命令所需要的全部。 # : test_bvc dup 0 >= over FFF <= and if 0 exit else ... item test_bvc test_bvc: .byte b_dup, b_neg .byte b_num0 .byte b_gteq .byte b_over, b_neg .byte b_lit16 .word 0xFFF .byte b_lteq .byte b_and .byte b_qnbranch8, 1f - . .byte b_num0 .byte b_exit item test_bv test_bv: .byte b_dup, b_lit8, 0x80, b_gteq, b_over, b_lit8, 0x7f, b_lteq, b_and, b_qnbranch8, 1f - ., b_num0 .byte b_exit 1: .byte b_dup .byte b_lit16 .word 0x8001 .byte b_gteq .byte b_over .byte b_lit16 .word 0x7ffe .byte b_lteq, b_and, b_qnbranch8, 2f - ., b_num1, b_exit 2: .byte b_dup .byte b_lit32 .int 0x80000002 .byte b_gteq .byte b_over .byte b_lit32 .int 0x7ffffffd .byte b_lteq, b_and, b_qnbranch8, 3f - ., b_num2, b_exit 3: .byte b_num3 .byte b_exit # - item compile_c compile_c: .byte b_get8 callb c_8 .byte b_exit # - item compile_b compile_b: callb here .byte b_num2, b_add .byte b_sub callb test_bvc .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop .byte b_neg .byte b_dup .byte b_lit8, 8 .byte b_rshift .byte b_lit8, b_call8b0 .byte b_or callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_call16 callb c_8 .byte b_wm callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_call32 callb c_8 .byte b_num3, b_sub callb c_32 .byte b_exit 3: .byte b_lit8, b_call64 callb c_8 .byte b_lit8, 7, b_sub callb c_64 .byte b_exit #: $compile dup c@ 0x80 and if cfa compile_c else cfa compile_b then ; item "$compile" _compile: .byte b_dup, b_get8, b_lit8, 0x80, b_and, b_qnbranch8, 1f - ., b_cfa callb compile_c .byte b_exit 1: .byte b_cfa callb compile_b .byte b_exit
现在我们需要编译数字。
编制数字(文字)
写了一个完整的字幕,准备专门描述文字的编译,但是事实证明没有什么特别的描述:)
我们已经在test_bv一词中完成了一半的工作。 仅保留调用test_bv,并根据结果编译lit8 / 16/32/64,然后编译大小分别为1、2、4或8个字节的值。
我们通过定义单词compile_n来实现 # item compile_n compile_n: callb test_bv .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop, b_lit8, b_lit8 callb c_8 callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - ., b_drop, b_lit8, b_lit16 callb c_8 callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - ., b_lit8, b_lit32 callb c_8 callb c_32 .byte b_exit 3: .byte b_lit8, b_lit64 callb c_8 callb c_64 .byte b_exit
修改解释器
一切准备就绪,可以编译命令和文字。 现在需要将其内置到解释器中。 此修改很简单。 执行命令的位置,添加状态检查。 如果state不为null,并且单词不包含立即标志,请执行$ compile而不是执行。 从输入流中获取数字的操作大致相同。 如果状态为零,则将数字保留在堆栈上,否则不调用compile_n。
这是口译员 item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop callb state .byte b_get .byte b_qnbranch8, irpt_execute - . # 0, .byte b_dup, b_get8, b_lit8, f_immediate, b_and # immediate .byte b_qbranch8, irpt_execute - . # - # ! callb _compile .byte b_branch8, 2f - . irpt_execute: .byte b_cfa # , (state = 0 immediate ) .byte b_execute .byte b_branch8, 2f - . 1: .byte b_drop .byte b_over, b_over .byte b_numberq # , .byte b_qbranch8, 3f - . # 0, , 3 .byte b_type # .byte b_strp # .byte 19 # .ascii " : word not found!\n" .byte b_quit # 3: .byte b_nip, b_nip # , ( b_over, b_over) # - callb state # , .byte b_get .byte b_qnbranch8, 2f - . # - ; - # callb compile_n 2: # .byte b_depth # .byte b_zlt # , 0 ( 0<) .byte b_qnbranch8, interpret_ok - . # , , .byte b_strp # .byte 14 .ascii "\nstack fault!\n" .byte b_quit # interpret_ok: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
现在我们离编译器仅一步之遥...
新单词的定义(单词“:”)
现在,如果将状态变量设置为非零值,则将开始编译过程。 但是结果将是无用的,我们无法实现它,甚至无法在内存中找到它。 为了使所有这些操作成为可能,有必要以字典文章的形式格式化编译结果。 为此,在打开编译模式之前,您需要为该单词创建标题。
标头应包含标志,通信字段和名称。 在这里,我们有一个熟悉的故事-通信字段可以是1、2、4或8个字节。 让我们使用compile_1248这个词,它将帮助我们形成这样一个交流领域。 堆栈上将使用两个数字-偏移量和test_bv命令生成的值。
编译_1248 # , , # , test_dv item compile_1248 compile_1248: .byte b_dup .byte b_zeq .byte b_qnbranch8, 1f - . .byte b_drop callb c_8 .byte b_exit 1: .byte b_dup, b_num1, b_eq, b_qnbranch8, 2f - . .byte b_drop callb c_16 .byte b_exit 2: .byte b_num2, b_eq, b_qnbranch8, 3f - . callb c_32 .byte b_exit 3: callb c_64 .byte b_exit
现在使$创建。 这将对我们不止一次有用。 您可以在需要为字典条目创建标题时使用它。 它将从堆栈中获取两个值-创建的单词的名称的地址及其长度。 执行完该单词后,创建的字典条目的地址将出现在堆栈中。
$创建 # : $create here current @ @ here - test_bv dup c, compile_1248 -rot str, current @ ! ' var0 here c!; item "$create" create: callb here callb current .byte b_get, b_get callb here .byte b_sub callb test_bv .byte b_dup callb c_8 callb compile_1248 .byte b_mrot callb c_str # callb current .byte b_get, b_set # - var0, here # , - , # , # 1 allot , .byte b_lit8, b_var0 callb here .byte b_set8 .byte b_exit
下一个单词将使用单词blword从输入流中选取新单词的名称,并调用$ create,以指定的名称创建一个新单词。
create_in item "create_in" create_in: .byte b_blword .byte b_dup .byte b_qbranch8 .byte 1f - . .byte b_strp # ( ) .byte 3f - 2f # 2: .ascii "\ncreate_in - name not found!\n" 3: .byte b_quit 1: callb create .byte b_exit
最后,加上单词“:”。 它将使用create_in创建一个新单词并设置编译模式,但尚未安装。 如果已安装,则会出现错误。 单词“:”将带有立即符号。
这个词: # : : create_in 1 state dup @ if ." : - no execute state!" then ! 110 ; immediate item ":", f_immediate colon: callb create_in .byte b_num1 callb state .byte b_dup .byte b_get .byte b_qnbranch8, 2f - . .byte b_strp # ( ) .byte 4f - 3f # 3: .ascii "\n: - no execute state!\n" 4: .byte b_quit 2: .byte b_set .byte b_lit8, 110 .byte b_exit
如果有人查看了代码,那么他会看到这个词还有其他作用:)
这是110 ???
是的,这个单词还将数字110推入堆栈,这就是原因。 编译时,各种构造必须是一个整体。 例如,如果必须之后。 使用“:”创建的单词应以“;”结尾。 为了检查这些条件,编译器的特殊字将某些值放在堆栈上并检查它们的存在。 例如,单词“:”的值是110,单词“;”的值是 检查110是否位于堆栈的顶部,如果不是这种情况,则为错误。 因此,控制结构没有配对。
此类检查是在编译器的所有此类词中进行的,因此,我们将为此专门制作一个词-“?Pairs”。 它将从堆栈中获取两个值,如果它们不相等,则抛出错误。
同样,换句话说,您通常必须检查是否设置了编译模式。 为此,让我们使用“?State”一词。
成对状态 #: ?pairs = ifnot exit then ." \nerror: no pairs operators" quit then ; item "?pairs" .byte b_eq, b_qbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no pairs operators" 3: .byte b_quit 1: .byte b_exit #: ?state state @ 0= if abort" error: no compile state" then ; item "?state" callb state .byte b_get, b_zeq, b_qnbranch8, 1f - . .byte b_strp .byte 3f - 2f 2: .ascii "\nerror: no compile state" 3: .byte b_quit 1: .byte b_exit
仅此而已! 我们不会在汇编器中手动编译其他任何东西:)
但是直到最后,还没有编写编译器,因此一开始您将不得不使用一些不寻常的方法...
让我们准备好使用创建的编译器编译创建的编译器
首先,您可以通过编译一些简单的命令来检查单词“:”的工作方式。 让我们做一个词,例如:
: ^2 dup * ;
这个词是平方。 但是我们没有单词“;”怎么办?
我们改写exit一词,然后编译。然后使用单词“ [”关闭编译模式,并降低值110: $ ./forth ( 0 ): > : ^2 dup * exit [ drop ( 0 ): > 4 ^2 ( 1 ): 16 >
有效!让我们继续...由于我们将继续在堡垒上编写堡垒,因此我们需要考虑堡垒的源代码在哪里以及何时编译。让我们做出最简单的选择。堡垒的源代码将作为文本字符串放置在汇编器的源代码中。为了避免占用过多空间,我们将其立即放置在此处的地址之后的可用内存区域中。当然,我们需要此区域进行编译,但是解释的“失控”速度将大于对新内存的需求。因此,从头开始,编译后的代码将开始覆盖要塞上的源代码,但是由于我们已经阅读并使用了本节,因此不再需要它。 fcode: .ascii " 2 2 + . quit"
但是,在该行的开头,应该放置一打空格。为了使此工作有效,我们更改了起始字节码,以便tib,#tib指向此行。最后,退出进入系统的正常命令行。起始字节码已经变成这样 start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call16 .word vhere - . - 2 .byte b_dup .byte b_call16 .word h - . - 2 .byte b_set .byte b_call16 .word definitions - . - 2 .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .word fcode_end - fcode .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_call16 .word interpret - . - 2 .byte b_quit
发射! $ ./forth 4 ( 0 ): >
太好了!
现在...用编译器编译编译器
接下来,我们在fcode行中编写代码。当然,要做的第一件事是单词“;”。 : ; ?state 110 ?pairs lit8 [ blword exit find cfa c@ c, ] c, 0 state ! exit [ current @ @ dup c@ 96 or swap c! drop
我会做一些解释。 ?state 110 ?pairs
在这里,我们检查编译状态是否已真正设置好,堆栈上是否有110,否则会错误地中断。 lit8 [ blword exit find cfa c@ c, ]
lit - exit. , exit, , . compile. , «compile exit» :)
c, 0 state !
exit ";", . "[" , immediate
,
";", .
exit [
我们已经经历过了。单词exit被编译,编译模式被关闭。一切,单词“;” 编译。还有什么进一步写的呢? current @ @ dup c@ 96 or swap c! drop
您需要为新单词设置立即标记。除了单词drop以外,这正是所指示的顺序。单词drop删除在创建开始时放置单词“:”的被遗忘的110。现在就全部了!我们启动并尝试。 $ ./forth ( 0 ): > : ^3 dup dup * * ; ( 0 ): > 6 ^3 . 216 ( 0 ): >
有!
这是我们的编译器“真正”编译的第一个词。但是我们仍然没有条件,没有循环,还有更多……让我们从一个很小但非常必要的词开始创建一个编译器:立即数。它在创建的最后一个单词上设置即时属性: : immediate current @ @ dup c@ 96 or swap c! ;
熟悉的序列:)最近,它是手动编写的,不再需要。现在让我们写一些小而有用的词: : hex 16 base ! ; : decimal 10 base ! ; : bl 32 ; : tab 9 ; : lf 10 ;
十六进制和十进制设置相应的数字系统。其余的是用于获取相应字符代码的常量。我们还说了一个复制带有计数器的行的方法:::在c @ 1+上移动cmove;现在我们将从事条件。通常,如果有一个词compile,它将看起来像这样: : if ?state compile ?nbranch8 here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate
开头的所有这些单词都会验证是否设置了编译模式,如果不是这种情况,则会生成错误。if字编译条件分支,为条件分支命令参数保留一个字节,并将该字节的地址压入堆栈。然后,它将控制值111压入堆栈,然后该字检查控制值111的存在,然后将偏移量写入堆栈中的地址。并立即说其他话。它在开始时编译无条件跳转命令以绕过else分支。好像还不知道过渡偏移,只是保留过渡偏移,并将其地址压入堆栈一样。好吧,此后,便完成了与之完全相同的操作:catch转换的地址设置为else分支。比代码本身更难描述:)如果某人想彻底理解它,则最好分解这样一个最大程度简化的代码: : if compile ?nbranch8 here 0 c, ; immediate : then dup here swap - swap c! ; immediate
好了,现在我们编写真实代码。由于我们没有编译一词,因此我们采用了与创建单词“;”相同的技巧: : if ?state lit8 [ blword ?nbranch8 find cfa c@ c, ] c, here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate : else ?state 111 ?pairs lit8 [ blword branch8 find cfa c@ c, ] c, here 0 c, swap dup here swap - swap c! 111 ; immediate
现在,您可以尝试编译条件。例如,假设有一个单词,如果堆栈上有5个,则输出1000;在其他情况下,则显示0: $ ./forth ( 0 ): > : test 5 = if 1000 . else 0 . then ; ( 0 ): > 22 test 0 ( 0 ): > 3 test 0 ( 0 ): > 5 test 1000 ( 0 ): >
显然,这样的结果不能立即生效,有错误,有调试。但最终,条件奏效了!关于过渡命令的长度有一点点偏离, , 127 . . , , . , , . 8 , 40 127 . , ?
. — 16 .
. 16 — . , , call, . , 11 ( 1023 ). 300 1000 . , . 3 , 8 . : (?nbranch), (?branch) (branch). — 24 .
我们有条件,生活会变得更轻松:)让我们说一个单词。“(点引号)。它在执行时显示指定的文本。使用这种方式: ." "
您只能在编译模式下使用此词。在我们分析这个单词的装置之后,这将变得显而易见: : ." ?state 34 word dup if lit8 [ blword (.") find cfa c@ c, ] c, str, else drop then ; immediate
该词在编译模式下执行。它从输入流到引号(34个字)取一个字符串。如果无法获得该行,则不执行任何操作。虽然,这里最好进行诊断。但是对于该行的输出,这个词正是我们正在做的事情:)如有必要,那么您可以使用诊断程序重新定义这个词。如果有可能获得该字符串,则先编译字节命令(。“),然后再接收该字符串。执行该字节命令(括号内的双引号)时,将显示编译在命令字节后面的字符串。 $ ./forth ( 0 ): > : test ." " ; ( 0 ): > test ( 0 ): >
最后,让我们编译一下这个词。显然,在编译模式下,该单词应采用流中下一个单词的名称,并在字典中找到它。然后会有一些选择:它可以是字节命令,也可以是用字节代码编写的单词。这些词必须以不同的方式进行编译。因此,我们将使用两个辅助词:“(compile_b)”和“(compile_c)”。(compile_b)将编译调用命令以调用字节码。参数将是一个64位字-被调用的字节码的地址。(compile_c)将编译字节命令。因此,该命令的参数将是一个字节-命令代码。好吧,单词compile本身将使用相应的参数编译(compile_b)或(compile_c)。让我们从(compile_c)开始,与最简单的一样: : (compile_c) r> dup c@ swap 1+ >rc, ;
尽管它很简单,我们还是先用字节码写一个单词,它本身具有参数。因此,我将发表评论。输入(compile_c)后,返回地址位于返回堆栈上,因为它不是陈旧的。这是调用命令后的下一个字节的地址。通话时的情况如下所示。 A0-调用命令代码,XX-调用命令参数-字(compile_c)字节代码的调用地址(偏移)。
返回地址指示字节NN。通常有命令下一个字节的代码。但是我们的单词有参数,因此NN只是单词“(compile_c)”的参数,即已编译命令的字节码。您需要读取该字节并通过将其移至下一个字节命令来更改返回地址。这是通过序列“ r> dup c @ swap 1+> r”完成的。此序列将返回地址从返回堆栈拉到常规堆栈,从中检索一个字节,向其添加一个字节(返回地址),然后将其返回到返回堆栈。其余命令“ c”编译从参数获得的字节命令代码。(compile_b)并不复杂: : (compile_b) r> dup @ swap 8 + >r compile_b ;
这里的一切都一样,只读取了64位参数,并且使用了compile_b来编译我们已经为编译器创建的单词。现在这个词编译了。正如已经讨论过的,它读取单词的名称,找到它的名称,然后编译前面两个命令之一。我不会对此发表评论,我们已经应用并分解了所有使用过的结构。Word编译 : compile blword over over find dup if dup c@ 128 and if cfa c@ (compile_b) [ blword (compile_c) find cfa , ] c, else cfa (compile_b) [ blword (compile_b) find cfa , ] , then drop drop else drop ." compile: " type ." - not found" then ; immediate
要检查创建的单词,我们将在其帮助下制作ifnot单词。 : ifnot ?state compile ?branch8 here 0 c, 111 ; immediate
看看吧! $ ./forth ( 0 ): > : test 5 = ifnot 1000 . else 0 . then ; ( 0 ): > 22 test 1000 ( 0 ): > 3 test 1000 ( 0 ): > 5 test 0 ( 0 ): >
一切都很好!现在是时候进行循环了……在本文中,我们将根据条件进行循环。堡垒有条件的自行车有两个选择。第一个选择是开始...直到。直到字会从堆栈中删除该值,如果该值不等于零,则循环结束。第二个选择是开始...而...重复。在这种情况下,将在执行单词while时进行检查。如果堆栈上的值为零,则退出循环。堡垒上的循环以与条件相同的方式进行-在有条件和无条件转换上。我带来了代码,我认为不需要注释。 : begin ?state here 112 ; immediate : until ?state 112 ?pairs compile ?nbranch8 here - c, ; immediate : while ?state 112 ?pairs compile ?nbranch8 here 0 c, 113 ; immediate : repeat ?state 113 ?pairs swap compile branch8 here - c, dup here swap - swap c! ; immediate
今天,我们完成了编译器。剩下的很少了。尚未实现的关键功能中只有带有计数器的循环。而且退出退出循环命令也是值得的。下次我们会做。但是我们没有经历过循环命令!我们通过写标准单词单词来做到这一点。我们最终必须看到我们的字典。为此,我们首先将单词link @。它将从字典条目中提取通信字段(偏移到上一个条目)。我们记得,通信字段的大小可以不同:1、2、4或8个字节。这个单词将把字典条目的地址放在堆栈上,并返回两个值:名称字段的地址和通讯字段的值。 : link@ dup c@ 3 and swap 1+ swap dup 0= if drop dup 1+ swap c@ else dup 1 = if drop dup 2 + swap w@ else 2 = if drop dup 4 + swap i@ else drop dup 8 + swap @ then then then ;
现在,您可以将单词改为: : words context @ @ 0 begin + dup link@ swap count type tab emit dup 0= until drop drop ;
发射中... $ ./forth ( 0 ): > words words link@ repeat while until begin ifnot compile (compile_b) (compile_c) ." else then if cmove tab bl decimal hex immediate ; bye ?state ?pairs : str, interpret $compile compile_b compile_n compile_1248 compile_c c, w, i, , allot here h test_bv test_bvc [ ] state .s >in #tib tib . #> #s 60 # hold span holdpoint holdbuf base quit execute cfa find word blword var16 var8 (.") (") count emit expect type lshift rshift invert xor or and >= <= > < = 0> 0< 0= bfind compare syscall fill move rpick r@ r> >r -! +! i! i@ w! w@ c! c@ ! @ depth roll pick over -rot rot swap drop dup abs /mod mod / * - + 1+ 1- exit ?nbranch16 ?nbranch8 ?branch16 ?branch8 branch16 branch8 call8b0 call64 call32 call16 call8f lit64 lit32 lit16 lit8 8 4 3 2 1 0 context definitions current forth ( 0 ): >
这就是我们的财富:)我想说的一切...不,我们还是可以使用堡垒程序指定一个文件作为参数进行编译和执行。我们使用syscall命令打开,关闭和读取文件。我们为它们定义必要的常数。 : file_open 0 0 0 2 syscall ; : file_close 0 0 0 0 0 3 syscall ; : file_read 0 0 0 0 syscall ; : file_O_RDONLY 0 ; : file_O_WRONLY 1 ; : file_O_RDWR 3 ;
现在,您可以将起始词_start设置为: : _start 0 pick 1 > if 2 pick file_O_RDONLY 0 file_open dup 0< if .\" error: \" . quit then dup here 32 + 32768 file_read dup 0< if .\" error: \" . quit then swap file_close drop #tib ! here 32 + tib ! 0 >in ! interpret then ;
该字将从文件中加载并执行任何要塞程序。更准确地说,解释器将执行此文件中的所有内容。例如,可能会有新单词的汇编及其执行。文件名在启动时由第一个参数指示。我不会详细介绍,但是Linux中的启动参数是通过堆栈传递的。单词_start将通过命令0 pick(参数数量)和2 pick(指向第一个参数的指针)到达它们。对于一个堡垒系统,这些值位于堆栈之外,但是您可以使用pick命令获取它们。文件大小限制为32 KB,而没有内存管理。现在仍然需要在末尾的fcode行中写: _start quit
创建文件test.f并在要塞上写一些东西。例如,用于找到最大公因子的欧几里得算法: : NOD begin over over <> while over over > if swap over - swap else over - then repeat drop ; 23101 44425 NOD . bye
我们开始。
$ ./forth test.f 1777 Bye! $
答案是正确的。这个词被编译,然后实现。显示结果,然后执行再见命令。如果删除最后两行,则将NOD单词添加到字典中,系统将转到其命令行。您已经可以编写程序了:-)仅此而已。
谁在乎,您可以从Github下载x86-64上Linux的源代码或现成的二进制文件:https : //github.com/hal9000cc/forth64源代码随附GNU GPL v2 DCH v1 许可证-做您想做的事情:-)