
Et encore une fois, j'ai surestimé le volume de l'article! J'ai prévu que ce serait le dernier article, où nous ferons un compilateur et effectuerons des tests. Mais le volume s'est avéré important et j'ai décidé de diviser l'article en deux.
Dans cet article, nous ferons presque toutes les fonctions de base du compilateur. Il prendra vie et il sera possible d'écrire, de compiler et d'exécuter du code assez sérieux. Et nous ferons des tests dans la partie suivante. (Au fait, les parties précédentes:
une ,
deux ,
trois ).
J'écris pour la première fois à Habré, ce n'est peut-être pas toujours bien. À mon avis, les articles 2, 3 se sont révélés plutôt secs, beaucoup de code, peu de description. Cette fois, je vais essayer de faire quelque chose de différent, en me concentrant sur la description des idées elles-mêmes. Eh bien, le code ... le code, bien sûr qu'il le sera! Qui veut bien comprendre, une telle opportunité sera. Dans de nombreux cas, je mettrai le code sous le spoiler. Et, bien sûr, vous pouvez toujours consulter la source complète sur le github.
Le compilateur continuera à écrire pendant un certain temps dans l'assembleur, mais ensuite aller au fort et continuer à écrire le compilateur sur nous-mêmes. Cela ressemblera au baron Munchausen, qui s'est tiré par les cheveux du marais. Mais, pour commencer, je vais décrire comment fonctionne le compilateur sur le fort. Bienvenue au chat!
Comment fonctionne le compilateur?
La mémoire du fort consiste en un fragment continu dans lequel les entrées du dictionnaire sont disposées séquentiellement. Après leur achèvement est suivi d'une zone de mémoire libre. Le premier octet libre est indiqué par la variable h. Il y a aussi le mot souvent utilisé ici, qui pousse l'adresse du premier octet libre sur la pile, il est déterminé très simplement:
: here h @ ;

Il convient de mentionner le mot allot, qui réserve le nombre spécifié d'octets en déplaçant le pointeur h. Le mot attribuer peut être défini comme suit:
: allot h +! ;
En fait, le compilateur utilise un mode interpréteur spécial et quelques mots spéciaux. Donc, avec une phrase, vous pouvez décrire tout le principe du compilateur dans le fort. Le mode de fonctionnement de l'interpréteur est déterminé par la variable d'état. S'il est nul, le mode d'exécution est défini, sinon - le mode de compilation. Nous connaissons déjà le mode d'exécution, les mots du tampon d'entrée y sont simplement exécutés l'un après l'autre. Mais en mode compilation, ils ne sont pas exécutés, mais sont compilés en mémoire par le pointeur h. Par conséquent, le pointeur avance.
Dans le fort classique, le mot "," est utilisé pour compiler une valeur entière, le mot "c", est utilisé pour compiler un octet. Notre système utilise des valeurs de différentes profondeurs de bits (8, 16, 32, 64), par conséquent, nous allons également créer les mots «w» et «i». Nous créons également le mot «str», qui compilera la chaîne, en prenant deux valeurs dans la pile - l'adresse et la longueur de la chaîne.
Des mots de compilation spéciaux sont utilisés pour former des structures de contrôle. Ce sont les mots if, do, loop, et autres. Ces mots sont exécutés même en mode compilation. Par exemple, le mot if compile une commande d'octet de branche conditionnelle (? Nbranch) à l'exécution. Pour que le système sache quels mots doivent être exécutés en mode compilation et non compilés, le drapeau immédiat (signe) est utilisé. Nous l'avons déjà dans le champ indicateur de l'entrée du dictionnaire. Dans le code source de l'assembleur, il est appelé f_immediate. Pour définir ce drapeau, utilisez le mot immédiat. Il n'a pas de paramètres, l'indicateur immédiat est positionné au dernier mot du dictionnaire.
Passons maintenant de la théorie à la pratique!
La préparation
Au début, nous devons faire quelques commandes d'octets simples en langage assembleur dont nous avons besoin. Les voici: déplacer (copier la zone mémoire), remplir (remplir la zone mémoire), opérations sur les bits (et, ou, xor, inverser), commandes de décalage des bits (rshift, lshift). Faisons le même rpick (c'est la même chose que pick, cela ne fonctionne qu'avec la pile de retour, pas la pile de données).
Ces commandes sont très simples, voici leur code 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
Encore faut-il faire le mot mot. C'est la même chose que blword, mais un délimiteur spécifique est indiqué sur la pile. Je ne fournis pas le code, il peut être trouvé dans la source. J'ai fait copier / coller les mots blworld et remplacé les commandes de comparaison.
En conclusion, nous faisons le mot syscall. Avec lui, il sera possible de faire les opérations système manquantes, par exemple, en travaillant avec des fichiers. Une telle solution ne fonctionnera pas si l'indépendance de la plateforme est requise. Mais ce système est maintenant utilisé pour les tests, alors qu'il en soit ainsi pour l'instant. Si nécessaire, toutes les opérations peuvent être converties en commandes d'octets, ce n'est pas du tout difficile. La commande syscall acceptera 6 paramètres pour l'appel système et le numéro d'appel de la pile. Il renverra un paramètre. Les affectations de paramètres et la valeur de retour sont déterminées par le numéro d'appel système.
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
Et maintenant, passons directement au compilateur.
Compilateur
Créons la variable h, tout est simple ici.
item h h: .byte b_var0 .quad 0
Nous écrirons son initialisation dans la ligne de départ:
# 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
Faisons le mot ici:
item here .byte b_call8, h - . - 1 .byte b_get .byte b_exit
Et aussi des mots pour compiler les valeurs: "allot" et "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
Faisons maintenant la variable d'état et deux mots pour contrôler sa valeur: "[" et "]". Habituellement, ces mots sont utilisés pour effectuer quelque chose au moment de la compilation. Par conséquent, le mot "[" désactive le mode de compilation et le mot "]" le active. Mais rien ne les empêche d'être utilisés dans d'autres cas lorsqu'il est nécessaire d'activer ou de désactiver le mode de compilation. Le mot "[" sera notre premier mot avec le signe immédiat. Sinon, il ne pourra pas désactiver le mode de compilation, car il sera compilé et non exécuté.
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
Le tour est venu pour le mot $ compile. Il prendra l'adresse de l'entrée du dictionnaire de la pile et compilera le mot spécifié. Pour compiler un mot dans des implémentations de Fort ordinaires, il suffit d'appliquer le mot "," à l'adresse d'exécution. Tout est beaucoup plus compliqué ici. Premièrement, il existe deux types de mots - le bytecode et le code machine. Les premiers sont compilés par octet et les seconds par la commande call byte. Et deuxièmement - nous avons jusqu'à quatre variantes de la commande d'appel: call8, call16, call32 et call64. Quatre? Non! Quand j'ai écrit le compilateur, j'en ai ajouté 16 de plus à ces quatre! :)
Comment est-ce arrivé? Nous devons faire une petite digression.
Amélioration de la commande d'appel
Lorsque le compilateur a commencé à fonctionner, j'ai constaté que dans de nombreux cas (mais pas tous) la commande call8 était suffisante. C'est lorsque le mot appelé est dans les 128 octets. J'ai pensé - et comment faire en sorte que cela se produise dans presque tous les cas? Comment mettre plus de 256 valeurs dans un octet?
Le premier point que j'ai remarqué est que dans le fort, l'appel va toujours vers des adresses plus basses. Cela signifie que vous pouvez refaire la commande d'appel de telle manière qu'elle ne puisse appeler que des adresses inférieures, mais pour 256 octets, pas 128. C'est mieux.
Mais si vous mettez quelques morceaux quelque part ... Il s'avère qu'il y a où! Nous avons deux octets: un octet est la commande, le second est l'offset. Mais rien n'empêche les bits inférieurs de la commande de placer les bits hauts du paramètre (offset). Pour une machine à octets, il semble qu'au lieu d'une commande d'appel, il y en ait plusieurs. Oui, de cette façon, nous occupons plusieurs cellules de la table de code d'octet-commande avec une seule commande, mais parfois cela vaut la peine. La commande d'appel est l'une des commandes les plus utilisées, j'ai donc décidé de mettre 4 bits de décalage dans la commande. Ainsi, vous pouvez effectuer un appel à une distance pouvant atteindre 4095 octets! Cela signifie qu'une commande d'appel aussi courte sera utilisée presque toujours. J'ai placé ces commandes avec le code 0xA0 et les lignes suivantes sont apparues dans le tableau de commandes:
.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
La première de ces commandes d'octets effectue simplement un appel en direction d'adresses inférieures à l'offset spécifié dans le paramètre (jusqu'à 255). Les autres ajoutent le décalage correspondant au paramètre. bcmd_call8b1 ajoute 256, bcmd_call8b2 ajoute 512, et ainsi de suite. J'ai fait la première commande d'appel séparément, le reste avec une macro.
Première commande:
b_call8b0 = 0xA0 bcmd_call8b0: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 sub r8, rax jmp _next
Macro et création du reste des commandes d'appel:
.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
Eh bien, j'ai refait l'ancienne commande call8 pour faire un renvoi, car nous avons déjà 16 équipes qui font un rappel. Quelle que soit la confusion, je l'ai renommé 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
Soit dit en passant, pour plus de commodité, j'ai créé une macro qui, dans l'assembleur, compile automatiquement l'appel correspondant dans 4095. Et je n'en ai jamais eu besoin :)
.macro callb adr .if \adr > . .error "callb do not for forward!" .endif .byte b_call8b0 + (. - \adr + 1) >> 8 .byte (. - \adr + 1) & 255 .endm
Et maintenant ...
Compilation d'équipe
Nous obtenons donc un algorithme de compilation de commandes assez compliqué. S'il s'agit d'une commande d'octets, compilez juste un octet (code de commande d'octets). Et si ce mot est déjà écrit en bytecode, vous devez compiler son appel avec la commande call, en choisissant un parmi vingt. Plus précisément 19, nous n'avons donc pas de renvoi d'appel, et call8f ne sera pas utilisé pour le fort.
Donc, le choix est le suivant. Si le décalage est compris entre 0 et 4095, sélectionnez la commande bcmd_call8b avec le code 0xA0, en plaçant les quatre bits de décalage les plus significatifs dans les bits les moins significatifs de la commande. Dans le même temps, pour la machine d'octets, le code de l'une des commandes bcmd_call8b0 est bcmd_call8b15.
Si le décalage vers l'arrière est supérieur ou égal à 4095, nous déterminons dans quelle dimension le décalage est placé et utilisons la commande appropriée à partir de call16 / 32/64. Il ne faut pas oublier que la compensation pour ces équipes est signée. Ils peuvent provoquer à la fois en avant et en arrière. Par exemple, call16 peut appeler une distance de 32767 dans les deux sens.
Voici l'implémentation en conséquence:
$ compilerCompile un mot. En paramètre, prend l'adresse de l'entrée du dictionnaire du mot compilé. En fait, il vérifie l'indicateur f_code, calcule l'adresse de code (cfa) et appelle compile_b ou compile_c (si l'indicateur est défini).
compile_cCompile une commande d'octets. Le mot le plus simple ici est décrit sur le fort comme ceci:
: compile_c c@ c, ;
compile_bIl prend une adresse de bytecode sur la pile et compile son appel.
test_bvIl prend un décalage par rapport à la pile (avec un signe) et détermine la profondeur de bits à utiliser (1, 2, 4 ou 8 octets). Renvoie la valeur 0, 1, 2 ou 3. En utilisant ce mot, vous pouvez déterminer celui à utiliser à partir des commandes call16 / 32/64. Ce mot sera utile lors de la compilation de nombres (un choix parmi lit8 / 16/32/64).
Au fait, vous pouvez démarrer le système et «jouer» dans la console du fort avec l'un de ces mots. Par exemple:
$ ./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_bvcIl prend un décalage (avec un signe) de la pile et détermine la commande d'appel à utiliser. En fait, il vérifie si le décalage se situe dans la plage de 0 à -4095 et renvoie 0. Dans ce cas, s'il n'y a pas de résultat dans cet intervalle, il appelle test_bv.
C'est tout ce qu'il faut pour compiler la commande. # : 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
Maintenant, nous devons compiler le nombre.
Compilation d'un nombre (littéral)
A écrit un sous-titre entier, préparé pour décrire spécifiquement la compilation du littéral, mais il s'avère qu'il n'y a rien de spécial à décrire :)
Nous avons déjà fait la moitié du travail dans le mot test_bv. Il ne reste plus qu'à appeler test_bv, et, selon le résultat, compiler lit8 / 16/32/64, puis la valeur correspondante de 1, 2, 4 ou 8 octets.
Nous le faisons en définissant le mot 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
Modifier l'interprète
Tout est prêt pour compiler la commande et les littéraux. Maintenant, il doit être intégré à l'interpréteur. Cette modification est simple. Où la commande a été exécutée, ajoutez la vérification d'état. Si l'état n'est pas nul et que le mot ne contient pas le drapeau immédiat, au lieu de l'exécution, vous devez appeler $ compile. Et à peu près la même chose à faire lorsque le nombre est obtenu à partir du flux d'entrée. Si l'état est nul, laissez simplement le numéro sur la pile et sinon, appelez compile_n.
Voici l'interprète 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
Maintenant, nous sommes à un pas du compilateur ...
Définition de nouveaux mots (mot ":")
Maintenant, si nous définissons la variable d'état sur une valeur non nulle, le processus de compilation commencera. Mais le résultat sera inutile, nous ne pourrons ni le réaliser, ni même le retrouver en mémoire. Pour permettre de faire tout cela, il faut formater le résultat de la compilation sous la forme d'un article de dictionnaire. Pour ce faire, avant d'activer le mode de compilation, vous devez créer un titre pour le mot.
L'en-tête doit contenir des drapeaux, un champ de communication et un nom. Ici, nous avons une histoire familière - le champ de communication peut être de 1, 2, 4 ou 8 octets. Faisons le mot compile_1248, qui nous aidera à former un tel champ de communication. Il faudra deux nombres sur la pile - l'offset et la valeur générée par la commande test_bv.
compile_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
Maintenant, créez le mot $ create. Il nous sera utile plus d'une fois. Vous pouvez l'utiliser chaque fois que vous devez créer un titre pour une entrée de dictionnaire. Il prendra deux valeurs de la pile - l'adresse du nom du mot créé et sa longueur. Après avoir exécuté ce mot, l'adresse de l'entrée de dictionnaire créée apparaîtra sur la pile.
$ créer # : $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
Le mot suivant récupérera le nom du nouveau mot dans le flux d'entrée en utilisant le mot blword et appellera $ create, créant un nouveau mot avec le nom spécifié.
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
Et enfin, faites le mot ":". Il va créer un nouveau mot en utilisant create_in et définir le mode de compilation, il n'est pas installé. Et s'il est installé, il donne une erreur. Le mot ":" aura le signe immédiat.
le mot: # : : 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
Si quelqu'un a regardé le code, il a vu que ce mot fait autre chose :)
Et voici 110 ???
Oui, ce mot pousse également le nombre 110 sur la pile, et c'est pourquoi. Une fois compilées, les différentes constructions doivent être un seul ensemble. Par exemple, après si doit être alors. Et le mot créé en utilisant ":" devrait se terminer par ";". Pour vérifier ces conditions, des mots spéciaux du compilateur mettent certaines valeurs sur la pile et vérifient leur présence. Par exemple, le mot ":" met la valeur 110 et le mot ";" vérifie que 110 se trouve en haut de la pile. Si ce n'est pas le cas, c'est une erreur. Ainsi, les structures de contrôle n'étaient pas appariées.
Une telle vérification est effectuée dans tous ces mots du compilateur, par conséquent, nous allons faire un mot spécial pour cela - "? Paires". Il prendra deux valeurs de la pile et générera une erreur si elles ne sont pas égales.
De plus, en de tels mots, vous devez souvent vérifier que le mode de compilation est défini. Faisons le mot "? État" pour cela.
? paires? état #: ?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
C’est tout! Nous ne compilerons rien d'autre dans l'assembleur manuellement :)
Mais jusqu'à la fin, le compilateur n'a pas encore été écrit, donc au début vous devrez utiliser des méthodes inhabituelles ...
Préparons-nous à compiler le compilateur créé avec le compilateur créé
Pour commencer, vous pouvez vérifier le fonctionnement du mot ":" en compilant quelque chose de simple. Faisons, par exemple, le mot:
: ^2 dup * ;
Ce mot est quadratique. Mais nous n'avons pas le mot ";" que faire?
Nous écrivons le mot exit à la place et il se compile. Et puis désactivez le mode de compilation avec le mot "[" et supprimez la valeur 110: $ ./forth ( 0 ): > : ^2 dup * exit [ drop ( 0 ): > 4 ^2 ( 1 ): 16 >
Ça marche! Continuons...Puisque nous continuerons d'écrire le fort sur le fort, nous devons penser à où sera le code source du fort, et quand le compiler. Faisons l'option la plus simple. Le code source du fort sera placé dans le code source dans l'assembleur, sous forme de chaîne de texte. Et pour qu'il ne prenne pas trop de place, nous le placerons immédiatement après l'adresse ici, dans la zone mémoire libre. Bien sûr, nous avons besoin de cette zone pour la compilation, mais la vitesse de «fugue» de l'interprétation sera supérieure au besoin de nouvelle mémoire. Ainsi, le code compilé commencera à écraser la source sur le fort, à partir du début, mais nous n'en aurons plus besoin, car nous avons déjà lu et utilisé cette section. fcode: .ascii " 2 2 + . quit"
Mais, au début de la ligne, cela vaut la peine de placer une douzaine d'espaces.Pour que cela fonctionne, nous modifions le bytecode de départ afin que tib, #tib pointe vers cette ligne. À la fin, il y a un arrêt pour entrer dans la ligne de commande normale du système.Le démarrage du bytecode est devenu comme ça 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
Lancez! $ ./forth 4 ( 0 ): >
Super!
Et maintenant ...Compilez le compilateur avec le compilateur
Ensuite, nous écrivons le code dans la ligne fcode. La première chose à faire, bien sûr, est le mot ";". : ; ?state 110 ?pairs lit8 [ blword exit find cfa c@ c, ] c, 0 state ! exit [ current @ @ dup c@ 96 or swap c! drop
Je ferai quelques explications. ?state 110 ?pairs
Ici on vérifie que l'état de compilation est bien défini, et 110 est sur la pile, sinon il y aura une interruption par erreur. lit8 [ blword exit find cfa c@ c, ]
Ceci nous compilons la commande allumée avec le bytecode de la commande exit. J'ai dû passer en mode d'exécution, trouver le mot exit, obtenir l'adresse d'exécution et récupérer le code de commande à partir de là. Tout cela était nécessaire car nous n'avons pas encore le mot compiler. Si c'était le cas, au lieu de tout cela, il suffirait d'écrire simplement «compiler exit» :) c, 0 state !
Cela compilera la commande exit lorsque le mot ";" est exécuté, puis le mode d'interprétation sera défini. Le mot "[" ne peut pas être utilisé ici, car il a le signe immédiat et est exécuté maintenant , mais nous devons compiler ces commandes dans le mot ";" afin qu'elles désactivent le mode de compilation. exit [
Nous l'avons déjà vécu. Le mot exit est compilé et le mode de compilation est désactivé. Tout, le mot ";" compilé. Et quoi d'autre est écrit là-bas? current @ @ dup c@ 96 or swap c! drop
Vous devez définir le drapeau immédiat pour le nouveau mot. C'est exactement ce que fait la séquence indiquée, à l'exception du mot drop. Le mot drop supprime le 110 oublié qui a placé le mot ":" au début de la création.Maintenant c'est tout!Nous lançons et essayons. $ ./forth ( 0 ): > : ^3 dup dup * * ; ( 0 ): > 6 ^3 . 216 ( 0 ): >
Voilà!
C'est le premier mot que notre compilateur a compilé «pour de vrai».Mais nous n'avons toujours pas de conditions, pas de boucles, et bien plus encore ... Commençons par un petit mais très nécessaire mot pour créer un compilateur: immédiat. Il définit l'attribut immédiat sur le dernier mot créé: : immediate current @ @ dup c@ 96 or swap c! ;
Une séquence familière :) Récemment, elle a été écrite manuellement, elle ne sera plus nécessaire.Faisons maintenant quelques mots petits mais utiles: : hex 16 base ! ; : decimal 10 base ! ; : bl 32 ; : tab 9 ; : lf 10 ;
hex et décimal définissent le système numérique correspondant. Les autres sont des constantes permettant d'obtenir les codes de caractères correspondants.Nous créons également un mot pour copier une ligne avec un compteur:: cmove sur c @ 1+ move;Et maintenant, nous serons engagés dans des conditions. En général, s'il y avait un mot compilé, il ressemblerait à ceci: : if ?state compile ?nbranch8 here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate
Tous ces mots au début vérifient que le mode de compilation est défini et génèrent une erreur si ce n'est pas le cas.Le mot if compile une branche conditionnelle, réserve un octet pour le paramètre de commande de branche conditionnelle et pousse l'adresse de cet octet sur la pile. Il pousse ensuite la valeur de contrôle 111 sur la pile.Le mot vérifie alors la présence de la valeur de contrôle 111, puis écrit le décalage dans l'adresse sur la pile.Et faites immédiatement le mot d'autre. Au début, il compile la commande de saut inconditionnel pour contourner la branche else. De la même manière que si, le décalage de transition n'est pas encore connu, il est simplement réservé et son adresse est poussée sur la pile. Eh bien, après cela, exactement la même chose se fait qu'avec then: l'adresse de la transition catch est définie sur la branche else. Quelque chose est plus difficile à décrire que le code lui-même :) Si quelqu'un veut le comprendre à fond, il vaut mieux analyser le travail d'un tel code simplifié au maximum: : if compile ?nbranch8 here 0 c, ; immediate : then dup here swap - swap c! ; immediate
Eh bien, maintenant nous programmons le vrai code. Comme nous n'avons pas le mot compiler, nous appliquons la même astuce que lors de la création du mot ";": : 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
Vous pouvez maintenant essayer de compiler la condition. Faisons, par exemple, un mot qui imprime 1000 s'il y en a 5 sur la pile, et 0 dans les autres cas: $ ./forth ( 0 ): > : test 5 = if 1000 . else 0 . then ; ( 0 ): > 22 test 0 ( 0 ): > 3 test 0 ( 0 ): > 5 test 1000 ( 0 ): >
Il est clair qu'un tel résultat n'a pas fonctionné tout de suite, il y a eu des erreurs, il y a eu un débogage. Mais au final, les conditions ont fonctionné!Une petite digression sur la longueur des commandes de transition, , 127 . . , , . , , . 8 , 40 127 . , ?
. — 16 .
. 16 — . , , call, . , 11 ( 1023 ). 300 1000 . , . 3 , 8 . : (?nbranch), (?branch) (branch). — 24 .
Nous avons des conditions, la vie devient plus facile :)Faisons un mot. "(Point-quote). Il affiche le texte spécifié lors de son exécution. Il est utilisé de cette façon: ." "
Vous ne pouvez utiliser ce mot qu'en mode compilation. Cela deviendra apparent après avoir analysé le dispositif de ce mot: : ." ?state 34 word dup if lit8 [ blword (.") find cfa c@ c, ] c, str, else drop then ; immediate
Ce mot est exécuté en mode compilation. Il prend une chaîne du flux d'entrée jusqu'aux guillemets (34 mots). Si la ligne n'a pas pu être obtenue, elle ne fait rien. Bien, ici, il serait préférable de dériver un diagnostic. Mais pour la sortie de la ligne, ce mot est exactement ce que nous faisons :) Si nécessaire, vous pouvez à nouveau redéfinir ce mot, déjà avec des diagnostics.S'il était possible d'obtenir la chaîne, la commande d'octets (. ") Est compilée, puis la chaîne reçue. Cette commande d'octets (guillemets pointillés entre crochets), lorsqu'elle est exécutée, affiche la chaîne qui a été compilée derrière l'octet de commande.Vérifiez. $ ./forth ( 0 ): > : test ." " ; ( 0 ): > test ( 0 ): >
Et enfin, faisons compiler le mot.Il est clair qu'en mode compilation ce mot doit prendre le nom du mot suivant du flux, le trouver dans le dictionnaire. Et puis il y aura des options: ce peut être une commande d'octets, ou ce peut être un mot écrit en code octet. Ces mots doivent être compilés de différentes manières. Par conséquent, nous allons créer deux mots auxiliaires: "(compile_b)" et "(compile_c)".(compile_b) compilera la commande d'appel pour appeler le bytecode. Le paramètre sera un mot de 64 bits - l'adresse du bytecode appelé.(compile_c) compilera la commande byte. Par conséquent, le paramètre de cette commande sera un octet - le code de commande.Eh bien, le mot compiler lui-même compilera (compile_b) ou (compile_c) avec les paramètres correspondants.Commençons par (compile_c),comme avec le plus simple: : (compile_c) r> dup c@ swap 1+ >rc, ;
Malgré sa simplicité, nous écrivons d'abord un mot en bytecode, qui en lui-même a des paramètres. Je vais donc commenter. Après avoir entré (compile_c), l'adresse de retour se trouve sur la pile de retour, car elle n'est pas banale. Il s'agit de l'adresse de l'octet suivant après la commande d'appel. La situation au moment de l'appel est indiquée ci-dessous. A0 - code de commande d'appel, XX - paramètre de commande d'appel - adresse d'appel (décalage) du code d'octet du mot (compile_c).
L'adresse de retour indique l'octet NN. Il existe généralement le code de l'octet suivant de la commande. Mais notre mot a des paramètres, donc NN n'est que les paramètres du mot "(compile_c)", à savoir le code d'octet de la commande compilée. Vous devez lire cet octet et modifier l'adresse de retour en la déplaçant vers la commande d'octet suivante. Cela se fait par la séquence «r> dup c @ swap 1+> r». Cette séquence extrait l'adresse de retour de la pile de retour vers la pile normale, en extrait un octet, y ajoute un (adresse de retour) et la renvoie à la pile de retour. La commande restante «c» compile le code de commande octet obtenu à partir des paramètres.(compile_b) n'est pas beaucoup plus compliqué: : (compile_b) r> dup @ swap 8 + >r compile_b ;
Ici, tout est pareil, seul le paramètre 64 bits est lu et le mot compile_b est utilisé pour compiler le mot que nous avons déjà créé pour le compilateur.Et maintenant, le mot compile. Comme déjà discuté, il lit le nom du mot, le trouve et compile l'une des deux commandes précédentes. Je ne le commenterai pas, nous avons déjà appliqué et démonté toutes les constructions utilisées.Compilation de mots : 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
Pour vérifier le mot créé, nous créons, avec son aide, le mot ifnot. : ifnot ?state compile ?branch8 here 0 c, 111 ; immediate
Découvrez-le! $ ./forth ( 0 ): > : test 5 = ifnot 1000 . else 0 . then ; ( 0 ): > 22 test 1000 ( 0 ): > 3 test 1000 ( 0 ): > 5 test 0 ( 0 ): >
Tout va bien! Et il est temps de faire des cycles ...Dans cet article nous allons faire des cycles avec une condition. Le fort a deux options pour un cycle avec une condition.La première option est de commencer ... jusqu'à. Le mot jusqu'à ce que supprime la valeur de la pile, et si elle n'est pas égale à zéro, le cycle se termine.La deuxième option est commencer ... tandis que ... répéter. Dans ce cas, la vérification se produit lorsque le mot while est exécuté. La boucle se termine si la valeur sur la pile est nulle.Les cycles sur le fort se font de la même manière que les conditions - sur les transitions conditionnelles et inconditionnelles. J'apporte le code, les commentaires, je pense, ne sont pas nécessaires. : 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
Aujourd'hui, nous avons terminé avec le compilateur. Il en reste très peu. Des fonctions clés qui n'ont pas encore été implémentées ne sont que des cycles avec un compteur. Et cela vaut également la peine de quitter la commande de boucle de sortie. Nous le ferons la prochaine fois.Mais nous n'avons pas expérimenté la commande de cycle!Nous le faisons en écrivant les mots standard. Il faut enfin voir notre dictionnaire.Pour ce faire, au début, nous faisons le mot lien @. Il extraira le champ de communication de l'entrée du dictionnaire (décalé par rapport à l'entrée précédente). Comme nous nous en souvenons, le champ de communication peut avoir une taille différente: 1, 2, 4 ou 8 octets. Ce mot prendra sur la pile l'adresse de l'entrée du dictionnaire et renverra deux valeurs: l'adresse du champ de nom et la valeur du champ de communication. : 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 ;
Et maintenant, vous pouvez créer les mots: : words context @ @ 0 begin + dup link@ swap count type tab emit dup 0= until drop drop ;
Lancement ... $ ./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 ): >
Ça y est, notre richesse :)Je voulais tout dire ... non, permettons néanmoins de spécifier un fichier avec un programme fort pour la compilation et l'exécution en paramètre.Nous faisons des commandes syscall pour ouvrir, fermer et lire le fichier. Nous définissons les constantes qui leur sont nécessaires. : 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 ;
Vous pouvez maintenant créer le mot de départ _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 ;
Ce mot se chargera du fichier et exécutera n'importe quel programme fort. Plus précisément, l'interpréteur exécutera tout ce qui sera dans ce fichier. Et il peut y avoir, par exemple, une compilation de nouveaux mots et leur exécution. Le nom du fichier est indiqué par le premier paramètre au démarrage. Je n'entrerai pas dans les détails, mais les paramètres de lancement sous Linux sont passés à travers la pile. Le mot _start les atteindra avec les commandes 0 pick (nombre de paramètres) et 2 pick (pointeur sur le premier paramètre). Pour un système de fort, ces valeurs se trouvent en dehors de la pile, mais vous pouvez les obtenir avec la commande pick. La taille du fichier est limitée à 32 Ko, alors qu'il n'y a pas de gestion de la mémoire.Il reste maintenant à écrire dans la ligne fcode à la fin: _start quit
Créez un fichier test.f et écrivez quelque chose sur le fort. Par exemple, l'algorithme euclidien pour trouver le plus grand facteur commun: : NOD begin over over <> while over over > if swap over - swap else over - then repeat drop ; 23101 44425 NOD . bye
Nous commençons.
$ ./forth test.f 1777 Bye! $
La réponse est correcte. Le mot a été compilé, puis rempli. Le résultat s'affiche, puis la commande bye a été exécutée. Si vous supprimez les deux dernières lignes, le mot NOD sera ajouté au dictionnaire et le système ira à sa ligne de commande. Vous pouvez déjà écrire des programmes :-)C’est tout.
Peu importe, vous pouvez télécharger la source ou le binaire prêt à l'emploi pour Linux sur x86-64 depuis Github: https://github.com/hal9000cc/forth64 Lessources sont livrées avec une licence GNU GPL v2 DCH v1 - Faites ce que vous voulez :-)