Completando un intérprete de lenguaje BASIC en 2025
Este es una continuación de mi artículo previo
Desarrollando un lenguaje BASIC en 2025, donde describo como me inspiré para comenzar a codificar un intérprete BASIC para el aditamento Mattel ECS de 1983 para Intellivision
A pesar de que mi intérprete ya era muy rápido y con suficientes instrucciones para hacer juegos, no estaba satisfecho porque me faltaba algo que implementa el BASIC ECS: cadenas de texto. Solo tres, A$, B$ y C$, con SET, GET y PUT. Para cosas como asignar una cadena, leer un nombre del teclado y mostrarlo. Cada cadena un máximo de 20 caracteres.
Pensé las cadenas por cuatro días y entonces decidí codificar las cosas como si supiera lo que estaba haciendo. Añadí un apuntador de pila de cadenas bas_strptr donde se agregan todas las cadenas creadas.
La primera cosa que implementé fue un arreglo para las variables de cadena (A$-Z$) cada elemento apuntando a la cadena contenida actual (o cero si ninguna). Modifiqué todo el analizador de expresiones para insertar el tipo en la bandera de acarreo (0= es un número, 1= es una cadena), entonces hice el primer soporte de cadenas en el lenguaje donde detecta si un nombre de cadena aparece (letra más el signo $) y la lee y la copia en una nueva cadena en la pila, regresando este apuntador como valor de la expresión (y por supuesto pone la bandera de acarreo)
El siguiente paso fue asignar las variables de cadena, simplemente tomé el apuntador y lo guardé en su respectivo apuntador de variable de cadena. Por supuesto, tenía miedo de estar creando un monstruo porque no había planeado el recolector de basura.
Entonces me fui a todo vapor y puse soporte para INPUT, PRINT, y añadí la concatenación de cadenas usando el operador de suma, y también las funciones ASC, CHR$, LEN, LEFT$, RIGHT$, MID$, INSTR, VAL y STR$. Por cierto, el BASIC original del Mattel ECS no tenía ninguna de estas.
Ahora tenía soporte de cadenas en mi lenguaje BASIC para ECS, pero en algún punto de la ejecución llenaría la pila, y se botaría con un mensaje "Out of Memory".
Recolección de basura
Era un poco loco tener un BASIC con soporte de cadenas y sin recolección de basura. Necesitaba una forma de copiar cadenas en su variable respectiva, y borrar las cadenas temporales creadas mientras las expresiones se evalúan.
Sería fácil teniendo un sistema de gestión de memoria tipo C ya que solo hay que reemplazar los apuntadores, y liberar el original. Pero cualquier manejo de memoria viene con encabezados y listas enlazadas, requerimientos de memoria extra, y lentitud. Dado que el procesador CP1610 de Intellivision ya es bastante lento (894 khz), me decidí en contra.
Sin embargo, noté que las cadenas temporales solo se creaban dentro del analizador de expresiones. ¿Qué tal una pila doble? Una pila para las cadenas en variables, y otra para las cadenas temporales.
Agregué un apuntador secundario bas_strbase (me gusta como suena como base espacial)
Al comienzo de cada sentencia, bas_strbase se copia en bas_strptr (de esta forma borrando efectivamente las cadenas temporales). Un problema que necesitaba ser solucionado: un apuntador bas_strbase que crece en cada asignación de cadenas.
Iba a implementar la solución más simple posible: pasar sobre las 26 variables de cadena haciendo comparación y movimiento de apuntadores, e insertar la nueva cadena en su lugar.
Justo mientras codificaba esto, noté que había una solución más fácil. Como estaba trabajando con palabras de 16 bits, no todos los valores son usados. Así que podía usar un valor como 0xcafe para marcar el espacio sin usar, y ¡bum! tuve una idea.
Cuando hago la asignación, borrar la cadena original (llenarla con palabras 0xcafe), ahora explorar el área de strbase para encontrar una cadena de palabras 0xcafe lo suficientemente grande para almacenar la nueva cadena.
La mejor parte es cuando no hay espacio para la cadena, simplemente copio el apuntador de cadena como el nuevo apuntador bas_strbase (efectivamente moviendo el área base de memoria), y todas las palabras entre el final de la cadena y el previo apuntador bas_strbase (adelante en memoria) se rellenan con palabras 0xcafe.
Soporte completo de recolección de basura con un pequeño precio en eficiencia. Exactamente lo que un procesador CP1610 necesita.
STRING_TRASH: EQU $CAFE
;
; Asigna una cadena.
; R1 = Apuntador a una variable de cadena.
; R3 = Nueva cadena.
;
string_assign: PROC
PSHR R5
MVII #STRING_TRASH,R4
;
; Borra el espacio usado de la pila.
;
MOVR R3,R2
MVI@ R2,R0
INCR R2
ADDR R0,R2
MVI bas_strbase,R0
CMPR R0,R2
BC @@3
@@4:
MVO@ R4,R2
INCR R2
CMPR R0,R2
BNC @@4
@@3:
;
; Borra la cadena original.
;
MVI@ R1,R2
TSTR R2
BEQ @@1
MVI@ R2,R0
MVO@ R4,R2
INCR R2
TSTR R0
BEQ @@1
@@2:
MVO@ R4,R2
INCR R2
DECR R0
BNE @@2
;
; Busca espacio en zonas de memorias más altas.
;
@@1:
MVII #start_strings-1,R2
CMP bas_strbase,R2 ; ¿Todo examinado?
BNC @@6 ; Sí, salta.
@@5:
CMP@ R2,R4 ; ¿Espacio encontrado?
BNE @@7 ; No, sigue buscando.
CLRR R5
@@8:
INCR R5
DECR R2
CMP bas_strbase,R2
BNC @@9
CMP@ R2,R4
BEQ @@8
@@9:
INCR R2
MVI@ R3,R0
INCR R0
CMPR R0,R5 ; ¿La cadena cabe?
BNC @@7
;
; La cadena cabe en el espacio previo.
;
MOVR R3,R4
MOVR R2,R5
MVO@ R2,R1 ; Nueva dirección.
@@10:
MVI@ R4,R2
MVO@ R2,R5
DECR R0
BNE @@10
PULR PC
@@7:
DECR R2
CMP bas_strbase,R2
BC @@5
;
; Sin espacio.
;
@@6:
MVO R3,bas_strbase ; Crece el espacio para variables de cadena.
MVO@ R3,R1
PULR PC
ENDP
Ejemplo de parser para un juego de aventuras de texto usando cadenas de función.
Volviéndose matemático
Desde que mi librería de punto flotante estaba completa con las cuatro operaciones, tenía un as bajo la manga: Ya había probado funciones sin y cos, pero por alguna razón tenían un error. Para sin(1°) el resultado era 0.0172.
Estas funciones están convertidas de mi
compilador de Pascal para transputer. Sucede que Pascal tiene exactamente las mismas funciones matemáticas que un intérprete de BASIC.
Después de todo un día examinando las operaciones instrucción por instrucción (aquí brilla el depurador de jzintv), descubrí que hice una comparación en la forma equivocada y lo corregí.
Me puse tan contento que de inmediato porté las funciones matemáticas restantes (ATN, TAN, LOG, EXP y derivé SQR y el operator de potencia ^). No hubo problemas en el camino, excepto uno, mi BASIC tiene una mantisa con un bit extra de precisión, y EXP(LOG(64)) retornaba 63.999999
Ambas operaciones usan una multiplicación con una constante (log lo hace al final, y exp al inicio). Noté que el valor estaba mal redondeado para 25 bits de mantisa, así que calculé una mejor constante, y ¡et voila! EXP(LOG(64)) retornó 64.
Más fácil para el usuario
Un montón de intérpretes BASIC en los ochenta no admitían instrucciones para gráficos. El Commodore 64 era particularmente conocido por requerir POKE para casi todo, a menos que tuvieras el cartucho de Simon BASIC que era algo caro.
Sin embargo, el Intellivision tiene pocas capacidades gráficas. En el modo Color Stack tiene algo llamado Colored Squares. Esto significa que cada caracter en la pantalla (20x12) puede tener cuatro colores. Esto significa una resolución bloxel de 40x24, y cada bloxel puede tener uno de ocho colores (uno es el fondo).
Implementé PLOT con estas limitaciones, y también agregué PRINT AT (para poner texto en cualquier posición de la pantalla), y TIMER para medir el tiempo.
Una de las cosas más difíciles fue implementar la lectura de números de punto flotante. Finalmente decidí hacerlo como procesar un entero, tomando nota del número de dígitos procesados, y tomar nota de la posición del punto. Una vez que alcanza el mayor número que puede representar (9,999,999) entonces comienza a ignorar cualquier digito posterior (pero sigue contándolos).
El paso final del cálculo es multiplicarlo o dividirlo tomando en cuenta la posición del punto. También toma en cuenta el exponente presente (por ejemplo, E+1 o E-3)
No fue tan caro en términos de tiempo de computación. Agregué también la función FRE(0) para saber cuanto espacio queda para los programas escritos.
Son los ochentas
Supongamos que estamos trabajando para que este intérprete de BASIC sea realmente útil para el Mattel ECS. Todavía necesitamos dos cosas: cassette e impresora.
Afortunadamente un montón de gente en los foros de Atariage han trabajado a lo largo de los años en descifrar el hardware ECS (gracias a intvnut, lathe26 y decle)
El ECS contiene el hardware para interfazar a un grabador/reproductor de casete a 300 baudios con FSK (Frequency-Shift Keying) de 2400/4800 hz (técnicamente esto es un modem) y también incluye un UART (Receptor/Transmisor Asincrono Universal) bastante parecido a un chip Motorola MC6850, pero el selector de frecuencia está separado, permitiendo activar y desactivar un relé (control remoto del casete), y switchear entre dos puertas (casete y puerta AUX para la impresora).
Ahora para el casete, iba a ir a 300 baudios, y esto significa alrededor de 30 caracteres por segundo. ¿Recuerdas tu modem de 56K? ¡Era 186 veces más rápido! Necesitaba optimizar mi BASIC porque estaba usando números de token arriba de $0100, para moverlos al área $0080-$00ff. Todas las palabras ahora solo usan los 8 bits bajos, y el programa tokenizado puede ser grabado como bytes.
Codifiqué las rutinas de casete basadas en el código publicado por decle en su artículo
ECS Text Editor written in IntyBASIC with tape support y agregué LOAD, SAVE y VERIFY.
Estuve muy feliz cuando estas rutinas de casete funcionaron en emulación, y ordené cables de Amazon para intentar grabar y reproducir en mi teléfono.
Por alguna razón probablemente relacionadas con niveles de audio y compresión automática, solo logré grabar audio del ECS en mi celular, pero al reproducirlo nunca salía nada.
Estaba cansado y decidí probar con mi PC. Conecté el ECS al Mic In y Line Out, y el mismo problema. Además las utilidades de Windows hacen increíblemente difícil cambiar la fuente y la reproducción. Recordé el programa Audacity, y tiene opciones muy cómodas para seleccionar entrada y salida. De nuevo sin resultados.
Escribí un pequeño programa para leer el UART continuamente, y no podía leer nada. Decidí intentar el efecto de amplificar de Audacity, y ¡et voila! Mi programa UART comenzó a aventar bytes decodificados, detuve el programa, e intenté la orden VERIFY (recuerda que justo acababa de guardar el mismo programa), pero no funcionó. Peor, cuando corrí de nuevo mi programa de prueba ¡No obtuve ningún dato!
Revisé los valores de configuración del UART, pero nada. Estuve totalmente confundido por varias horas hasta que recordé un chip que se volvía loco si lo accedías muy rápido. ¿Puede ser eso? ¿Es el CP1610 demasiado rápido? Agregué un retraso en cada acceso del chip UART.
Tecleé de nuevo mi programa de prueba, hice SAVE, grabé en la PC, amplifiqué, corrí mi programa con RUN, y reproducí el audio de vuelta. Muy bien, el UART estaba leyendo cosas. Ahora detuve el programa de prueba, hice VERIFY, ejecuté el audio de nuevo en la PC. Los 20 segundos más largos de mi vida ¡y funcionó!
De inmediato reinicié el ECS, perdiendo el programa, e hice LOAD (por supuesto, reproduciendo el audio) y de nuevo funcionó.
Mi programa de prueba de UART después de un LOAD exitoso.
Aunque graba programas BASIC, estos programas no son compatibles con el ECS BASIC original porque este es un lenguaje BASIC completamente diferente.
Intermedio: Como no iba a volver a perder programas, decidí probar mi lenguaje BASIC con un programa real "largo". Así que tomé El Libro Gigante de los Juegos para Computadora de Tim Hartnell, y tecleé el juego de Reversi. Tuve que adaptarlo porque mi BASIC no admitía arreglos multidimensionales, y también el posicionamiento de pantalla. Encontré unos pocos bugs en mi intérprete (
INPUT W$ todavía no estaba escrito con el soporte para el recolector de basura, y las variables no se borraban correctamente al hacer
RUN), pero fue sorprendente ver el juego de Reversi jugando contra mí. Hice una grabación en WAV
aquí.
Juego Reversi de El Libro Gigante de los Juegos para Computadora de Tim Hartnell funcionando en mi intérprete BASIC para el Mattel ECS.
La impresora está en la habitación
Después de leer
Aquarius Printer Technical Info and Reverse Engineering y el
documento de jzintv sobre ECS, decidí que usar la impresora era muy fácil, y compré un Mattel Aquarius localmente porque incluía la impresora y papel térmico.
Mientras la impresora estaba en proceso de envío, implementé LLIST y LPRINT. Modifiqué el núcleo de ambas sentencias para pasar la salida a través de una función indirecta. Así que solo se cambia el apuntador para poner la pantalla o la impresora como objetivo. Detecté un bug en jzintv que evita que pueda guardar los datos de impresora en un archivo.
Obtuve el Mattel Aquarius junto con la impresora unos pocos días después. Tuve que limpiarlo porque estaba polvoso. La impresora no tenía la cubierta que protege el rollo de papel, pero incluía el rollo de papel, y afortunadamente todavía tenía el cilindro que ayuda a que el papel corra.
Computadora Mattel Aquarius con una tarjeta de expansión, dos juegos, cables, y la impresora Aquarius.
Ajusté el papel, encendí la impresora, y verifiqué que pudiera avanzar el papel (tener un motor funcional es 90% de la impresora)
Construí el cable serie con las instrucciones del artículo de lathe26, y la primera vez no funcionó (aterricé el cable CTS accidentalmente), después de corregirlo, yo esperaba basura en mi primer intento, en lugar de eso, obtuve una muy bonita impresión.
Por supuesto, no pude resistirme a imprimir algunos listados, y una onda sinosoidal. Muy rápida la impresora para funcionar a 1200 baudios.
El código fuente impreso de mi juego UFO. El rollo de papel es muy viejo.
¿Qué queda por hacer?
Agregué las sentencias DRAW y CIRCLE, y la función POINT para terminar el soporte de gráficos. Estas son suficientes para crear bonitos juegos sin usar sprites. Hice una demo gráfica para llenar la pantalla con líneas, y noté que mi generador de números seudoaleatorios no cubría la pantalla, así que lo mejoré.
Programa DRAW para mi intérprete BASIC ECS.
También agregué las funciones POS y LPOS para saber la posición horizontal del cursor. Las funciones SPC y TAB para PRINT. Más la función HEX$ para facilitar la programación de sistemas.
En la tabla de tokenización, agregué marcadores temporales para expandir el lenguaje y no romper la compatibilidad con cualquier cinta creada.
Con esto se volvió un intérprete BASIC completo para el Mattel ECS que usa 38 kilobytes, en lugar de los 48 kilobytes del lento y limitado intérprete BASIC original de Mattel
No veo nada más que pueda hacer en el futuro cercano, tal vez expandir el editor a un editor de pantalla completa. Actualmente es un editor de línea que lee su entrada de la pantalla.
En este punto, es una experiencia divertida el proceso de teclear programas BASIC en el ECS, y ver los resultados. Se pueden guardar los programas o imprimirlos. Y por supuesto, se puede imaginar el éxito que Mattel Electronics hubiera tenido con un buen BASIC en su Mattel ECS.
Pequeñas estadísticas del código ensamblador:
- basic.asm: 5333 líneas.
- fplib.asm: 718 líneas.
- fpio.asm: 462 líneas.
- fpmath.asm: 516 líneas.
- uart.asm: 341 líneas.
- Un total de 7370 líneas de código ensamblador escrito entre el 17 de septiembre y el 12 de octubre, alrededor de 300 líneas al día.
El código fuente esta disponible en
https://github.com/nanochess/ecsbasic. Intenté liberarlo lo más pronto posible, para que puedas ver como iba creciendo en los commits.
¡Disfrutalo!
¿Te encantó este artículo? Invitame un café en ko-fie
Ligas relacionadas
Última actualización: 12-oct-2025