Jupiter Ace Forth: Utilidades

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 25 Ene 2022 22:34

Último mensaje de la página anterior:

dancresp escribió:Y viendo lo visto, y asumiendo que en Forth estás más formado que yo, en breve te iré haciendo consultas varias, para mejorar mi nivel a base de programar juegos.


Dudo mucho que esté más puesto que tú en Forth. Lo que estoy algo puesto es en el funcionamiento interno del Jupiter Ace Forth. A modo de comparación, si el Forth fuera el ajedrez, estoy puesto en sus reglas y movimientos de las fichas, pero de lo importante, saber jugar al ajedrez, poco.

dancresp
Mensajes: 5740
Registrado: 13 Nov 2010 02:08
Agradecido : 390 veces
Agradecimiento recibido: 508 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor dancresp » 25 Ene 2022 22:41

Elurdio escribió:Dudo mucho que esté más puesto que tú en Forth. Lo que estoy algo puesto es en el funcionamiento interno del Jupiter Ace Forth. A modo de comparación, si el Forth fuera el ajedrez, estoy puesto en sus reglas y movimientos de las fichas, pero de lo importante, saber jugar al ajedrez, poco.

Pues el tema interno es lo que más me falla a mi.

Por ejemplo, quiero codificar niveles, como haría con una matriz o DATA del BASIC, y con el Jupiter Ace no sé como hacerlo, si no es volcando los datos a partir de una dirección de memoria. Pero desconozco si hay una forma más "bonita" de hacerlo.

Eso sí, intento sacar el máximo provecho de mis conocimientos. Aunque sean escasos.
Buscando la IP de la W.O.P.R. he encontrado mi índice

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 25 Ene 2022 23:26

Por ejemplo, quiero codificar niveles, como haría con una matriz o DATA del BASIC, y con el Jupiter Ace no sé como hacerlo, si no es volcando los datos a partir de una dirección de memoria. Pero desconozco si hay una forma más "bonita" de hacerlo.


Las matrices se explican bastante a fondo en el manual del JA. Pero en el ejemplo que pone (2-D) hay mucho de medidas de chequeo de errores sobre la validez de los índices y queda todo un poco "liado". Esos chequeos son muy útiles si se va a usar de modo interactivo, pero si se va a usar internamente en un programa, sobra.

Un volcado de datos en memoria, lo más sencillo es hacerlo dentro de una palabra. Para ello lo más fácil es usar CREATE.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 26 Ene 2022 11:35

Matrices en Ace-Forth

Las matrices internamente se guardan como una serie consecutiva de bytes. Veámoslo con un ejemplo:

Sea una matriz de 2x3 (2 filas x 3 columnas) cuyos elementos son números enteros (2 bytes cada uno). Una manera de disponerlos en memoria, entre otras posibles, es colocar los 3 números de la primera fila seguidos (col1, col2, col3) luego colocar los 3 números de la segunda fila (col1, col2, col3) y así sucesivamente si hubieran más filas.

Captura 3.jpg
Captura 3.jpg (61.15 KiB) Visto 990 veces


En el dibujo, "Inicio" es la dirección de inicio del bloque de memoria. El primer byte es el apuntado directamente por "inicio", lo reservamos para guardar el número de bytes que tiene una fila (dato que necesitaremos cuando se use la matriz). En nuestro caso serán 6 bytes (3 elementos de 2 bytes cada uno). En general su valor (n) es: n = columnas x 2 (número de columnas por los bytes de cada elemento, 2 en nuestro caso).

Para crear la matriz necesitamos una palabra definidora de matrices. En nuestro caso la llamamos 2-D y esta es su definición (versión simplificada sin chequeos de seguridad, OJO!!):

Código: Seleccionar todo

DEFINER 2-D
 ( filas,columnas - )
 DUP 2* C, * 2*
 ALLOT
DOES>
 ( fila,columna,direccion ini.)
 ROT 1- OVER C@ *
 + SWAP 2* + 1-
;

Cuando creamos la matriz 2x3 (sea NAVES) 2-D espera encontrar en la pila el número de filas (nf) y el número de columnas (nc). Entonces crea un Header para NAVES y luego ejecuta el código antes del DOES>. En nuestro caso lo que hace es guardar en el primer byte (nc*2) que es el número de bytes de una fila. A continuación reserva espacio para los elementos de la matriz (nf*nc*2) pues ésta tiene nf*nc elementos de 2 bytes cada uno (números enteros).

2 3 2-D NAVES

Ya tenemos creada la matriz NAVES. Lo que queremos que haga es que cuando le demos (en la pila) una fila (f) y una columna (c) nos devuelva la dirección de memoria del elemento correspondiente. Es lo mismo que pasaría si ese elemento lo hubiéramos guardado en una VARIABLE de forth: Ejecutando el nombre de una variable nos deja en la pila la dirección de memoria de su contenido. Aquí es lo mismo solo que hay que dar el nombre de la matriz y los índices (f y c) del elemento concreto.
Una vez tenemos la dirección de memoria de un elemento en la pila la podemos usar como cualquier variable normal:

Ej:

2 3 NAVES @ nos devuelve el valor del elemento de la fila dos columna 3, o sea del elemento (2,3)
15 1 2 NAVES ! nos asigna el valor 15 al elemento de la fila 1 columna 2, (1,2)
7 2 1 NAVES +! nos suma 7 al valor del elemento de la fila 2 columna 1, (2,1)
etc.

Internamente, cuando ejecutamos 2 3 NAVES, ponemos 2 y 3 en la pila y NAVES, al ejecutarse hace dos cosas:
1- Pone en la pila la dirección de Inicio del bloque de memoria dónde guarda la matriz (su parameter field)
2- Ejecuta las instrucciones que hay después del DOES> en 2-D.

Y esas instrucciones lo que hacen es a partir de los tres datos en la pila (fila,col,direccion) calcular la direccion del elemento en cuestión (usando la fórmula del dibujo) y ponerla en la pila.

NOTA1 tal como se ha definido 2-D, admite como máximo 127 columnas, pues hay que guardar en un solo byte el número de columnas de la matriz multiplicado por dos.
NOTA2 la fórmula del dibujo se simplifica a: (F-1)*n+C*2+Inicio-1

Funcionamiento Interno Aparte, lo que interesa saber es que una matriz devuelve la dirección de memoria del elemento correspondiente. Lo mismo que haría una variable.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 06 Feb 2022 23:57

Programas Fantasma (II)

Otra cosa que estoy haciendo es que algunas palabras del Programa Fantasma estén disponibles como si estuvieran en el diccionario. O sea, están linkadas al diccionario, pero físicamente están mucho más allá del final físico del diccionario (HERE). Así que las nuevas palabras que se creen después de activarse el programa fantasma (de autoborrarse) se compilarán en el diccionario como si estas palabras no estuvieran. Al final el programa las deslinkará del diccionario y es como si nunca hubieran existido.

Esto lo hago porque hace poco hice una versión fantasma del cargador de código fuente y me he encontrado que las palabras inmediatas "(" y "\" no están disponibles, pues al ser fantasmas no se ejecutan mientras se carga el diccionario en código fuente. Con esta técnica consigo que estas palabras hagan su función y, al no afectar al HERE (pues son fantasmas), al finalizar el programa no dejarán huella en el diccionario recién creado.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 10 Feb 2022 11:55

VLIST inverso y solo de palabras de usuario

Esta pequeña palabra, LISTV, (PCW abril 1983) funciona como VLIST pero solo lista las palabras de usuario y en el orden contrario al de VLIST.

Su funcionamiento es muy interesante, pues rastrea el diccionario "hacia arriba", empezando por el principio en vez de por el final, de ahí el orden inverso del listado. Esto hace que no podamos usar los Link Field (Lnk.F.) de las cabeceras de las palabras, pues estos links van "hacia abajo", o sea, cada palabra apunta a la que le precede.

Esta técnica es la que uso en el Extractor de Diccionario para localizar la siguiente palabra libre para ser redefinida, pues esta búsqueda es hacia arriba.

NOTA: Tal como está definida LISTV fallará si el diccionario ocupa mucha memoria pues, al usar DO LOOP, las direcciones las contempla con signo.

Copio aquí el listado:

Código: Seleccionar todo

: LISTV
  CLS HERE 15440         \ limits of user vocab.
  DO
   I C@ DUP 127 AND      \ fetch char. and strip inverse bit
   EMIT 127 >            \ print. check if inverse, ie last char.
   IF
     SPACE I 1+ @ DUP    \ fetch length field
     0=
     IF
       LEAVE             \ if 0 last word has been listed
     THEN
     1+                  \ add to length to increment loop index
   ELSE
     1                   \ increment index to next char in name
   THEN
 +LOOP
;

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 10 Feb 2022 12:51

Aquí está la palabra del Extractor de Diccionario que utiliza la técnica usada por LISTV:

Código: Seleccionar todo

: NEXTUP
 ( CFA1 - CFA2)
 5 - DUP @ +
 BEGIN
  DUP C@ 127 >
  IF
   6 + 1
  ELSE
   1+ 0
  THEN
 UNTIL
;


NEXTUP espera el CFA (Code Field Address) de una palabra en la pila y devuelve el de la siguiente "hacia arriba"

Funcionamiento detallado:

Por la estructura de la cabecera de una palabra (Header)

Word_map3.jpg
Word_map3.jpg (51.97 KiB) Visto 933 veces

vemos que:

5 - DUP @ +

5 bytes antes del CFA se encuentra el Length Field (LF). El LF contiene la longitud de la palabra sin contar el nombre (7 bytes del Header y lo que ocupe el Parameter Field (PF)). Así, sumando a la dirección del LF su contenido obtenemos la dirección del primer carácter del nombre de la siguiente palabra (hacia arriba).

Bucle BEGIN UNTIL:

Va mirando cada carácter del nombre (códigos ascii) hasta encontrar uno mayor que 127. El último carácter del nombre tiene sumado 128 a su valor ascii (los nombres solo admiten caracteres hasta ascii 127 como máximo). Una vez localizado el último carácter, sabemos por la estructura de la cabecera que el CFA de esta palabra está 6 bytes más adelante.

En el Extractor de Diccionario, NEXTUP se aplica reiteradamente hasta encontrar la primera palabra que no formará parte del nuevo diccionario, o sea, que se puede sobreescribir con REDEFINE.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 11 Feb 2022 22:47

He encontrado un Bug en las palabras ULIST y WLIST que comenté hacia el principio de este hilo. Resulta que para listar el nombre de la palabra usan un bucle DO LOOP con el margen de memoria como límites. Si se da la "circunstancia" que el nombre de la palabra que se está listando comienza en una dirección de memoria positiva y acaba en una dirección de memoria negativa, entonces ese nombre quedaría truncado. Los demás nombres no se ven afectados, por lo que cuesta de ver...

De hecho no lo he visto usando las palabras (y mira que las uso constantemente) sino revisando códigos y darme cuenta que usaban el DO LOOP y que muchos de mis diccionarios superan la dirección de memora 32767. Me extrañó que aparentemente funcionaban bien dichas palabras. Así que forcé la "circunstancia" para apreciarlo.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 12 Feb 2022 00:35

Volviendo al tema sobre las rutinas en código máquina con CODE o como primitivas, comentar dos aspectos más:

1- El método CODE tiene un cierto overhead, pues cada vez que se ejecuta tiene que saltar a la palabra definidora.

2- Si la rutina en código máquina accede al Return Stack esperando encontrar datos allí (como es el caso de mi rutina ULOOP en versión código máquina), debe tenerse en cuenta que CODE lo utiliza, por lo que en este caso no se pueden intercambiar los métodos. O sea, el simple cambio del contenido del CFA de la palabra definida con CODE a CFA+2 para convertirla en primitiva que comenté en su día, lleva al desastre!

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 14 Feb 2022 16:51

Unsigned LOOPS

Como ya expuse en su día, he ido tirando con unos bucles DO/LOOP para límites sin singo (unsigned) combinando dos nuevas palabras UDO/ULOOP con un bucle BEGIN/UNTIL.

Hoy voy a poner aquí la versión hecha con COMPILER para no tener que apoyarme en el BEGIN/UNTIL

O sea, ahora UDO/ULOOP funcionan por sí solas, aunque más lentas.

Código: Seleccionar todo

0 COMPILER UDO
 HERE                 \ Al compilar UDO deja en la pila la dirección dónde comienza el bucle
RUNS>
 DROP R> ROT >R SWAP  \ Coloca en la pila de Retornos los límites del bucle igual que hace el DO 
 >R >R
;


Código: Seleccionar todo

2 COMPILER ULOOP
 HERE - ,               \ Al compilar ULOOP resta de la dirección de inicio del bucle (que dejó antes UDO) la
                        \ dirección justo después de ULOOP y almacena este valor (negativo) a continuación.
                        \ Es su Operand Field (OP).
RUNS>                 
 R> R> 1+ >R >R         \ Incremente el índice
 I' J U< 0=             \ Comprueba si se alcanzó el límite (unsigned)
 IF
  DROP R> R> R> DROP    \ Elimina los límites de la pila de Retornos
  DROP >R
 ELSE
  DUP @ + R> DROP       \ Calcula la dirección de memoria del inicio del bucle y substituye con ésta la que había en
  >R                    \ la pila de  Retornos (repite el bucle).
 THEN                   
;


ULOOP se puede modificar fácilmente para obtener el equivalente a ULOOP+2 (igual pero en vez de uno suma dos) y ULOOP+ (que espera el incremento en la pila).

Ejemplo:

Código: Seleccionar todo

: BUCLE
 -32766 32765 UDO I U.
 ULOOP
 ;


Imprime: 32765 32766 32767 32768 32769

Los tiempos de ejecución para:

Código: Seleccionar todo

: BUCLE1
 TIMEX_INI 1000 0 UDO
 BEGIN
  ULOOP
 UNTIL
 TIMEX
;
 
: BUCLE2
 TIMEX_INI 1000 0 UDO ULOOP
 TIMEX
;


(FAST/SLOW) en segundos por 1000 vueltas.
BUCLE1: 1.0/1.9
BUCLE2: 1.5/2.9

En BUCLE1 la repetición del bucle (la vuelta al principio) la ejecuta BEGIN/UNTIL que es una primitiva de JA, mientras en BUCLE2 hay que hacerlo con palabras FORTH:

Así, ambos esperan un valor en la pila:

BUCLE1: ULOOP pone un 0 en la pila para el UNTIL cuando se ha de repetir el bucle

BUCLE2: ULOOP deja en la pila la dirección donde se almacena el número negativo con los bytes hacia atrás hasta el inicio del bucle y DUP @ + R> DROP >R calculan su dirección de memoria y la ponen en la pila de retornos en vez de la que había.

NOTA: Aunque las palabras UDO y ULOOP de BUCLE1 y BUCLE2 tienen el mismo nombre, internamente son diferentes. Pero ULEAVE sirve la misma para ambos casos.

EDICION: (15-2-2022) He modificado algo ULOOP para que vaya ligeramente más rápido

Código: Seleccionar todo

2 COMPILER ULOOP
 HERE - ,
RUNS>
 R> R> 1+ DUP >R
 I' U<
 IF
  DROP DUP @ + >R
 ELSE
  R> DROP R> DROP >R
  DROP
 THEN
;


Ahora:
(FAST/SLOW) en segundos por 1000 vueltas.
Antes: 1.5/2.9
Ahora: 1.3/2.4
uloops.TZX
(230 Bytes) Descargado 11 veces

Si se quiere un aumento importante de la velocidad habría que hacer que el run-time de ULOOP ejecutara código máquina, como ya hice con el ULOOP del BUCLE1 que tiene su versión c.m.

EDICION-2: (21-3-22) He modificado el ULOOP en Forth para que vaya un pelín más rápido todavía.

Código: Seleccionar todo

2 COMPILER ULOOP
 HERE - ,
RUNS>
 @ R> R> 1+ DUP
 I U<
 IF
  >R + 2- >R
 ELSE
  DROP R> DROP >R DROP
 THEN
;

(FAST/SLOW) en segundos por 1000 vueltas.
Antes: 1.3/2.4
Ahora: 1.2/2.2
No incluyo fichero. Solo listado.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 16 Feb 2022 17:03

He hecho la versión Código Máquina del ULOOP autónomo (el que no usa BEGIN/UNTIL)

NOTA: En realidad, para lo que uso yo el UDO/ULOOP tengo de sobras con la versión "compiler" del post anterior. Todo esto de aquí es por hobby.

Este es le código fuente para el assembler/disassembler de Boldfield

Código: Seleccionar todo

CODE ULOOPMC
      \ ld hl,NN    \ 4 ))
      \ add hl,sp   \      { HL = SP+4 apunta al índice actual del bucle}
      \ inc(hl)     \
      \ jr nz,DIS   \ ~ a
      \ inc hl      \
      \ inc(hl)     \      { índice = índice+1}
      \ jr DIS      \ ~ b 
 ` a  \ inc hl      \
 ` b  \ inc hl      \      { HL = SP+6 apunta al límite del bucle}
      \ ld e,(hl)   \
      \ inc hl      \
      \ ld d,(hl)   \   
      \ ex de,hl    \      { HL = límite del bucle}
      \ pop af      \
      \ pop bc      \
      \ pop de      \
      \ push de     \
      \ push bc     \
      \ push af     \      { DE = índice actual del bucle}
      \ or a        \      { Borro el Carry flag}
      \ sbc hl,de   \      { Resto al límite el valor actual del índice}
      \ jr z,DIS    \ ~ c  { Si iguales se acabó el bucle}
      \ jr c,DIS    \ ~ c  { Si límite menor que índice, fin también}
      \ rst24       \     
      \ ex de,hl    \      { HL = número de la Pila de datos (lo saca)}
      \ ld c,(hl)   \      { que apunta a donde está el desplazamiento}
      \ inc hl      \      { negativo hacia el principio del bucle}
      \ ld b,(hl)   \      { BC = (HL)}
      \ dec hl      \
      \ add hl,bc   \      { HL = HL + (HL)}
      \ pop bc      \
      \ pop de      \
      \ push hl     \
      \ push bc     \      { (SP+2)=HL sin alterar el resto del R.stack}
      \ jp(iy)      \      { se repite el bucle}
 ` c  \ pop bc      \
      \ pop de      \
      \ pop hl      \
      \ pop hl      \
      \ push de     \
      \ push bc     \      { Elmina SP+4 y SP+6 del R.stack}
      \ rst24       \      { Elimina el número en D.stack (DROP)}
      \ jp(iy)      \      { Se termina el bucle y pasa a siguiente word}
ENDC



Estaría bien que alguien lo mejorara, pues me he limitado a traducir "a lo bruto" el código FORTH de la zona run-time de ULOOP (el no optimizado) a código máquina. Seguro que se puede hacer mucho mejor.

ULOOP ahora es:

Código: Seleccionar todo

2 COMPILER ULOOP
 HERE - ,
RUNS>
 ULOOPMC
;


ULOOPMC es la palabra que contiene el código máquina correspondiente al run-time de ULOOP (no ejecutarla a mano!!)

Los tiempos ahora son:

(FAST/SLOW) en segundos por 1000 vueltas.
Antes: 1.3/2.4
Ahora: 0.3/0.38

En cuanto a ULOOPMC, es una de las opciones para introducir el código máquina en ULOOP. Hay tres opciones más:

1-Colocar el código maquina en ULOOP, en la zona del compile-time. Así:

Código: Seleccionar todo

2+n COMPILER ULOOP
 HERE - , n1 C, n2 C, ····
RUNS>
 DUP 2+ CALL
;

Dónde n es el tamaño en bytes de la rutina en código máquina y n1, n2, etc son lo bytes que la componen.

Ventaja: No necesitamos la palabra independiente ULOOPMC
Inconveniente: La versión normal de ULOOP al usarse en una palabra (compilarse) utiliza 4 bytes, mientras que ésta necesita 4+n bytes (en nuestro caso n es de unos 52 bytes). O sea, estamos copiando la rutina en m.c. junto con cada compilación de ULOOP en una palabra dada.

2- Colocar el código máquina en la zona del run-time de ULOOP, algo así:

Código: Seleccionar todo

2 COMPILER ULOOP
 HERE - ,
RUNS>
 PC 2+ CALL EXIT [ n ALLOT ]
;


Colocando posteriormente el código de la rutina en la zona que hemos reservado con "n ALLOT" (n es el tamaño en bytes de la rutina en m.c.).

Ventaja: Poca, porque, si bien no necesitamos ULOOPMC, sí se necesita la palabra PC. Esta palabra es de uso general, y devuelve la dirección de memoria donde se encuentra la siguiente palabra a ejecutar (su CFA compilado).
Inconveniente: La palabra ULOOP ya NO se puede ni listar ni editar. Pero como ni LIST ni EDIT lo saben, puedes hacerlo y en ese caso el JA intentantrá descompilar el código máquina incrustado en ULOOP y ... nada bueno pasará!!.

Palabra PC

Código: Seleccionar todo

CODE PC
  \ POP DE  \    { Copia en la pila de Datos el contenido de}
  \ PUSH DE \    { la primera posición de la pila de Retornos}
  \ RST16   \
  \ JP(IY)  \
ENDC

En nuestro caso PC devolverá la dirección del CFA de 2+ compilado en ULOOP. Cada CFA compilado son 2 bytes. La rutina comienza después del EXIT, por lo que en total será el valor devuelto por PC + 6 (2 bytes por instrucción y hay 3 instrucciones: 2+, CALL y EXIT)

3- Hacer ULOOP 100% código máquina

Es la mejor opción, y el c.m. no es mucho más que el de la opción del ULOOPMC. La dejo para más adelante.
Adjuntos
uloopsmc.TZX
(252 Bytes) Descargado 9 veces

jltursan
Mensajes: 4622
Registrado: 20 Sep 2011 13:59
Ubicación: Madrid
Agradecido : 667 veces
Agradecimiento recibido: 1506 veces
Contactar:

Re: Jupiter Ace Forth: Utilidades

Mensajepor jltursan » 16 Feb 2022 18:07

ULOOPMC es el infierno con tanto push/pop así que ojito con las optimizaciones -grin

Hay alguna cosa que veo que tanto me da, que me da tanto ya que se queda igual de tamaño y ciclos; pero con otra pinta; pero si que veo un par de cosas con potencial según el contexto:

Código: Seleccionar todo

      \ or a        \
      \ sbc hl,de   \
      \ jr z,DIS    \ ~ c
      \ jr c,DIS    \ ~ c


No se que se está comparando; pero tiene pinta que la primera comparación será más rara que la segunda, si es así, se ganarían ciclos intercambiando el orden.

Código: Seleccionar todo

      \ ld c,(hl)   \
      \ inc hl      \
      \ ld b,(hl)   \
      \ dec hl      \
      \ add hl,bc   \


Si sustituyes "dec hl" por "dec c" ganas algún ciclo; pero claro, eso sólo funciona si en C nunca pueda llegar cero y eso no lo veo muy probable...

Lo de los push/pop tiene potencial de optimación (probablemente cambiando el "jr c" por el inverso y cambiando el orden de los bloques se podría ahorrar alguno), veo demasiados; pero ahora mismo no los veo ni emparejados por lo que mejor no tocar ni un pelo :roll:

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 16 Feb 2022 22:36

No se que se está comparando; pero tiene pinta que la primera comparación será más rara que la segunda, si es así, se ganarían ciclos intercambiando el orden.


HL es el límite del bucle y DE es el valor actual del índice. En teoría, el bucle acaba cuando DE = HL, por lo que Z sería cero (el bucle incrementa el índice en una unidad cada vuelta). El C lo pongo por seguridad, por si alguien en su bucle concreto altera el valor del índice por su cuenta y queda mayor que el límite, lo cual es legal hacerlo, pero no muy habitual. Si se usa ULEAVE para terminar el bucle, lo que hace es igualar el límite y el índice.

Si sustituyes "dec hl" por "dec c" ganas algún ciclo; pero claro, eso sólo funciona si en C nunca pueda llegar cero y eso no lo veo muy probable...


HL contiene una dirección que apunta a un valor de 16 bits (sea J). Tengo que hacer HL = HL + J En concreto J es un número negativo (en complemento a dos). Es la parte del m.c. que calcula la dirección de la palabra que hay justo después de UDO, para repetir el bucle.

Lo de los push/pop tiene potencial de optimación (probablemente cambiando el "jr c" por el inverso y cambiando el orden de los bloques se podría ahorrar alguno), veo demasiados; pero ahora mismo no los veo ni emparejados por lo que mejor no tocar ni un pelo


Aquí he ido a saco, a hacer lo que hago en la versión FORTH con los >R y los R> (>R es lo mismo que PUSH y R> equivale a POP). Es un poco diferente que el ULOOP en FORTH porque con ULOOPMC estoy con un nivel más en la pila de Retornos. Opero con los 4 primeros números almacenados en R.Stack (mientras que ULOOP en FORTH opera con los 3 primeros, que serían el 2, 3 y 4 de la lista de abajo), a saber:

Te lo detallo por si ves algo que mejorar:

1-dirección de retorno cuando termine ULOOPMC (el código máquina) (lo coloca el JA) (apunta al ";" al final del bloque RUNS>)
2-dirección de retorno cuando termine ULOOP (lo coloca el JA) (apunta a la palabra FORTH que venga después del ULOOP)
3-valor actual del índice del bucle (lo colocó UDO) (ULOOP, vía ULOOPMC, le suma 1 cada vez que se ejecuta)
4-valor del límite del bucle (lo colocó UDO)

el orden es: si hago un POP, afectaría al "1", otro POP al "2", etc. O sea, el 4 es el más profundo (el que está en una dirección mayor de memoria)

Hay dos partes en las que trabajo con ellos:

La primera es la parte en que pongo el índice y el límite en los registros para hacer la resta que comprueba si se acabó el bucle o hay que seguir repitiendo.

El segundo bloque es lo que se hace con la pila de retornos según el caso (terminar bucle o repetirlo):

  1. Cuando se termina el bucle se han de eliminar el 3 y 4 (los más profundos) (pues los colocó UDO y se han de quitar antes de salir del bucle) y preservar el 1 y el 2
  2. Cuando se repite el bucle se he de substituir el 2 por el valor calculado en la rutina (para provocar la repetición del bucle). Es aquél HL=HL+J de antes

NOTA: Tendría que poner comentarios en la rutina, porque así como está, ni yo sabré lo que hacen dentro de un par de días... ya me ha costado un poco entenderla al escribir este post! -banghead

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 17 Feb 2022 13:06

Hoy he intentado hacer la versión 2 del ULOOP en m.c., la que incrusta el código máquina en la zona run-time y ya veo que no es posible tal cual está. El problema es que al cargar un diccionario que la contenga (o al redefinir otra palabra con ésta), el JA tiene que actualizar los CFAs compilados en la definición de ULOOP y tal como la he descrito, pensará que el m.c. allí puesto son CFAs y los modificará, lo cual vuelve la palabra inservible. Es más, se cuelga en el proceso...
Tendría que definir una palabra que puesta antes del m.c. reservara el espacio que ocupa el m.c. como su Operand field y así se resolvería el problema, pero entonces ya son dos palabras (PC y ésta) para ahorrarnos una (ULOOPMC) lo cual no es negocio. De todos modos lo intentaré por hacer algo.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 17 Feb 2022 17:04

A modo de ejercicio he hecho ULOOP compiler con el m.c. incrustado en la zona Run-time.

Para poder incrustar el código de modo que no se altere al cargar el diccionario o redefinir otra palabra con él, he hecho una palabra compilante de uso general(*). Al usar esta palabra ya no necesitamos PC.

La he llamado RESERVE y lo que hace es reservar tantos bytes a continuación de ella como el número que se le suministra al compilarla. Además de hacer innecesario el uso de PC tiene otra ventaja, a saber, que la palabra ULOOP se puede listar. También se puede editar, pero no es conveniente por lo que luego explicaré.

Código: Seleccionar todo

-1 COMPILER RESERVE
 DUP , ALLOT
RUNS>
;


Armados con RESERVE ya podemos definir ULOOP

Código: Seleccionar todo

2 COMPILER ULOOP
 ( 52)
 HERE - ,
RUNS>
 [ 52 ] RESERVE 2+ CALL
;


He incluido un comentario con el número que precisa suministrársele a RESERVE cuando se define ULOOP. El número se ha de dar en el momento de la compilación, de ahí el uso de los corchetes. No se puede usar ningún número que estuviera en la pila antes de empezar la definición de una palabra, pues el JA usa un sistema de comprobación que no lo permite (el JA comprueba que la pila queda igual al final de la compilación que antes, si no es así, da error #5). Es el número de bytes que tiene la rutina en m.c. (es la misma que ULOOPMC).

Si ahora listamos ULOOP veríamos esto:

Código: Seleccionar todo

2 COMPILER ULOOP
 ( 52)
 HERE - ,
RUNS>
 RESERVE 2+ CALL
;


El 52 en el comentario nos recuerda que hemos reservado 52 bytes de espacio entre el RESERVE y el +2 (sin contar los 2 primeros bytes que se reserva el propio RESERVE para guardar el número)

RESERVE al ejecutarse deja en la pila la dirección dónde se guarda el tamaño de la reserva. Este número está almacenado en los dos bytes que siguen a RESERVE. Luego de ellos vienen los bytes reservados para el m.c. Así, para ejecutar la rutina, usamos CALL con el número que devuelve RESERVER +2.

Ahora hay que cargar el programa en m.c. en ULOOP. Lo hago artesanalmente. Aunque es pesado, solo se hace una vez. Luego ya tenemos ULOOP para siempre.
NOTA: Si se editara ULOOP lo mejor es borrar el ULOOP nuevo, pues tendrá el campo de 52 bytes SIN el m.c. Habría que volver a cargarlo!!

Proceso de carga del m.c. (código máquina) a la zona reservada dentro de ULOOP:

Primero convierto ULOOPMC en tipo CREATE así:

DECIMAL
4076 FIND ULOOPMC !

Luego, con mi volcador hexadecimal a 3 columnas SHOWV (o con cualquier otro medio, incluyento el Debugger del EigthyOne) y sabiendo la estructura general de una palabra tipo COMPILER y la particular de ULOOP:

FIND ULOOP 2+ SHOWV

localizo dónde comienza la zona de 52 bytes reservadas en la zona Run-time de ULOOP. En mi caso era en la posición 20103 (este valor varía si se mueve la palabra). Me hago una palabra para realizar la copia:

Código: Seleccionar todo

: COPY
 52 0
 DO
  I ULOOPMC + C@ I
  20103 + C!
 LOOP
;


y la ejecuto.

Con esto queda la palabra lista para usarse.

Timings: FAST/SLOW en segundos para 1000 vueltas
Método 1 (ULOOPMC): 0.30/0.38
Método 2 (RESERVE) : 0.59/0.77
Hay más overhead (sobre todo el debido a RESERVE)

NOTA: El método 3 (100% m.c.) lo dejo aparcado porque el assembler/Disassmbler de Boldfield (el que uso) se queda corto para lo que hay que hacer. Tendría que usar el TASM o algo por el estilo, con más apoyo, si no sería un lío hacerlo con el Boldfield. Así que queda aparcado.

(*) Al ser de uso general significa que, en el caso hipotético y muy poco probable, que definiéramos varias palabras compilantes (u otro tipo) que incrusten el código máquina de modo parecido, SOLO necesitamos un RESERVE mientras que por el método otro, necesitamos tantas "ULOOPMC"s como palabras definamos.

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 17 Feb 2022 23:11

Elurdio escribió:···NOTA: El método 3 (100% m.c.) lo dejo aparcado···


Le he estado dando vueltas a como podría hacerlo y la cosa no la veo clara.

Puedo hacer una palabra ULOOP 100% m.c. y que sea inmediata. Esta palabra hará todo lo que tiene que hacer tanto en modo compilación como ejecución.

Hay un problema.Supongamos que tengo ULOOP 100% m.c. y que lo uso en la palabra MIBUCLE para definir una estructura UDO/ULOOP. Tiene que guardar dentro de MIBUCLE un número de 2 bytes que le dice cuantos bytes hacia atrás se encuentra el inicio del bucle, para poder repetirlo.

Y este número ha de quedar "oculto" para el JA. Es lo que hacen las palabras compilantes declarándolo como su OF (Operand Field). Pero la mía no lo es y no puede hacerlo. Si no es así, el JA no sabrá que hay un OF a continuación y durante las redefiniciones que hace el JA al cargar (LOAD) y/o mover la palabra en el diccionario (que sucede casi cada vez que se redefine una palabra cualquiera por debajo de MIBUCLE) lo modificará y estropeará la palabra .

Se me ocurre usar un truco. Guardar el número de marras como un número normal justo después del ULOOP. O sea, que ULOOP compile su CFA seguido del CFA de LITERAL y seguido del número. Así me aseguro que el JA no lo modificará, pues sabrá que es el OF de LITERAL. Por otro lado, ya se programa ULOOP de modo que, en modo ejecución, cuando termine se salte el conjunto LITERAL+OF y pase a la siguiente instrucción.

Pero... cuando listemos/editemos MIBUCLE el número aparecerá en el listado y no queda bien. Esto se podría solucionar creando mi propio LITERAL_B que haga lo mismo que LITERAL pero que al ser una compilante de usuario ni LIST ni EDIT pueden mostrar el OF. Eso sí, mostrarían LITERAL_B o cualquiera que sea el nombre que le damos, aunque fuera de una sola letra .... no queda bien, es raro. Y lo que es peor, cuando se compile la palabra editada se repite el LITERAL (el que vuelve a generar ULOOP y el que había...)
Total: Método 3 DESCARTADO!! -no

Elurdio
Mensajes: 410
Registrado: 07 Dic 2021 21:33
Ubicación: Barcelona
Agradecido : 90 veces
Agradecimiento recibido: 84 veces

Re: Jupiter Ace Forth: Utilidades

Mensajepor Elurdio » 18 Feb 2022 11:41

Elurdio escribió:···El número se ha de dar en el momento de la compilación, de ahí el uso de los corchetes. No se puede usar ningún número que estuviera en la pila antes de empezar la definición de una palabra, pues el JA usa un sistema de comprobación que no lo permite (el JA comprueba que la pila queda igual al final de la compilación que antes, si no es así, da error #5)···


Explico en detalle las comprobaciones que hace el JA si usamos palabras compilantes "primitivas" (DO/LOOP, BEGIN/UNTIL, etc.) cuando definimos una nueva palabra:

Veámoslo con un ejemplo:

Supongamos que queremos definir esto:

Código: Seleccionar todo

: BUCLE 5 0 DO I . LOOP ;

¿Que hace el JA durante la compilación de esta palabra tras pulsar el ENTER?

Para verlo sin que se altere la definición, lo haremos así:

Código: Seleccionar todo

: BUCLE [ .S ] 5 0 DO [ .S ] I . LOOP [ .S ] ;

.S es una palabra que nos muestra el contenido de la pila sin modificarla (ver manual del JA pág 143). Tras pulsar ENTER nos saldrá en pantalla (con VIS activado):

Código: Seleccionar todo

: bucle [ .s 10 ] 5 0 do [ .s 10 21313 3 ] i . loop [ .s 10 ] ;

Así vemos que (haciendo esto con la pila vacía y comprobando que sigue vacía después del ENTER) :

  • ":" planta un 10 en la pila
  • "DO" planta el valor de HERE (*) y un 3
  • "LOOP" usa el valor del HERE tras DO y quita el 3 que plantó DO
  • ";" quita el 10 que plantó ":"

Deducimos que el JA comprueba que:

  • El 10 que planta al principio de la definición sigue estando al final de la misma.
  • Cuando se compila el LOOP tiene que haber un 3 en la pila.

Para otras palabras compilantes como :BEGIN, IF/ELSE y WHILE los códigos son 1, 2 y 4 respectivamente.
Todo esto permite al JA comprobar que las compilantes están pareadas (no hay DOs sin LOOPs, etc) y bien anidadas.

Para qué nos sirve saber esto:

  • Si al programar ULOOP hubiera seguido este protocolo entonces el JA se encargaría de comprobar que está todo bien (pareado/anidado).
  • Podría usar ULOOP con DO (no necesitaría el UDO)
  • Podemos engañar al JA para que nuestras compilantes puedan tomar números de la pila (**).
  • Etc.

(*) Que 21313 es el valor del HERE en ese momento, se comprueba de esta manera:

Código: Seleccionar todo

: bucle [ .s ] 5 0 [ here .s drop ] do [ here .s drop ] i . loop [ .s ] ;
que dará este resultado
: bucle [ .s 10 ] 5 0 [ here .s 10 21311 drop ] do [ here .s 10 21313 3 21313 drop ] i . loop [ .s 10 ] ;

(**) Podemos, por ejemplo, utilizar números de la pila (que estaban antes de empezar la definición) siempre y cuando nos aseguremos que antes que se compile el ";" haya un 10 en la pila... etc.

NOTA: Al preparar este post me he dado cuenta que LEAVE no se comprueba, por lo que ULEAVE no es es necesaria. Se puede usar LEAVE en un bucle UDO/ULOOP.
-------------------------------------------------------------------
He aquí una modificación de ULOOP que se ajusta a lo expuesto. Esta versión funciona con DO sin problemas.

Código: Seleccionar todo

2 COMPILER ULOOP
 3 =
 IF
  HERE - ,
 ELSE
  5 ERROR
 THEN
RUNS>
 ULOOPMC
;

ERROR toma un número n de la pila, imprime ERROR n en pantalla y realiza un ABORT.


Volver a “Jupiter Ace”

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 1 invitado