Simulando CHAIN en el ZX Spectrum o como encadenar programas multicarga en el Basic Sinclair. He decidido reunir todas mis notas en un solo artículo, respecto al modo de emular en el ZX Spectrum, el comando CHAIN de algunas versiones de BASIC, pero curiosamente tambien inexistente en los BASIC del APPLE, Commodore y Atari entre otros competidores del Spectrum. PARTE I CHAIN & COMMON CHAIN Literalmente cadena o sucesión. To chain = encadenar. Permite que un programa demasiado largo para caber en la memoria de una micro computadora, sea dividido en varias partes. Esta sentencia hace que un programa llame y ejecute otro programa de modo automático, como LOAD pero sin borrar las variables. Por ejemplo, un programa puede permitir al usuario dar el numero de horas de trabajo de los empleados. Este haría CHAIN a otro programa que listaria los pagos y a su vez invocaría a un tercero que daría el costo por horas basándose en los datos del fichero. Un buen uso de CHAIN seria en el diseño de juegos. Si hubieran en el juego, presentaciones y textos, dichas instrucciones podrían ser puestas en un programa que se encadenaría al programa principal del juego, dejando libre mucha de la memoria de la computadora. sintaxis 1: CHAIN "nombre"{,linea}{,ALL} Ejemplos: CHAIN "prueba1" - como comando directo carga y ejecuta prueba1 desde la primera linea del programa 1000 CHAIN "prueba2" - como linea final del programa, llama y pasa el control al sigte. CHAIN "prueba2",500 - lo ejecuta a partir de la línea 500 CHAIN "prueba2",,ALL - pasa todas las variables del programa actual al encadenado, en este caso a prueba2. sintaxis 2: CHAIN MERGE "nombre"{,DELETE line1 - line2} Ejemplos: CHAIN MERGE "demo1" - fusiona el programa actual con el programa demo1 (salvado previamente solo en formato ASCII) CHAIN MERGE "demo2",DELETE 500-1000 - elimina las lineas seleccionadas del programa actual antes de cargar demo2. COMMON Instrucción que sólo tiene sentido usada con CHAIN. Permite seleccionar las variables que se van a pasar del programa que inicia la cadena al programa encadenado. Esta instrucción debe estar normalmente al principio del programa. Ejemplo: COMMON texto$(),libre,actual,ultimo - la orden CHAIN pasará sólo las variables seleccionadas por COMMON, haciéndolas comunes a todos los programas encadenados o enlazados e ignorará todas las demás del programa principal. Ejemplo 1 de CHAIN: 10 REM PROGRAMA TEST 20 PRINT "EL PROGRAMA TEST CORRE AHORA" 30 FOR X=1 TO 9 40 PRINT X; 50 NEXT X 60 PRINT "ESTE PROGRAMA DEBE ENCADENAR DE VUELTA CON EL PROGRAMA PRINCIPAL" 70 CHAIN "MAIN", 40 99 END Salvar este programa en disco o cinta bajo el nombre TEST y a continuación: 10 REM PROGRAMA MAIN 20 PRINT "ESTE PROGRAMA DEBE CARGAR Y CORRER EL PROGRAMA TEST" 30 CHAIN "TEST" 40 PRINT "CHAIN PASO LA PRUEBA SI EL PROGRAMA TEST" 50 PRINT "IMPRIMIO UNA SERIE DE NUMEROS" 99 END salvarlo bajo el nombre MAIN y ejecutarlo. Replicándolo mediante MERGE Ahora veremos como el ejemplo anterior es replicado en el Spectrum usando la instrucción MERGE: 100 REM PROGRAMA TEST 110 PRINT "EL PROGRAMA TEST CORRE AHORA" 120 FOR X=1 TO 9 130 PRINT X; 140 NEXT X 150 PRINT "ESTE PROGRAMA DEBE ENCADENAR DE VUELTA CON EL PROGRAMA PRINCIPAL" 160 GOTO 40 salvar con: SAVE "test" 10 REM PROGRAMA MAIN 20 PRINT "ESTE PROGRAMA DEBE CARGAR Y CORRER EL PROGRAMA TEST" 30 MERGE "test": GOTO 100 40 PRINT "CHAIN PASO LA PRUEBA SI EL PROGRAMA TEST" 50 PRINT "IMPRIMIO UNA SERIE DE NUMEROS" 60 GOTO 10000 Salvar esta parte con: SAVE "main" LINE 10 Ejemplo 2 Encadenado mediante LOAD El sigte. ejemplo, el cual no requiere invocar a un nro. de línea, pensado originalmente para el TRS-80 con discos, nos muestra como encadenar tres programas, adaptado para el Spectrum, usando LOAD en vez de MERGE. Nota: los listados originales usaban RUN "nombre" en vez de LOAD. 10 REM PROGRAMA MAIN 20 PRINT 30 PRINT "ESTE ES EL PROGRAMA PRINCIPAL" 40 INPUT "ENCADENAMOS AL PROGRAMA 1 O 2 ";i 60 IF i=1 THEN LOAD "test1" 70 IF i=2 THEN LOAD "test2" SAVE "MAIN" LINE 10 10 REM PROGRAMA TEST1 20 PRINT "EL PROGRAMA TEST1 CORRE AHORA" 30 FOR X=1 TO 9 40 PRINT "UNO", 50 NEXT X: PRINT 60 PRINT "NOS ENCADENAREMOS DE VUELTA CON EL PROGRAMA PRINCIPAL" 70 LOAD "MAIN" SAVE "test1" LINE 10 10 REM PROGRAMA TEST2 20 PRINT "EL PROGRAMA TEST2 CORRE AHORA" 30 FOR X=1 TO 9 40 PRINT "DOS", 50 NEXT X: PRINT 60 PRINT "NOS ENCADENAREMOS DE VUELTA CON EL PROGRAMA PRINCIPAL" 70 LOAD "MAIN" SAVE "test2" LINE 10 Ejemplo 3 con CHAIN y COMMON: 10 REM programa TEST 20 COMMON A 30 PRINT "EL PROGRAMA TEST RECIBE A=";A 40 A=A*2 50 CHAIN MAIN,50 99 END 10 REM programa MAIN 20 COMMON A 30 A=5 40 CHAIN TEST 50 PRINT "Y RETORNA A=";A 99 END Replicándolo mediante DIM y SAVE/LOAD DATA (). En este caso todas las variables que seran consideradas COMMON, comunes para ambos programas deben ser reemplazadas por variables DIM. La variable A del ejemplo debe ser inicializada en el programa principal MAIN, salvada antes de llamar al sigte. programa de la cadena, el cual la cargará en su inicio y la volverá a salvar para que sea usada por la sigte. encadenación. 10 REM programa TEST 20 LOAD "A.DAT" DATA A() 30 PRINT "EL PROGRAMA TEST RECIBE A=";A(1) 40 LET A(1)=A(1)*2 45 SAVE "A.DAT" DATA A() 50 LOAD "T2" 10 REM programa MAIN 20 DIM A(1) 30 LET A(1)=5 35 SAVE "A.DAT" DATA A() 40 LOAD "TEST" 10 REM programa T2 20 LOAD "A.DAT" DATA A() 50 PRINT "Y RETORNA A=";A(1) 99 REM END De este modo, una línea que contenga: COMMON texto$(),libre,actual,ultimo se reemplazaría por: DIM t$(x): DIM n(3) donde x = la cantidad de caracteres de t$ y n contendría las tres variables libre,actual y ultimo, siendo salvadas y cargadas por cada programa de la cadena, como variables en comun. Equivalente Betabasic Si se tienen las Versiones BetaBasic 3 y 4, exclusivas para Microdrive y Opus Discovery, estas disponen de dos instrucciones que permiten salvar y verificar solamente los datos de un programa. Ejemplos: SAVE DATA 1,"VARS.bbv" y VERIFY DATA 1,"VARS.bbv" Tomaremos ahora los programas del ejemplo 3 y lo adaptaremos del sigte. modo: 10 REM programa TEST 20 MERGE "A.var" 30 PRINT "EL PROGRAMA TEST RECIBE A=";A 40 LET A=A*2 45 SAVE DATA "A.var" 50 LOAD "T2" 10 REM programa MAIN 30 LET A=5 35 SAVE DATA "A.var" 40 LOAD "TEST" 10 REM programa T2 20 MERGE "A.var" 50 PRINT "Y RETORNA A=";A 99 REM END En suma, CHAIN permite crear y trabajar con programas multicarga en BASIC, compartiendo entre sí, ya sea todas las variables (ALL) o solamente una selección mediante COMMON. Es muy util cuando la máquina con la cual se trabaja tiene muy poca memoria para almacenar un programa largo. En el caso del Spectrum, ejecutar un programa de más de 60 k por ejemplo, lo que obliga a dividir el programa en varias partes encadenadas todas por compartir datos en común. PARTE II Salvando sólo las variables Ahora, ¿qué otras opciones nos ofrece el Basic del ZX Spectrum, para manipular y salvar solamente las variables, si no se tienen las versiones de Betabasic que admiten la orden SAVE DATA "nombre", o si no se desea usar la combinacion DIM - SAVE/LOAD DATA ()? Eso es lo que trataremos a continuación: Primer intento Salvar el área de las variables como bytes de código máquina. 1 DEF FN z(z)= ((PEEK 23627+256*PEEK 23628) AND z=1) +((PEEK 23641+256*PEEK 23642) AND z=2): DEF FN w()=FN z(2)-FN z(1) 10 LET a=20 20 LET a$="alfa" 30 LET beta=a*3 100 FOR z=FN z(1) TO FN z(2) 120 PRINT PEEK z;" ";: NEXT z 130 PRINT ''FN w() 150 STOP 200 SAVE "readvars": STOP 205 SAVE "vars.var"CODE FN z(1),FN w() La línea 1 define las funciones con las cuales podremos saber cuales son el inicio y el final del área de variables (FN z) así como la longitud en bytes (FN w), para poderlo salvar. Primero salvamos con RUN 200, luego ejecutamos con RUN y por último hacemos GO TO 205. Para verificar que todo salió bien, hacer las sigtes. modificaciones: 10 LET a=0 20 LET a$="" 30 LET beta=0 Lo ejecutamos y enseguida hacemos LOAD "vars.var"CODE FN z(1). Los valores de las tres variables vuelven a ser 20, "alfa" y 60 respectivamente. Funciona con la sintaxis de todos los sistemas de disco para Spectrum, incluidos el Microdrive y Wafadrive. El problema es que si se cargan tras un NEW, RUN o CLEAR, o por otro programa, obtendremos el error de Variable no encontrada. Es decir que solo funciona, si el programa que los cargue tambien los tiene ya predefinidos en memoria. Dicho esto, su única utilidad es recuperar los valores en caso de ser modificados dentro del mismo programa que los usa. Otro dilema es que cualquier otra nueva variable será borrada al recargar las variables salvadas por este modo. Segundo intento Salvar el área de las variables como programa Basic. Usamos el programa ejemplo anterior y reemplazamos las líneas 200 y 205 por: 200 SAVE "savevars": STOP 400 POKE 23635,PEEK 23627: POKE 23636,PEEK 23628: SAVE "vars.sav": POKE 23635,203: POKE 23636,92 Los primeros POKEs de la línea 400, mueven el área de Basic al área de variables haciendo que el listado Basic desaparezca, quedando sólo las variables para ser salvadas. Los últimos POKEs devuelven todo a la normalidad. Primero salvamos con RUN 200, luego ejecutamos con RUN y por último hacemos GO TO 400. Para verificar que todo salió bien, hacer NEW, y cargar las variables archivadas como "var.sav", usando LOAD o preferentemente MERGE, si se desea usarlas en otro programa. Esta rutina funciona igual que el SAVE DATA ... del Betabasic 3 y 4. Funciona también con la sintaxis del MBO2+, D80 Mdos, Opus Dos y Disciple. El Wafadrive requiere en cambio la sigte. modificacion: 200 SAVE *"savevars2": STOP 400 POKE 23728,PEEK 23635: POKE 23729,PEEK 23636: POKE 23635,PEEK 23627: POKE 23636,PEEK 23628 404 SAVE *"vars.sav": POKE 23635,PEEK 23728: POKE 23636,PEEK 23729 Los primeros POKEs de la línea 400, son variables del sistema no usadas, a excepción del Timex FDD que las requiere para salvar su propia información. Ya que el wafadrive al igual que el Microdrive, mueve el área de variables, es necesario salvar previamente el área de Basic para su posterior recuperación tras hacer el SAVE que salva sólo las variables, y así continuar con la ejecución normal del programa. Pero en el caso del Betadisk con TRdos: 200 RAND.USR 15619: REM: SAVE "savevars2" 202 STOP 400 POKE 23728,PEEK 23635: POKE 23729,PEEK 23636: POKE 23635,PEEK 23627: POKE 23636,PEEK 23628 402 RAND.USR 15619: REM: SAVE "vars.sav" 404 POKE 23635,PEEK 23728: POKE 23636,PEEK 23729 El caso del Microdrive Es al parecer imposible de usar esta rutina con el Microdrive, debido a que genera el error Sin sentido en Basic, con lo que el programa desaparece y lo deja a uno solo con las variables. Si bien estas aun se pueden salvar, es solamente con un SAVE como comando directo, pero no desde un programa Basic. Hay que volver a POKEar para que reaparezca el listado, de lo contrario cualquier intento de hacer un LOAD u otra operacion Microdrive, genera un RESET del sistema o un buen cuelgue. Tercer intento Eliminar el Basic dejando solo las variables en memoria En el caso del Betabasic basta con hacer DELETE x TO y, para eliminar las lineas de Basic y dejar solamente las variables, para, a continuación usar MERGE para cargar el otro programa de la cadena. La instrucción 'CHAIN MERGE "nombre" DELETE x,y' puede ser emulada por: DELETE x TO y: MERGE "nombre". El ejemplo 1 de la parte I ahora debe verse del sigte modo: 10 REM PROGRAMA TEST 20 PRINT "EL PROGRAMA TEST CORRE AHORA" 30 FOR X=1 TO 9 40 PRINT X; 50 NEXT X 60 PRINT "ESTE PROGRAMA DEBE ENCADENAR DE VUELTA CON EL PROGRAMA PRINCIPAL" 9000 REM CHAIN main 9010 DELETE TO 60 9020 MERGE "main": GOTO 40 salvar como SAVE "test" 10 REM PROGRAMA MAIN 20 PRINT "ESTE PROGRAMA DEBE CARGAR Y CORRER EL PROGRAMA TEST" 30 GOTO 9000 40 PRINT "CHAIN PASO LA PRUEBA SI EL PROGRAMA TEST" 50 PRINT "IMPRIMIO UNA SERIE DE NUMEROS" 60 GOTO 10000 9000 REM CHAIN test 9010 DELETE TO 60: MERGE "test": GOTO 10 Salvar como SAVE "main" y ejecutar con RUN. Tal como vemos en los listados del ejemplo, es necesario que exista un GOTO tras cada MERGE, si es que deseamos que se ejecute el programa invocado de este modo. De hecho DELETE puede omitirse en el ejemplo dado ya que ambos programas tienen los mismos numeros de línea y eso hace más facil la sustitución. Pero si los listados difiriesen demasiado, DELETE es la mejor alternativa para borrar esas lineas innecesarias del programa anterior. Si no se tuviese el Betabasic en ninguna de sus versiones, habría que usar alguna de las muchas rutinas de borrado de líneas que se hayan publicado para el spectrum y añadirlas a cada listado del programa multicarga. El único problema de usar MERGE, es que hay que esperar a diferencia de LOAD, para que el programa corra, ya que primero debe reubicar todas las lineas en su lugar. Para cerrar, dos ejemplos más: Reemplazando COMMON por DIM, y CHAIN por SAVE/LOAD DATA () Ej.1 Versión original QuickBasic rem main defint i, k dim a$(25, 25) common a$() for i = 0 to 25 for k = 0 to 25 print i, k, spc(10) a$(i, k) = str$(i) print a$(i, k) next k next i chain "code2" rem code2 dim a$(25, 25) common a$() print for i = 0 to 25 print a$(i, 1) next i end Versión Spectrum 10 REM programa PRINCIPAL 20 DIM a$(26, 26): REM COMMON a$() 30 DIM s$(32) 40 FOR i = 1 to 26 50 FOR k = 1 to 26 60 PRINT i;" ";k; s$(TO 10) 70 LET a$(i, k) = STR$(i) 80 PRINT a$(i, k) 90 NEXT k 100 NEXT i 110 SAVE "code.dat" DATA a$() 120 LOAD "code2" 10 REM code2 20 LOAD "code.dat" DATA a$ 30 PRINT 40 FOR i = 1 to 26 50 PRINT a$(i, 2) 60 NEXT i Ej.2 Versión original QuickBasic ' ' *** COM1_EX.BAS - COMMON statement programming example ' DIM Values(1 TO 50) COMMON Values(), NumValues PRINT "Enter values one per line. Type 'END' to quit." NumValues = 0 DO INPUT "-> ", N$ IF I >= 50 OR UCASE$(N$) = "END" THEN EXIT DO NumValues = NumValues + 1 Values(NumValues) = VAL(N$) LOOP PRINT "Leaving COM1_EX.BAS to chain to COM2_EX.BAS" PRINT "Press any key to chain... " DO WHILE INKEY$ = "" LOOP CHAIN "com2_ex" ' ' *** COM2_EX.BAS - COMMON statement programming example ' DIM X(1 TO 50) COMMON X(), N PRINT PRINT "Now executing file com2_ex.bas, reached through a CHAIN command" IF N > 0 THEN Sum = 0 FOR I = 1 TO N Sum = Sum + X(I) NEXT I PRINT "The average of the values is"; Sum / N END IF Version Spectrum 10: 20 REM *** COM1_EX.BAS - Ejemplo de programa con COMMON 30: 40 DIM V(50): DIM n(1) 50 REM COMMON V(), n() 60 PRINT "Entre los valores uno por linea. Teclee 'END' para terminar." 70 LET NumValues = 0 80 REM DO 90 INPUT "-> "; N$ 100 IF I >= 50 OR N$ = "END" THEN GOTO 140 110 LET NumValues = NumValues + 1 120 LET V(NumValues) = VAL(N$) 130 GOTO 80: REM LOOP 140 PRINT "Saliendo de COM1_EX.BAS para enlazar con COM2_EX.BAS" 150 PRINT "Pulse una tecla para continuar... " 160 IF INKEY$ = "" THEN GOTO 160 170 SAVE "comv.dat" DATA v() 175 LET n(1)= NumValues: SAVE "comn.dat" DATA n() 180 LOAD "com2_exbas" 10: 20 REM *** COM2_EX.BAS - Ejemplo de programa con COMMON 30: 40 LOAD "comv.dat" DATA v(): LOAD "comn.dat" DATA n() 50 PRINT 60 PRINT "Ejecutando com2_ex.bas, llamado por la orden CHAIN" 70 IF N > 0 THEN LET Sum = 0: FOR I = 1 TO N(1): LET Sum = Sum + v(I) : NEXT I 80 PRINT "El promedio de los valores es"; Sum / N(1) Notas: Los ejemplos usados en la PARTE I sobre CHAIN fueron tomados y adaptados del "THE BASIC Handbook. 2nd. Edition". Los POKEs usados en el segundo intento de la PARTE II, fueron tomados de la sección IDEAS de la revista ZX #16. Todo lo demás es mi propia recopilación. (C) 2014 zx_if1@hotmail.com