Byte-machine para el fuerte (y no solo) en nativos americanos (parte 4)

Fort Byte Coche (y más) Nativos americanos

¡Y nuevamente sobreestimé el volumen del artículo! Planeé que este sería el artículo final, donde haremos un compilador y realizaremos pruebas. Pero el volumen resultó ser grande, y decidí dividir el artículo en dos.

En este artículo, haremos casi todas las funciones básicas del compilador. Tendrá vida y será posible escribir, compilar y ejecutar código bastante serio. Y haremos pruebas en la siguiente parte. (Por cierto, las partes anteriores: uno , dos , tres ).

Escribo por primera vez en Habré; quizás no siempre está bien. En mi opinión, los artículos 2, 3 resultaron ser bastante secos, mucho código, poca descripción. Esta vez intentaré hacer algo diferente, centrarme en la descripción de las ideas mismas. Bueno, el código ... el código, ¡por supuesto que lo hará! Quien quiera entender a fondo, tal oportunidad será. En muchos casos, pondré el código debajo del spoiler. Y, por supuesto, siempre puedes ver la fuente completa en el github.

El compilador continuará escribiendo durante algún tiempo en ensamblador, pero luego irá al fuerte y continuará escribiendo el compilador en nosotros mismos. Esto se parecerá al barón Munchausen, que se tiró del pelo del pantano. Pero, para empezar, describiré cómo funciona el compilador en el fuerte. ¡Bienvenido a cat!

¿Cómo funciona el compilador?


La memoria en el fuerte consiste en un fragmento continuo en el que las entradas del diccionario se ordenan secuencialmente. Después de su finalización es seguido por un área de memoria libre. El primer byte libre está indicado por la variable h. También existe la palabra de uso frecuente aquí, que empuja la dirección del primer byte libre en la pila, se determina de manera muy simple:

: here h @ ; 



Vale la pena mencionar la palabra asignación, que reserva el número especificado de bytes moviendo el puntero h. La palabra asignación se puede definir de la siguiente manera:

 : allot h +! ; 

De hecho, el compilador utiliza un modo de intérprete especial más algunas palabras especiales. Entonces, con una oración, puedes describir todo el principio del compilador en el fuerte. La variable de estado determina en qué modo trabaja el intérprete. Si es cero, se establece el modo de ejecución; de lo contrario, modo de compilación. Ya estamos familiarizados con el modo de ejecución, en él las palabras del búfer de entrada simplemente se ejecutan una tras otra. Pero en modo de compilación no se ejecutan, sino que se compilan en la memoria mediante el puntero h. En consecuencia, el puntero se mueve hacia adelante.

En el fuerte clásico, la palabra "," se usa para compilar un valor entero, la palabra "c" se usa para compilar un byte. Nuestro sistema utiliza valores de diferentes profundidades de bits (8, 16, 32, 64), por lo tanto, también haremos las palabras "w" e "i". También hacemos la palabra "str", que compilará la cadena, tomando dos valores de la pila: la dirección y la longitud de la cadena.

Se usan palabras especiales del compilador para formar estructuras de control. Estas son las palabras si, entonces, do, loop y otras. Estas palabras se ejecutan incluso en modo de compilación. Por ejemplo, la palabra if compila un comando de byte de rama condicional (? Nbranch) en la ejecución. Para que el sistema sepa qué palabras deben ejecutarse en modo de compilación, y no compilarse, se utiliza el indicador (signo) inmediato. Ya lo tenemos en el campo de marca de la entrada del diccionario. En el código fuente del ensamblador, se llama f_immediate. Para establecer esta bandera, use la palabra inmediato. No tiene parámetros, el indicador inmediato se establece en la última palabra del diccionario.

¡Ahora pasemos de la teoría a la práctica!

Preparación


Al principio, necesitamos hacer algunos comandos de bytes simples en lenguaje ensamblador que necesitamos. Aquí están: mover (copiar el área de memoria), llenar (llenar el área de memoria), operaciones de bits (y, o, xor, invertir), comandos de desplazamiento de bits (rshift, lshift). Hagamos el mismo rpick (esto es lo mismo que pick, solo funciona con la pila de retorno, no con la pila de datos).

Estos comandos son muy simples, aquí está su código
 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 

Todavía necesito hacer que la palabra sea palabra. Esto es lo mismo que blword, pero se indica un delimitador específico en la pila. No proporciono el código, se puede encontrar en la fuente. Copié / pegué las palabras blworld y reemplacé los comandos de comparación.

En conclusión, hacemos la palabra syscall. Con él, será posible realizar las operaciones faltantes del sistema, por ejemplo, trabajar con archivos. Tal solución no funcionará si se requiere independencia de la plataforma. Pero este sistema ahora se usa para pruebas, así que déjalo así por ahora. Si es necesario, todas las operaciones se pueden convertir en comandos de bytes, no es nada difícil. El comando syscall aceptará 6 parámetros para la llamada al sistema y el número de llamada de la pila. Devolverá un parámetro. Las asignaciones de parámetros y el valor de retorno están determinados por el número de llamada del 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 

Y ahora procedamos directamente al compilador.

Compilador


Crea la variable h, todo es simple.

  item h h: .byte b_var0 .quad 0 
Escribiremos su inicialización en la línea de inicio:
 # 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 

Hagamos la palabra aquí:

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

Y también palabras para compilar los valores: "asignación" y "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 

Ahora hagamos que el estado sea variable y dos palabras para controlar su valor: "[" y "]". Por lo general, estas palabras se utilizan para realizar algo en el momento de la compilación. Por lo tanto, la palabra "[" desactiva el modo de compilación y la palabra "]" lo activa. Pero nada impide que se usen en otros casos cuando es necesario activar o desactivar el modo de compilación. La palabra "[" será nuestra primera palabra con el signo inmediato. De lo contrario, no podrá desactivar el modo de compilación, ya que se compilará, no se ejecutará.

  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 

Llegó el turno de la palabra $ compilar. Tomará la dirección de la entrada del diccionario de la pila y compilará la palabra especificada. Para compilar una palabra en implementaciones ordinarias de Fort, es suficiente aplicar la palabra "," a la dirección de ejecución. Aquí todo es mucho más complicado. En primer lugar, hay dos tipos de palabras: código de bytes y código de máquina. Los primeros se compilan por byte y los segundos por el comando call byte. Y en segundo lugar, tenemos hasta cuatro variantes del comando de llamada: call8, call16, call32 y call64. Cuatro? No! Cuando escribí el compilador, ¡agregué 16 más a estos cuatro! :)

¿Cómo sucedió esto? Tenemos que hacer una pequeña digresión.

Mejorando el comando de llamada


Cuando el compilador comenzó a funcionar, descubrí que en muchos casos (pero no en todos) el comando call8 es suficiente. Esto es cuando la palabra llamada está dentro de 128 bytes. Pensé, ¿y cómo asegurarme de que esto suceda en casi todos los casos? ¿Cómo poner más de 256 valores en un byte?
El primer punto que noté fue que en el fuerte la llamada siempre se dirige a direcciones más bajas. Esto significa que puede rehacer el comando de llamada de tal manera que solo pueda llamar a direcciones inferiores, pero para 256 bytes, no 128. Es mejor.

Pero si pones algunos bits en alguna parte ... ¡Resulta que allí es donde! Tenemos dos bytes: un byte es el comando, el segundo es el desplazamiento. Pero nada impide que los bits más bajos del comando coloquen los bits más altos del parámetro (desplazamiento). Para una máquina de bytes, parece que en lugar de un comando de llamada, hay varios. Sí, de esta manera ocupamos varias celdas de la tabla de códigos de byte-command con un comando, pero a veces vale la pena hacerlo. El comando de llamada es uno de los comandos más utilizados, así que decidí poner 4 bits de desplazamiento en el comando. ¡Por lo tanto, puede hacer una llamada a una distancia de hasta 4095 bytes! Esto significa que un comando de llamada tan corto se usará casi siempre. Coloqué estos comandos con el código 0xA0 y las siguientes líneas aparecieron en la tabla 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 

El primero de estos comandos de bytes simplemente realiza una llamada en la dirección de las direcciones más bajas en el desplazamiento especificado en el parámetro (hasta 255). El resto agrega el desplazamiento correspondiente al parámetro. bcmd_call8b1 agrega 256, bcmd_call8b2 agrega 512, y así sucesivamente. Hice el primer comando de llamada por separado, el resto con una macro.

Primer 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 y creación del resto de los comandos de llamada:

 .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 

Bueno, rehice el antiguo comando call8 para llamar hacia adelante, ya que ya tenemos 16 equipos haciendo una devolución de llamada. Cualquiera sea la confusión, le cambié el nombre por 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 

Por cierto, por conveniencia, hice una macro, que en ensamblador compila automáticamente la llamada correspondiente dentro de 4095. Y luego nunca necesité :)

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

Y ahora ...

Equipo de compilación


Entonces, obtenemos un algoritmo de compilación de comandos bastante complicado. Si se trata de un comando de byte, compile solo un byte (código de comando de byte). Y si esta palabra ya está escrita en bytecode, debe compilar su llamada con el comando de llamada, eligiendo uno de los veinte. Más precisamente 19, por lo que no tenemos desvío de llamadas, y call8f no se utilizará para el fuerte.

Entonces la elección es esta. Si el desplazamiento se encuentra dentro de 0 ...- 4095, seleccione el comando bcmd_call8b con el código 0xA0, colocando los cuatro bits de desplazamiento más significativos en los bits menos significativos del comando. Al mismo tiempo, para la máquina de bytes, el código para uno de los comandos bcmd_call8b0 es bcmd_call8b15.

Si el desplazamiento hacia atrás es mayor o igual a 4095, entonces determinamos en qué dimensión se coloca el desplazamiento y usamos el comando apropiado de call16 / 32/64. Debe tenerse en cuenta que la compensación para estos equipos está firmada. Pueden causar tanto hacia adelante como hacia atrás. Por ejemplo, call16 puede llamar a una distancia de 32767 en ambas direcciones.

Aquí está la implementación como resultado:

$ compilar

Compila una palabra. Como parámetro, toma la dirección de la entrada del diccionario de la palabra compilada. De hecho, comprueba el indicador f_code, calcula la dirección del código (cfa) y llama a compile_b o compile_c (si el indicador está configurado).

compilar_c

Compila un comando de byte. La palabra más simple aquí se describe en el fuerte así:

 : compile_c c@ c, ; 

compilar_b
Toma una dirección de bytecode en la pila y compila su llamada.

test_bv

Toma un desplazamiento de la pila (con un signo) y determina qué profundidad de bits usar (1, 2, 4 u 8 bytes). Devuelve el valor 0, 1, 2 o 3. Con esta palabra, puede determinar cuál usar a partir de los comandos call16 / 32/64. Esta palabra será útil al compilar números (una opción de lit8 / 16/32/64).

Por cierto, puede iniciar el sistema y "jugar" en la consola fort con cualquiera de estas palabras. Por ejemplo:

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

Toma un desplazamiento (con un signo) de la pila y determina qué comando de llamada usar. De hecho, verifica si el desplazamiento se encuentra dentro del rango de 0 ... -4095, y devuelve 0. En este caso, si no hay acierto en este intervalo, llama a test_bv.

Eso es todo lo que se necesita para compilar el 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 


Ahora necesitamos compilar el número.

Compilar un número (literal)


Escribió un subtítulo completo, preparado para describir específicamente la compilación del literal, pero resulta que no hay nada especial para describir :)

Ya hemos hecho la mitad del trabajo en la palabra test_bv. Solo queda llamar a test_bv y, según el resultado, compilar lit8 / 16/32/64, y luego el valor correspondiente de 1, 2, 4 u 8 bytes de tamaño.

Hacemos esto definiendo la palabra 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 

Modificar el intérprete


Todo está listo para compilar el comando y los literales. Ahora debe integrarse en el intérprete. Esta modificación es simple. Donde se ejecutó el comando, agregue la comprobación de estado. Si el estado no es nulo y la palabra no contiene el indicador inmediato, en lugar de la ejecución, debe llamar a $ compile. Y casi lo mismo que hacer cuando el número se obtiene de la secuencia de entrada. Si el estado es cero, simplemente deje el número en la pila y, si no, llame a compile_n.

Aquí esta el 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 

Ahora estamos a un paso del compilador ...

Definición de nuevas palabras (palabra ":")


Ahora, si establecemos la variable de estado en un valor distinto de cero, comenzará el proceso de compilación. Pero el resultado será inútil, no podemos cumplirlo ni encontrarlo en la memoria. Para hacer posible todo esto, es necesario formatear el resultado de la compilación en forma de artículo de diccionario. Para hacer esto, antes de activar el modo de compilación, debe crear un título para la palabra.

El encabezado debe contener banderas, un campo de comunicación y un nombre. Aquí tenemos una historia familiar: el campo de comunicación puede ser de 1, 2, 4 u 8 bytes. Hagamos la palabra compile_1248, que nos ayudará a formar dicho campo de comunicación. Tomará dos números en la pila: el desplazamiento y el valor generado por el comando test_bv.

compilar_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 

Ahora haga la palabra $ crear. Nos será útil más de una vez. Puede usarlo siempre que necesite crear un título para una entrada de diccionario. Tomará dos valores de la pila: la dirección del nombre de la palabra creada y su longitud. Después de ejecutar esta palabra, la dirección de la entrada del diccionario creada aparecerá en la pila.

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

La siguiente palabra recogerá el nombre de la nueva palabra de la secuencia de entrada usando la palabra blword y llamará a $ create, creando una nueva palabra con el nombre 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 

Y finalmente, haga la palabra ":". Creará una nueva palabra usando create_in y establecerá el modo de compilación, no está instalado. Y si está instalado, da un error. La palabra ":" tendrá el signo inmediato.

la palabra
 # : : 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 alguien miró el código, vio que esta palabra hace otra cosa :)

¿Y aquí hay 110 ???

Sí, esta palabra también empuja el número 110 a la pila, y es por eso. Cuando se compila, las diversas construcciones deben ser un todo único. Por ejemplo, después de si debe ser entonces. Y la palabra creada usando ":" debe terminar con ";". Para verificar estas condiciones, palabras especiales del compilador ponen ciertos valores en la pila y verifican su presencia. Por ejemplo, la palabra ":" pone el valor 110 y la palabra ";" comprueba que 110 está en la parte superior de la pila. Si este no es el caso, entonces es un error. Entonces, las estructuras de control no estaban emparejadas.

Tal verificación se lleva a cabo en todas las palabras del compilador, por lo tanto, haremos una palabra especial para esto: "? Pares". Tomará dos valores de la pila y arrojará un error si no son iguales.

Además, en tales palabras, a menudo tiene que verificar que el modo de compilación esté configurado. Hagamos la palabra "? Estado" para esto.

estado de "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 

Eso es todo! No compilaremos nada más en ensamblador manualmente :)

Pero hasta el final, el compilador aún no se ha escrito, por lo que al principio tendrá que usar algunos métodos inusuales ...

Preparémonos para compilar el compilador creado con el compilador creado.


Para comenzar, puede verificar cómo funciona la palabra ":" compilando algo simple. Hagamos, por ejemplo, la palabra:

 : ^2 dup * ; 

Esta palabra es cuadrada. Pero no tenemos la palabra ";" ¿qué hacer?En su lugar, escribimos la palabra salir, y se compila. Y luego apague el modo de compilación con la palabra "[" y suelte el valor 110:

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

Funciona! Continuemos

...

Dado que continuaremos escribiendo el fuerte en el fuerte, tenemos que pensar dónde estará el código fuente del fuerte y cuándo compilarlo. Hagamos la opción más fácil. El código fuente de la fortaleza se colocará en el código fuente en ensamblador, como una cadena de texto. Y para que no ocupe demasiado espacio, lo colocaremos inmediatamente después de la dirección aquí, en el área de memoria libre. Por supuesto, necesitamos esta área para la compilación, pero la velocidad de "fuga" de la interpretación será mayor que la necesidad de una nueva memoria. Por lo tanto, el código compilado comenzará a sobrescribir la fuente en el fuerte, comenzando desde el principio, pero ya no lo necesitaremos, ya que ya hemos leído y usado esta sección.

 fcode: .ascii " 2 2 + . quit" 

Pero, al comienzo de la línea, vale la pena colocar una docena de espacios.

Para que esto funcione, cambiamos el bytecode de inicio para que tib, #tib apunte a esta línea. Al final hay que salir para ingresar a la línea de comando normal del sistema.

Comenzar bytecode se ha convertido así
 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 

Lanzamiento

 $ ./forth 4 ( 0 ): > 

Genial

Y ahora ...

Compila el compilador con el compilador


Luego, escribimos el código en la línea fcode. Lo primero que debe hacer, por supuesto, es la palabra ";".

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

Haré algunas explicaciones.

 ?state 110 ?pairs 

Aquí comprobamos que el estado de compilación está realmente configurado y que 110 está en la pila, de lo contrario, habrá una interrupción por error.

 lit8 [ blword exit find cfa c@ c, ] 

Esto compilamos el comando iluminado con el bytecode del comando de salida. Tuve que pasar al modo de ejecución, encontrar la palabra salir, obtener la dirección de ejecución y obtener el código de comando desde allí. Todo esto fue necesario porque todavía no tenemos la palabra compilar. Si lo fuera, en lugar de todo esto, sería suficiente simplemente escribir "compilar salir" :)

 c, 0 state ! 

Esto compilará el comando de salida cuando se ejecute la palabra ";", y luego se establecerá el modo de interpretación. La palabra "[" no se puede usar aquí, ya que tiene el signo inmediato y se ejecuta ahora , pero necesitamos compilar dichos comandos en la palabra ";" para que apaguen el modo de compilación.

 exit [ 

Ya hemos experimentado esto. La palabra salir se compila y el modo de compilación se desactiva. Todo, la palabra ";" compilado ¿Y qué más se escribe allí más?

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

Debe establecer la bandera inmediata para la nueva palabra. Esto es exactamente lo que hace la secuencia indicada, excepto la caída de palabras. La caída de palabras elimina el 110 olvidado que colocó la palabra ":" al comienzo de la creación.

¡Ahora es todo!

Lanzamos e intentamos.

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

Hay!Esta es la primera palabra que nuestro compilador compiló "de verdad".

Pero todavía no tenemos condiciones, ni bucles, y mucho más ... Comencemos con una palabra pequeña pero muy necesaria para crear un compilador: inmediato. Establece el atributo inmediato en la última palabra creada:

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

Una secuencia familiar :) Recientemente, se escribió manualmente, ya no será necesario.
Ahora hagamos algunas palabras pequeñas pero útiles:

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

hexadecimal y decimal establecen el sistema numérico correspondiente. El resto son constantes para obtener los códigos de caracteres correspondientes.

También hacemos una palabra para copiar una línea con un contador
:: cmove sobre c @ 1+ move;

Y ahora estaremos comprometidos en las condiciones. En general, si hubiera una palabra compilar, se vería así:

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

Todas estas palabras al principio verifican que el modo de compilación esté configurado y generan un error si este no es el caso.

La palabra if compila una rama condicional, reserva un byte para el parámetro del comando de rama condicional y empuja la dirección de ese byte a la pila. Luego empuja el valor de control 111 sobre la pila.

La palabra luego verifica la presencia del valor de control 111, y luego escribe el desplazamiento en la dirección en la pila.

E inmediatamente haga la palabra más. Al principio, compila el comando de salto incondicional para omitir la rama else. De la misma manera que si, el desplazamiento de transición aún no se conoce, simplemente se reserva y su dirección se inserta en la pila. Bueno, después de eso, se hace exactamente lo mismo que en ese momento: la dirección de la transición de captura se establece en la rama else. Algo es más difícil de describir que el código en sí :) Si alguien quiere resolverlo a fondo, es mejor analizar el trabajo de un código tan simplificado al máximo:

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

Bueno, ahora programamos el código real. Como no tenemos la palabra compilar, aplicamos el mismo truco que cuando creamos la palabra ";":

 : 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 

Ahora puede intentar compilar la condición. Hagamos, por ejemplo, una palabra que imprima 1000 si hay 5 en la pila y 0 en otros casos:

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

Está claro que tal resultado no funcionó de inmediato, hubo errores, hubo depuración. Pero al final, ¡las condiciones funcionaron!

Una pequeña digresión sobre la longitud de los comandos de transición
, , 127 . . , , . , , . 8 , 40 127 . , ?

. — 16 .

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

Tenemos condiciones, la vida se vuelve más fácil :)

Hagamos una palabra. "(Comillas). Muestra el texto especificado cuando se ejecuta. Se utiliza de esta manera:

 ."    " 

Puede usar esta palabra solo en modo de compilación. Esto se hará evidente después de analizar el dispositivo de esta palabra:

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

Esta palabra se ejecuta en modo de compilación. Toma una cadena desde la secuencia de entrada hasta comillas (34 palabras). Si no se pudo obtener la fila, no hace nada. Aunque, aquí sería mejor derivar un diagnóstico. Pero para la salida de la línea, esta palabra es exactamente lo que estamos haciendo :) Si es necesario, puede redefinir esta palabra nuevamente, ya con diagnósticos.

Si fue posible obtener la cadena, se compila el comando de byte (. ") Y luego se recibe la cadena. Este comando de byte (comillas de puntos entre paréntesis), cuando se ejecuta, muestra la cadena que se compiló detrás del byte de comando.

Verificar.

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

Y finalmente, hagamos que la palabra se compile.

Está claro que en el modo de compilación esta palabra debe tomar el nombre de la siguiente palabra de la transmisión, encuéntrela en el diccionario. Y luego habrá opciones: puede ser un comando de byte, o puede ser una palabra escrita en código de byte. Estas palabras deben compilarse de diferentes maneras. Por lo tanto, crearemos dos palabras auxiliares: "(compile_b)" y "(compile_c)".

(compile_b) compilará el comando de llamada para invocar el bytecode. El parámetro será una palabra de 64 bits: la dirección del bytecode que se llama.

(compile_c) compilará el comando byte. En consecuencia, el parámetro de este comando será un byte: el código del comando.

Bueno, la palabra compilar en sí compilará (compile_b) o (compile_c) con los parámetros correspondientes.

Comencemos con (compile_c),como con el más simple:

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

A pesar de su simplicidad, primero escribimos una palabra en bytecode, que en sí misma tiene parámetros. Por lo tanto, comentaré. Después de ingresar (compile_c), la dirección de retorno se encuentra en la pila de retorno, ya que no es trillada. Esta es la dirección del siguiente byte después del comando de llamada. La situación en el momento de la llamada se muestra a continuación. A0 - código de comando de llamada, XX - parámetro de comando de llamada - dirección de llamada (desplazamiento) del código de byte de la palabra (compile_c).



La dirección de retorno indica el byte NN. Por lo general, existe el código para el siguiente byte del comando. Pero nuestra palabra tiene parámetros, por lo que NN son solo los parámetros de la palabra "(compile_c)", es decir, el código de bytes del comando compilado. Debe leer este byte y cambiar la dirección de retorno moviéndolo al siguiente comando de byte. Esto se realiza mediante la secuencia "r> dup c @ swap 1+> r". Esta secuencia extrae la dirección de retorno de la pila de retorno a la pila normal, recupera un byte, le agrega uno (dirección de retorno) y la devuelve a la pila de retorno. El comando restante "c" compila el código de comando de byte obtenido de los parámetros.

(compile_b) no es mucho más complicado:

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

Aquí todo es igual, solo se lee el parámetro de 64 bits y la palabra compile_b se usa para compilar la palabra, que ya hemos creado para el compilador.

Y ahora la palabra compilar. Como ya se discutió, lee el nombre de la palabra, la encuentra y compila uno de los dos comandos anteriores. No voy a comentarlo, ya hemos aplicado y desmontado todas las construcciones utilizadas.

Word compila
 : 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 la palabra creada, hacemos, con su ayuda, la palabra ifnot.

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

¡Compruébalo!

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

Todo esta bien! Y es hora de hacer ciclos ...

En este artículo haremos ciclos con una condición. El fuerte tiene dos opciones para un ciclo con una condición.

La primera opción es comenzar ... hasta. La palabra hasta elimina el valor de la pila, y si no es igual a cero, el ciclo termina.

La segunda opción es comenzar ... mientras ... repetir. En este caso, la comprobación se produce cuando se ejecuta la palabra mientras se ejecuta. El ciclo sale si el valor en la pila es cero.

Los ciclos en el fuerte se hacen de la misma manera que las condiciones: en transiciones condicionales e incondicionales. Traigo el código, los comentarios, creo, no son necesarios.

 : 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 

Hoy hemos terminado con el compilador. Queda muy poco. De las funciones clave que aún no se han implementado son solo ciclos con un contador. Y también vale la pena hacer que salga el comando del bucle de salida. Lo haremos la próxima vez.

¡Pero no experimentamos el comando del ciclo!

Hacemos esto escribiendo las palabras de palabras estándar. Finalmente debemos ver nuestro diccionario.
Para hacer esto, al principio, hacemos la palabra enlace @. Extraerá el campo de comunicación de la entrada del diccionario (desplazado a la entrada anterior). Como recordamos, el campo de comunicación puede tener un tamaño diferente: 1, 2, 4 u 8 bytes. Esta palabra tomará en la pila la dirección de la entrada del diccionario y devolverá dos valores: la dirección del campo de nombre y el valor del campo de comunicación.

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

Y ahora puedes hacer la palabra palabras:

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

Lanzando ...

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

Aquí está, nuestra riqueza :)

Quería decir todo ... no, sin embargo, hagamos posible especificar un archivo con un programa fort para compilación y ejecución como parámetro.

Hacemos comandos syscall para abrir, cerrar y leer el archivo. Definimos las constantes necesarias para ellos.

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

Ahora puede hacer que la palabra de inicio _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 palabra se cargará desde el archivo y ejecutará cualquier programa fort. Más precisamente, el intérprete ejecutará todo lo que estará en este archivo. Y puede haber, por ejemplo, una compilación de nuevas palabras y su ejecución. El nombre del archivo se indica mediante el primer parámetro al inicio. No entraré en detalles, pero los parámetros de lanzamiento en Linux se pasan a través de la pila. La palabra _start los alcanzará con los comandos 0 pick (número de parámetros) y 2 pick (puntero al primer parámetro). Para un sistema fort, estos valores se encuentran fuera de la pila, pero puede obtenerlos con el comando pick. El tamaño del archivo está limitado a 32 KB, mientras que no hay administración de memoria.

Ahora queda escribir en la línea fcode al final:

 _start quit 

Crea un archivo test.f y escribe algo allí en el fuerte. Por ejemplo, el algoritmo euclidiano para encontrar el mayor factor común:

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

Empezamos

 $ ./forth test.f 1777 Bye! $ 

La respuesta es correcta La palabra fue compilada, luego cumplida. Se muestra el resultado, luego se ejecutó el comando bye. Si elimina las dos últimas líneas, la palabra NOD se agregará al diccionario y el sistema irá a su línea de comando. Ya puedes escribir programas :-)

Eso es todo.A quién le importa, puede descargar la fuente o el binario listo para Linux en x86-64 desde Github: https://github.com/hal9000cc/forth64

Las fuentes vienen con una licencia GNU GPL v2 DCH v1 - Haga lo que quiera :-)

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


All Articles