Byte-machine para o forte (e não apenas) no nativo americano (parte 4)

Fort Byte Carro (e mais) Nativo Americano

E novamente superestimei o volume do artigo! Planejei que este seria o artigo final, onde faremos um compilador e faremos o teste. Mas o volume acabou sendo grande e eu decidi dividir o artigo em dois.

Neste artigo, faremos quase todas as funções básicas do compilador. Ele ganhará vida e será possível escrever, compilar e executar um código bastante sério. E faremos testes na próxima parte. (A propósito, as partes anteriores: um , dois , três ).

Estou escrevendo pela primeira vez em Habré; talvez nem sempre esteja certo. Na minha opinião, os artigos 2, 3 revelaram-se bastante secos, muito código, pouca descrição. Desta vez, tentarei fazer algo diferente, enfocando a descrição das próprias idéias. Bem, o código ... o código, é claro que sim! Quem quer entender completamente, essa oportunidade será. Em muitos casos, colocarei o código sob o spoiler. E, é claro, você sempre pode procurar a fonte completa no github.

O compilador continuará escrevendo por algum tempo no assembler, mas depois vá para o forte e continuará escrevendo o compilador em nós mesmos. Isso se parecerá com o barão Munchausen, que se puxou pelos cabelos do pântano. Mas, para começar, descreverei como o compilador do forte funciona. Bem-vindo ao gato!

Como o compilador funciona?


A memória no forte consiste em um fragmento contínuo no qual as entradas do dicionário são organizadas em sequência. Após a conclusão, é seguida por uma área de memória livre. O primeiro byte livre é indicado pela variável h. Há também a palavra frequentemente usada aqui, que envia o endereço do primeiro byte livre da pilha, é determinada de maneira muito simples:

: here h @ ; 



Vale mencionar a palavra allot, que reserva o número especificado de bytes movendo o ponteiro h. A palavra allot pode ser definida da seguinte maneira:

 : allot h +! ; 

De fato, o compilador usa um modo de intérprete especial mais algumas palavras especiais. Assim, com uma frase, você pode descrever todo o princípio do compilador no forte. O modo em que o intérprete trabalha é determinado pela variável de estado. Se for zero, o modo de execução é definido, caso contrário - modo de compilação. Já estamos familiarizados com o modo de execução, nele as palavras do buffer de entrada são simplesmente executadas uma após a outra. Mas no modo de compilação, eles não são executados, mas são compilados na memória pelo ponteiro h. Por conseguinte, o ponteiro avança.

No forte clássico, a palavra "," é usada para compilar um valor inteiro, a palavra "c" é usada para compilar um byte. Nosso sistema utiliza valores de diferentes profundidades de bits (8, 16, 32, 64); portanto, criaremos adicionalmente as palavras "w" e "i". Também criamos a palavra "str", que irá compilar a string, obtendo dois valores da pilha - o endereço e o comprimento da string.

Palavras especiais do compilador são usadas para formar estruturas de controle. Essas são as palavras if, then, do, loop e outras. Essas palavras são executadas mesmo no modo de compilação. Por exemplo, a palavra se compila um comando condicional de byte de ramificação (? Nbranch) na execução. Para que o sistema saiba quais palavras precisam ser executadas no modo de compilação, e não compiladas, o sinalizador imediato (sinal) é usado. Já o temos no campo sinalizador da entrada do dicionário. No código-fonte do assembler, é chamado f_immediate. Para definir esse sinalizador, use a palavra imediato. Não possui parâmetros, o sinalizador imediato é definido na última palavra do dicionário.

Agora vamos passar da teoria para a prática!

Preparação


No começo, precisamos executar alguns comandos simples de byte na linguagem assembly que precisamos. Aqui estão eles: mover (copiar a área de memória), preencher (preencher a área de memória), operações de bits (e, ou, xor, invert), comandos de troca de bits (rshift, lshift). Vamos fazer o mesmo rpick (isso é o mesmo que pick, ele só funciona com a pilha de retorno, não com a pilha de dados).

Esses comandos são muito simples, aqui está o código deles
 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 

Ainda precisa fazer a palavra palavra. É o mesmo que blword, mas um delimitador específico é indicado na pilha. Não forneço o código, ele pode ser encontrado na fonte. Fiz copiar / colar as palavras blworld e substitui os comandos de comparação.

Em conclusão, criamos a palavra syscall. Com ele, será possível executar as operações ausentes do sistema, por exemplo, trabalhando com arquivos. Essa solução não funcionará se a independência da plataforma for necessária. Mas esse sistema agora é usado para testes, então deixe que seja por enquanto. Se necessário, todas as operações podem ser convertidas em comandos de byte, não é nada difícil. O comando syscall aceitará 6 parâmetros para a chamada do sistema e o número da chamada da pilha. Ele retornará um parâmetro. As atribuições de parâmetros e o valor retornado são determinados pelo número de chamada do sistema.

 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 

E agora vamos prosseguir diretamente para o compilador.

Compilador


Vamos criar a variável h, tudo é simples aqui.

  item h h: .byte b_var0 .quad 0 
Vamos escrever sua inicialização na linha de partida:
 # 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 

Vamos fazer a palavra aqui:

  item here .byte b_call8, h - . - 1 .byte b_get .byte b_exit 

E também palavras para compilar os valores: "allot" e "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 

Agora vamos fazer a variável state e duas palavras para controlar seu valor: "[" e "]". Normalmente, essas palavras são usadas para executar algo no momento da compilação. Portanto, a palavra "[" desativa o modo de compilação e a palavra "]" o ativa. Mas nada impede que eles sejam usados ​​em outros casos quando é necessário ativar ou desativar o modo de compilação. A palavra "[" será a nossa primeira palavra com o sinal imediato. Caso contrário, ele não poderá desativar o modo de compilação, pois será compilado, não executado.

  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 

Chegou a vez da palavra $ compile. Ele pegará o endereço da entrada do dicionário da pilha e compilará a palavra especificada. Para compilar uma palavra nas implementações comuns do Fort, basta aplicar a palavra "," ao endereço de execução. Tudo é muito mais complicado aqui. Primeiramente, existem dois tipos de palavras - bytecode e código de máquina. Os primeiros são compilados por byte e os últimos pelo comando call byte. E em segundo lugar - temos até quatro variantes do comando call: call8, call16, call32 e call64. Quatro? Não! Quando escrevi o compilador, adicionei mais 16 a esses quatro! :)

Como isso aconteceu? Temos que fazer uma pequena digressão.

Melhorando o Comando de Chamada


Quando o compilador começou a funcionar, descobri que em muitos casos (mas não em todos) o comando call8 é suficiente. É quando a palavra chamada está dentro de 128 bytes. Eu pensei - e como garantir que isso aconteça em quase todos os casos? Como colocar mais de 256 valores em um byte?
O primeiro ponto que notei foi que, no forte, a ligação sempre vai para endereços mais baixos. Isso significa que você pode refazer o comando de chamada de forma que ele possa chamar apenas endereços inferiores, mas com 256 bytes, não com 128. É melhor.

Mas se você colocar alguns bits em algum lugar ... Acontece que é onde! Temos dois bytes: um byte é o comando, o segundo é o deslocamento. Mas nada impede que os bits mais baixos do comando coloquem os bits altos do parâmetro (deslocamento). Para uma máquina de bytes, parece que, em vez de um comando de chamada, existem vários. Sim, dessa maneira ocupamos várias células da tabela de códigos de comando de bytes com um comando, mas às vezes vale a pena fazê-lo. O comando call é um dos comandos mais usados, então decidi colocar 4 bits de deslocamento no comando. Assim, você pode fazer uma chamada a uma distância de até 4095 bytes! Isso significa que um comando de chamada curta será usado quase sempre. Coloquei esses comandos com o código 0xA0 e as seguintes linhas apareceram na tabela de comandos:

 .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 

O primeiro desses comandos de byte simplesmente faz uma chamada na direção de endereços inferiores no deslocamento especificado no parâmetro (até 255). O restante adiciona o deslocamento correspondente ao parâmetro. bcmd_call8b1 adiciona 256, bcmd_call8b2 adiciona 512 e assim por diante. Fiz o primeiro comando de chamada separadamente, o restante com uma macro.

Primeiro comando:

 b_call8b0 = 0xA0 bcmd_call8b0: movzx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 sub r8, rax jmp _next 

Macro e criando o restante dos comandos de chamada:

 .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 

Bem, refiz o antigo comando call8 para encaminhar chamadas, já que já temos 16 equipes fazendo uma chamada de volta. Qualquer que seja a confusão, eu a renomei 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 

A propósito, por conveniência, criei uma macro que no assembler compila automaticamente a chamada correspondente de volta em 4095. E então eu nunca precisei :)

 .macro callb adr .if \adr > . .error "callb do not for forward!" .endif .byte b_call8b0 + (. - \adr + 1) >> 8 .byte (. - \adr + 1) & 255 .endm 

E agora ...

Compilação da equipe


Portanto, obtemos um algoritmo de compilação de comandos bastante complicado. Se este for um comando byte, compile apenas um byte (código de comando do byte). E se essa palavra já estiver escrita em código de bytes, você precisará compilar sua chamada com o comando call, escolhendo uma das vinte. Mais precisamente 19, portanto, não temos encaminhamento de chamada, e o call8f não será usado para o forte.

Então a escolha é essa. Se o deslocamento estiver dentro de 0 ...- 4095, selecione o comando bcmd_call8b com o código 0xA0, colocando os quatro bits de deslocamento mais significativos nos bits menos significativos do comando. Ao mesmo tempo, para a máquina de bytes, o código para um dos comandos bcmd_call8b0 é bcmd_call8b15.

Se o deslocamento para trás for maior ou igual a 4095, determinaremos em qual dimensão o deslocamento é colocado e usaremos o comando apropriado da chamada 16 / 32/64. Deve-se ter em mente que a compensação para essas equipes é assinada. Eles podem causar tanto para a frente quanto para trás. Por exemplo, call16 pode chamar uma distância de 32767 em ambas as direções.

Aqui está a implementação como resultado:

$ compilar

Compila uma palavra. Como parâmetro, pega o endereço da entrada de dicionário da palavra compilada. De fato, ele verifica o sinalizador f_code, calcula o endereço do código (cfa) e chama compile_b ou compile_c (se o sinalizador estiver definido).

compile_c

Compila um comando byte. A palavra mais simples aqui é descrita no forte assim:

 : compile_c c@ c, ; 

compile_b
Ele pega um endereço de bytecode na pilha e compila sua chamada.

test_bv

Ele pega um deslocamento da pilha (com um sinal) e determina qual profundidade de bit usar (1, 2, 4 ou 8 bytes). Retorna o valor 0, 1, 2 ou 3. Usando esta palavra, você pode determinar qual delas usar nos comandos call16 / 32/64. Essa palavra será útil ao compilar números (uma opção entre lit8 / 16/32/64).

A propósito, você pode iniciar o sistema e "brincar" no console do forte com qualquer uma dessas palavras. Por exemplo:

 $ ./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

Ele pega um deslocamento (com um sinal) da pilha e determina qual comando de chamada usar. De fato, ele verifica se o deslocamento está entre 0 ... -4095 e retorna 0. Nesse caso, se não houver acerto nesse intervalo, ele chama test_bv.

É o suficiente para compilar o comando.
 # : 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 


Agora precisamos compilar o número.

Compilando um número (literal)


Escreveu uma legenda inteira, preparada para descrever especificamente a compilação do literal, mas acontece que não há nada especial para descrever :)

Já fizemos metade do trabalho na palavra test_bv. Resta apenas chamar test_bv e, dependendo do resultado, compilar lit8 / 16/32/64 e, em seguida, o valor correspondente de 1, 2, 4 ou 8 bytes de tamanho.

Fazemos isso definindo a palavra 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 

Modifique o intérprete


Tudo está pronto para compilar o comando e os literais. Agora ele precisa ser incorporado ao intérprete. Essa modificação é simples. Onde o comando foi executado, adicione a verificação de estado. Se state não for nulo e a palavra não contiver o sinalizador imediato, em vez da execução, você precisará chamar $ compile. E quase a mesma coisa a fazer onde o número é obtido a partir do fluxo de entrada. Se o estado for zero, deixe o número na pilha e, se não, chame compile_n.

Aqui está o intérprete
  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 

Agora estamos a um passo do compilador ...

Definição de novas palavras (palavra ":")


Agora, se definirmos a variável de estado como um valor diferente de zero, o processo de compilação começará. Mas o resultado será inútil, não podemos cumpri-lo nem encontrá-lo na memória. Para possibilitar tudo isso, é necessário formatar o resultado da compilação na forma de um artigo de dicionário. Para fazer isso, antes de ativar o modo de compilação, você precisa criar um título para a palavra.

O cabeçalho deve conter bandeiras, um campo de comunicação e um nome. Aqui temos uma história familiar - o campo de comunicação pode ser de 1, 2, 4 ou 8 bytes. Vamos criar a palavra compile_1248, que nos ajudará a formar esse campo de comunicação. Serão necessários dois números na pilha - o deslocamento e o valor gerado pelo comando 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 

Agora faça a palavra $ create. Será útil para nós mais de uma vez. Você pode usá-lo sempre que precisar criar um título para uma entrada do dicionário. Serão necessários dois valores da pilha - o endereço do nome da palavra criada e seu comprimento. Depois de executar esta palavra, o endereço da entrada de dicionário criada aparecerá na pilha.

$ create
 # : $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 

A próxima palavra selecionará o nome da nova palavra no fluxo de entrada usando a palavra blword e chamará $ create, criando uma nova palavra com o nome especificado.

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 

E, finalmente, faça a palavra ":". Ele criará uma nova palavra usando create_in e definirá o modo de compilação, não está instalado. E se instalado, dá um erro. A palavra ":" terá o sinal imediato.

a palavra:
 # : : 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 

Se alguém examinasse o código, ele viu que essa palavra faz outra coisa :)

E aqui está 110 ???

Sim, essa palavra também coloca o número 110 na pilha, e é por isso. Quando compiladas, as várias construções devem ser um único todo. Por exemplo, depois de se deve ser então. E a palavra criada usando ":" deve terminar com ";". Para verificar essas condições, palavras especiais do compilador colocam certos valores na pilha e verificam sua presença. Por exemplo, a palavra ":" coloca o valor 110 e a palavra ";" verifica se 110 está no topo da pilha.Se esse não for o caso, isso é um erro. Portanto, as estruturas de controle não foram emparelhadas.

Essa verificação é realizada em todas as palavras do compilador; portanto, criaremos uma palavra especial para isso - "? Pares". Ele pega dois valores da pilha e gera um erro se eles não forem iguais.

Além disso, em tais palavras, você geralmente precisa verificar se o modo de compilação está definido. Vamos criar a palavra "? State" para isso.

estado dos pares
 #: ?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 

Isso é tudo! Não vamos compilar mais nada no assembler manualmente :)

Mas até o final, o compilador ainda não foi escrito, portanto, no começo, você terá que usar alguns métodos incomuns ...

Vamos nos preparar para compilar o compilador criado com o compilador criado


Para começar, você pode verificar como a palavra ":" funciona compilando algo simples. Vamos criar, por exemplo, a palavra:

 : ^2 dup * ; 

Esta palavra é quadrada. Mas não temos a palavra ";" o que fazer?Em vez disso, escrevemos a palavra exit e ela compila. E, em seguida, desative o modo de compilação com a palavra "[" e solte o valor 110:

 $ ./forth ( 0 ): > : ^2 dup * exit [ drop ( 0 ): > 4 ^2 ( 1 ): 16 > 

Isso funciona! Vamos

continuar ...

Como continuaremos a escrever o forte, precisamos pensar sobre onde estará o código fonte do forte e quando compilar. Vamos fazer a opção mais fácil. O código fonte do forte será colocado no código fonte no assembler, como uma sequência de texto. E para que ele não ocupe muito espaço, o colocaremos imediatamente após o endereço aqui, na área de memória livre. Obviamente, precisamos dessa área para compilação, mas a velocidade da "fuga" da interpretação será maior que a necessidade de nova memória. Assim, o código compilado começará a sobrescrever a fonte no forte, começando do início, mas não precisaremos mais dele, pois já lemos e usamos esta seção.

 fcode: .ascii " 2 2 + . quit" 

Mas, no início da linha, vale a pena colocar uma dúzia de espaços.

Para fazer isso funcionar, alteramos o código de início para que tib, #tib aponte para esta linha. No final, é necessário encerrar a entrada na linha de comando normal do sistema.

Iniciando bytecode tornou-se assim
 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 

Lançamento!

 $ ./forth 4 ( 0 ): > 

Ótimo!

E agora ...

Compile o compilador com o compilador


Em seguida, escrevemos o código na linha fcode. A primeira coisa a fazer, é claro, é a palavra ";".

 : ; ?state 110 ?pairs lit8 [ blword exit find cfa c@ c, ] c, 0 state ! exit [ current @ @ dup c@ 96 or swap c! drop 

Vou fazer algumas explicações.

 ?state 110 ?pairs 

Aqui, verificamos se o estado de compilação está realmente definido e se 110 está na pilha, caso contrário, haverá uma interrupção por engano.

 lit8 [ blword exit find cfa c@ c, ] 

Nós compilamos o comando lit com o bytecode do comando exit. Eu tive que entrar no modo de execução, encontrar a palavra exit, obter o endereço de execução e obter o código de comando a partir daí. Tudo isso foi necessário porque ainda não temos a palavra compilar. Se fosse, em vez de tudo isso, seria suficiente simplesmente escrever “compile exit” :)

 c, 0 state ! 

Isso compilará o comando exit quando a palavra ";" for executada e, em seguida, o modo de interpretação será definido. A palavra "[" não pode ser usada aqui, pois possui o sinal imediato e é executada agora , mas precisamos compilar esses comandos na palavra ";" para que eles desativem o modo de compilação.

 exit [ 

Nós já experimentamos isso. A palavra exit é compilada e o modo de compilação está desativado. Tudo, a palavra ";" compilado. E o que mais está escrito lá ainda mais?

 current @ @ dup c@ 96 or swap c! drop 

Você precisa definir o sinalizador imediato para a nova palavra. É exatamente isso que a sequência indicada faz, exceto a palavra drop. A palavra drop remove os 110 esquecidos que colocavam a palavra ":" no início da criação.

Agora é tudo!

Lançamos e tentamos.

 $ ./forth ( 0 ): > : ^3 dup dup * * ; ( 0 ): > 6 ^3 . 216 ( 0 ): > 

Existe!Esta é a primeira palavra que nosso compilador compilou "de verdade".

Mas ainda não temos condições, loops e muito mais ... Vamos começar com uma palavra pequena, mas muito necessária para criar um compilador: imediato. Ele define o atributo imediato na última palavra criada:

 : immediate current @ @ dup c@ 96 or swap c! ; 

Uma sequência familiar :) Recentemente, ele foi escrito manualmente, não será mais necessário.
Agora vamos fazer algumas palavras pequenas, mas úteis:

 : hex 16 base ! ; : decimal 10 base ! ; : bl 32 ; : tab 9 ; : lf 10 ; 

hex e decimal definem o sistema numérico correspondente. O restante são constantes para obter os códigos de caracteres correspondentes.

Também
criamos uma palavra para copiar uma linha com um contador :: cmove sobre c @ 1+ move;

E agora estaremos envolvidos em condições. Em geral, se houvesse uma palavra compilar, ficaria assim:

 : if ?state compile ?nbranch8 here 0 c, 111 ; immediate : then ?state 111 ?pairs dup here swap - swap c! ; immediate 

Todas essas palavras no início verificam se o modo de compilação está definido e geram um erro se esse não for o caso.

A palavra if compila uma ramificação condicional, reserva um byte para o parâmetro de comando de ramificação condicional e envia o endereço desse byte para a pilha. Em seguida, ele empurra o valor de controle 111 para a pilha.A

palavra verifica a presença do valor de controle 111 e grava o deslocamento no endereço na pilha.

E imediatamente faça a palavra mais. No início, ele compila o comando de salto incondicional para ignorar o ramo else. Da mesma maneira como se, o deslocamento da transição ainda não é conhecido, é simplesmente reservado e seu endereço é empurrado para a pilha. Bem, depois disso, exatamente o mesmo é feito como em seguida: o endereço da transição de captura é definido como o ramo else. Algo é mais difícil de descrever do que o próprio código :) Se alguém quiser descobrir completamente, é melhor analisar o trabalho de um código tão simplificado:

 : if compile ?nbranch8 here 0 c, ; immediate : then dup here swap - swap c! ; immediate 

Bem, agora nós programamos o código real. Como não temos a palavra compilar, aplicamos o mesmo truque ao criar a palavra ";":

 : 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 

Agora você pode tentar compilar a condição. Vamos criar, por exemplo, uma palavra que imprima 1000 se houver 5 na pilha e 0 em outros casos:

 $ ./forth ( 0 ): > : test 5 = if 1000 . else 0 . then ; ( 0 ): > 22 test 0 ( 0 ): > 3 test 0 ( 0 ): > 5 test 1000 ( 0 ): > 

É claro que esse resultado não funcionou imediatamente, houve erros, houve depuração. Mas no final, as condições funcionaram!

Uma pequena digressão sobre o comprimento dos comandos de transição
, , 127 . . , , . , , . 8 , 40 127 . , ?

. — 16 .

. 16 — . , , call, . , 11 ( 1023 ). 300 1000 . , . 3 , 8 . : (?nbranch), (?branch) (branch). — 24 .

Temos condições, a vida fica mais fácil :)

Vamos fazer uma palavra. "(Aspas). Ele exibe o texto especificado quando executado. É usado desta maneira:

 ."    " 

Você pode usar esta palavra apenas no modo de compilação. Isso ficará aparente depois de analisarmos o dispositivo desta palavra:

 : ." ?state 34 word dup if lit8 [ blword (.") find cfa c@ c, ] c, str, else drop then ; immediate 

Esta palavra é executada no modo de compilação. É preciso uma sequência do fluxo de entrada até aspas (34 palavras). Se a linha não puder ser obtida, ela não fará nada. Embora, aqui seria melhor derivar um diagnóstico. Mas para a saída da linha, essa palavra é exatamente o que estamos fazendo :) Se necessário, você poderá redefinir essa palavra novamente, já com diagnóstico.

Se fosse possível obter a sequência, o comando byte (. ") É compilado e a sequência recebida. Esse comando byte (aspas entre colchetes), quando executado, exibe a sequência que foi compilada atrás do byte de comando

.

 $ ./forth ( 0 ): > : test ."    " ; ( 0 ): > test     ( 0 ): > 

E, finalmente, vamos compilar a palavra.

É claro que, no modo de compilação, essa palavra deve pegar o nome da próxima palavra do fluxo e encontrá-la no dicionário. E então haverá opções: pode ser um comando de byte ou uma palavra escrita em código de byte. Essas palavras devem ser compiladas de maneiras diferentes. Portanto, criaremos duas palavras auxiliares: "(compile_b)" e "(compile_c)".

(compile_b) irá compilar o comando de chamada para chamar o bytecode. O parâmetro será uma palavra de 64 bits - o endereço do bytecode que está sendo chamado.

(compile_c) irá compilar o comando byte. Assim, o parâmetro deste comando será um byte - o código do comando.

Bem, a própria palavra compilar compilará (compile_b) ou (compile_c) com os parâmetros correspondentes.

Vamos começar com (compile_c),como no mais simples:

 : (compile_c) r> dup c@ swap 1+ >rc, ; 

Apesar de sua simplicidade, primeiro escrevemos uma palavra em bytecode, que por si só tem parâmetros. Portanto, vou comentar. Depois de inserir (compile_c), o endereço de retorno está localizado na pilha de retorno, pois não é trivial. Este é o endereço do próximo byte após o comando de chamada. A situação no momento da ligação é mostrada abaixo. A0 - código de comando de chamada, XX - parâmetro de comando de chamada - endereço de chamada (deslocamento) do código de bytes da palavra (compile_c).



O endereço de retorno indica o byte NN. Geralmente, há o código para o próximo byte do comando. Mas nossa palavra tem parâmetros, então NN são apenas os parâmetros da palavra "(compile_c)", ou seja, o código de bytes do comando compilado. Você precisa ler este byte e alterar o endereço de retorno, movendo-o para o próximo comando de byte. Isso é feito pela sequência "r> dup c @ swap 1+> r". Essa sequência puxa o endereço de retorno da pilha de retorno para a pilha normal, recupera um byte, adiciona um (endereço de retorno) e retorna para a pilha de retorno. O comando restante "c" compila o código de comando byte obtido dos parâmetros.

(compile_b) não é muito mais complicado:

 : (compile_b) r> dup @ swap 8 + >r compile_b ; 

Tudo é o mesmo aqui, apenas o parâmetro de 64 bits é lido e a palavra compile_b é usada para compilar a palavra, que já criamos para o compilador.

E agora a palavra compilar. Como já discutido, ele lê o nome da palavra, encontra-o e compila um dos dois comandos anteriores. Não vou comentar, já aplicamos e desmontamos todas as construções usadas.

Compilação do 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 

Para verificar a palavra criada, criamos, com sua ajuda, a palavra se não.

 : ifnot ?state compile ?branch8 here 0 c, 111 ; immediate 

Confira!

 $ ./forth ( 0 ): > : test 5 = ifnot 1000 . else 0 . then ; ( 0 ): > 22 test 1000 ( 0 ): > 3 test 1000 ( 0 ): > 5 test 0 ( 0 ): > 

Está tudo bem! E é hora de fazer ciclos ...

Neste artigo, faremos ciclos com uma condição. O forte tem duas opções para um ciclo com uma condição.

A primeira opção é começar ... até. A palavra até remove o valor da pilha e, se não for igual a zero, o ciclo termina.

A segunda opção é começar ... enquanto ... repetir. Nesse caso, a verificação ocorre quando a palavra while é executada. O loop sai se o valor na pilha for zero.

Os ciclos no forte são feitos da mesma maneira que as condições - em transições condicionais e incondicionais. Trago o código, acho que comentários não são necessários.

 : 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 

Hoje terminamos o compilador. Ainda resta muito pouco. Das principais funções que ainda não foram implementadas são apenas ciclos com um contador. E também vale a pena deixar o comando exit loop sair. Faremos isso na próxima vez.

Mas não experimentamos o comando cycle!

Fazemos isso escrevendo as palavras padrão. Finalmente devemos ver o nosso dicionário.
Para fazer isso, no início, criamos a palavra link @. Ele extrairá o campo de comunicação da entrada do dicionário (deslocada para a entrada anterior). Como lembramos, o campo de comunicação pode ter um tamanho diferente: 1, 2, 4 ou 8 bytes. Essa palavra pegará na pilha o endereço da entrada do dicionário e retornará dois valores: o endereço do campo de nome e o valor do campo de comunicação.

 : 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 ; 

E agora você pode fazer a palavra palavras:

 : words context @ @ 0 begin + dup link@ swap count type tab emit dup 0= until drop drop ; 

A iniciar ...

 $ ./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 ): > 

Aqui está, nossa riqueza :)

Eu queria dizer tudo ... não, no entanto, vamos permitir especificar um arquivo com um programa forte para compilação e execução como parâmetro.

Criamos comandos syscall para abrir, fechar e ler o arquivo. Definimos as constantes necessárias para elas.

 : 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 ; 

Agora você pode fazer a palavra inicial _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 ; 

Esta palavra será carregada do arquivo e executará qualquer programa fort. Mais precisamente, o intérprete executará tudo o que estiver neste arquivo. E pode haver, por exemplo, uma compilação de novas palavras e sua execução. O nome do arquivo é indicado pelo primeiro parâmetro na inicialização. Não entrarei em detalhes, mas os parâmetros de inicialização no Linux são passados ​​pela pilha. A palavra _start os alcançará com os comandos 0 pick (número de parâmetros) e 2 pick (ponteiro para o primeiro parâmetro). Para um sistema forte, esses valores estão fora da pilha, mas você pode obtê-los com o comando pick. O tamanho do arquivo é limitado a 32 KB, enquanto não há gerenciamento de memória.

Agora resta escrever na linha fcode no final:

 _start quit 

Crie um arquivo test.f e escreva algo no forte. Por exemplo, o algoritmo euclidiano para encontrar o maior fator comum:

 : NOD begin over over <> while over over > if swap over - swap else over - then repeat drop ; 23101 44425 NOD . bye 

Começamos.

 $ ./forth test.f 1777 Bye! $ 

A resposta está correta. A palavra foi compilada e depois cumprida. O resultado é exibido e o comando bye foi executado. Se você remover as duas últimas linhas, a palavra NOD será adicionada ao dicionário e o sistema passará à sua linha de comando. Você já pode escrever programas :-)

Só isso.Quem se importa, você pode baixar o código-fonte ou o binário pronto para Linux no x86-64 no Github: https://github.com/hal9000cc/forth64 As

fontes vêm com uma licença GNU GPL v2 DCH v1 - Faça o que você quiser :-)

Source: https://habr.com/ru/post/pt437466/


All Articles