Portando mi compilador de C para transputer
por Oscar Toledo G. 21-sep-2026
Hace unas semanas encontré un disco flexible conteniendo mi último compilador de C para transputer. Fue mejorado para ser compilado con Delorie's G++ o DJGPP (1998). Por supuesto, generaba un montón de advertencias en compilación, pero funcionaba bastante bien porque era un compilador de 32 bits siendo compilado en un procesador Intel 80486 de 32 bits.
Este era un paso intermedio antes de convertirlo para generar código para un
procesador AMD Am29000, y esta versión fue mejorada eliminando vestigios de código del Small-C para tener un analizador léxico adecuado, sintaxis ANSI C y un preprocesador completo.
Como un ejercico de compilar cosas viejas, intenté compilar esta versión de 1998 de mi compilador de C para transputer en una computadora moderna de 64 bits, una Macbook Air M1, y encontré un montón de dificultades. Por ejemplo:
- El código usa enteros y apuntadores de forma intercambiable (ambos de 32 bits)
- El tipo FILE * no fue usado, en lugar de eso se usó int.
- No hay prototipos para las funciones del compilador, así que clang se quejó por una buena razón debido a que los apuntadores son de 64 bits y los enteros son de 32 bits.
Algunos de estos problemas de portabilidad fueron heredados de Small-C. Ya que no tenía la palabra struct, todas las estructuras requeridas eran creadas en un arreglo de bytes (char), y las palabras eran divididas en dos bytes (para el procesador 8080), y cuando lo expandí a 32 bits, estás palabras se volvieron de 4 bytes. Y peor, los apuntadores eran convertidos al tipo int. Pero ahora es imposible convertir un apuntador de 64 bits a un entero de 32 bits. ¡Se requiere un cambio de diseño!
Fragmento de código de Small-C donde un valor es salvado como dos bytes en un arreglo.
Primeros pasos
Esta no sería la primera vez que un programa antiguo no puede ser compilado en un sistema moderno de 64 bits. Sin embargo, en lugar de dejar que este compilador permanezca como una curiosidad funcionando solo en emulación, así que hagamoslo funcionar en una laptop moderna. Spoiler: No fue fácil.
Me puse un objetivo: Quería modificar el compilador lo suficiente para compilarlo en una plataforma moderna de 64 bits, pero todavía quería que fuera posible compilarse a si mismo en el transputer. Esto significa evitar usar cosas no implementadas en mi compilador.
El compilador se divide en varios archivos de código fuente, y todos son llamados de un solo archivo llamado cc.c.
Mi port para DJGPP creó dos archivos diferentes: cc.c y cc2.c. El primero fue creado para ser compilado con DJGPP, y el segundo era para compilarse con mi compilador de C directamente en el transputer.
Comencé por mover las variables entrada, salida, and entrada2 a estos archivos principales. Modifiqué el designado para la máquina moderna para usar FILE *, y también comencé a agregar prototipos de función K&R. Los prototipos K&R solo están hechos para indicar el tipo de retorno correcto. Por ejemplo, unsigned char *expresion();.
Que comience la diversión
La directiva #include salva el estado de procesamiento actual dentro del código fuente, y lo hace de esta forma:
incl[nivel_incl++] = entrada;
incl[nivel_incl++] = funcion_actual;
incl[nivel_incl++] = comienzo_funcion;
incl[nivel_incl++] = linea_actual;
incl[nivel_incl++] = dentro_funcion;
Donde incl es un arreglo de enteros. La primera línea es para el archivo actual, y la siguiente es para el apuntador a la definición de función, los otros tres son enteros. ¿Puedes ver el gran problema? Una máquina de 64 bits tiene apuntadores de 64 bits, mientras que int sigue siendo 32 bits.
También todo el código fuente del compilador sigue usando comentarios en español así como en los nombres de variables.
Reescribí el código en términos de struct:
struct {
FILE *entrada;
unsigned char *funcion_actual;
int comienzo_funcion;
int linea_actual;
int dentro_funcion;
} incl[MAX_INCL];
Y la nueva versión del código es portátil y considerablemente más clara:
incl[nivel_incl].entrada = entrada;
incl[nivel_incl].funcion_actual = funcion_actual;
incl[nivel_incl].comienzo_funcion = comienzo_funcion;
incl[nivel_incl].linea_actual = linea_actual;
incl[nivel_incl].dentro_funcion = dentro_funcion;
nivel_incl++;
Así como el rediseño progresó, hice más prototipos de funciones. Justo cuando estaba pensando que todo era muy fácil...
No es tan fácil
La primera llamada de atención vino de la rutina para procesamiento de expresiones:
/*
** Analiza una expresión, y genera el codigo.
*/
unsigned char *expresion()
{
struct nodo *origen;
unsigned char *tipo;
origen = ultimo_nodo;
tipo = almacena_expresion(SI);
evalua_arbol(NO);
libera_arbol(ultimo_nodo);
ultimo_nodo = origen;
return tipo;
}
Esta función realiza la compilación de una expresión en C, y regresa un apuntador al tipo. Sin embargo, la función almacena_expresion hace esto:
/*
** Analiza una expresión y la mantiene en memoria.
*/
int almacena_expresion(operador_coma)
int operador_coma;
{
int info[1], izq;
if (operador_coma) {
if (nivel0(info))
carga_valor(info);
} else {
if (nivel1(info))
carga_valor(info);
}
return info[0];
}
El apuntador de tipo se guarda en un arreglo int. Comencé a reescribir el archivo ccexpr.c completo para cambiar int info[1] a unsigned char *info;.
En expresion_constante tuve que reemplazar int origen con struct nodo *origen. Ya que aparentemente mi compilador no daba advertencias al asignar un apuntador a entero y viceversa.
Pronto descubrí que el procesamiento de tipos tenía sus propios problemas de apuntador. Por ejemplo, hay una subrutina de copia de tipos, y se ve como esto:
/*
** Copia un tipo en la siguiente posición disponible.
*/
copia_tipo(tipo)
unsigned char *tipo;
{
int a;
while (*tipo >= APUNTADOR) {
if (*tipo == MATRIZ) {
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
} else
guarda_tipo(*tipo++);
}
if (*tipo == STRUCT) {
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
guarda_tipo(*tipo++);
}
guarda_tipo(*tipo++);
}
Esta subrutina realiza la copia de una descripción de tipo C. Es útil cuando se pone el mismo tipo para múltiples variables como int a, b, c;. La función guarda_tipo simplemente copia un byte en el almacenamiento de tipos.
Sin embargo, el tipo de arreglo (MATRIZ) contiene el tamaño del arreglo (32 bits salvados como cuatro bytes), y el tipo de estructura (STRUCT) apunta a la definición de estructura. Un apuntador de 32 bits convertido a un entero y salvado como cuatro bytes. De nuevo totalmente no portable a una arquitectura de 64 bits.
La forma más fácil es extender el código a ocho llamadas guarda_tipo (64 bits en ocho bytes), pero por suerte no hice eso.
También tenía el problema de que las estructuras se asignaban en el mismo almacenamiento de bytes, justo como esto (sigue código realmente penoso):
/*
** Una nueva estructura.
*/
unsigned char *nueva_estructura(nombre)
unsigned char *nombre;
{
unsigned char *ap;
int conteo;
if(ultima_estruct != NULL)
escribe_entero(ultima_estruct + EST_SIG, sig_tipo);
ultima_estruct = sig_tipo;
if(lista_estruct == NULL)
lista_estruct = sig_tipo;
conteo = 0;
while(conteo++ < EST_NOMBRE)
guarda_tipo(0);
while(*nombre)
guarda_tipo(*nombre++);
guarda_tipo(0);
return ultima_estruct;
}
Y entonces cada miembro de la estructura también se asigna en este almacenamiento de bytes.
Seguí adelante y lo rastrée hasta tres grupos de macros donde cada grupo define una pseudo-estructura:
/* Definiciones de estructura */
#define EST_QUE_ES 0 /* char, indica si es un rótulo de struct o enum */
#define EST_ES_UNION 1 /* char, indica si es una unión o una estructura */
#define EST_TAM 2 /* int, tamaño total de la estructura/unión */
#define EST_LISTA 6 /* char*, lista de miembros */
#define EST_SIG 10 /* char*, siguiente rótulo */
#define EST_NOMBRE 14 /* char[], rótulo */
/* Definiciones de miembros */
#define MIE_TIPO 0 /* char*, tipo del miembro */
#define MIE_POSICION 4 /* int, posición dentro de la estructura */
#define MIE_SIG 8 /* char*, siguiente miembro */
#define MIE_NOMBRE 12 /* nombre del miembro */
/* Definiciones de enumeradores */
#define ENUM_VALOR 0 /* int, valor del enumerador */
#define ENUM_SIG 4 /* char*, siguiente enumerador */
#define ENUM_NOMBRE 8 /* char[], nombre del enumerador */
Hay cinco apuntadores embebebidos aquí. Podía trabajar una conversión de esto a estructuras, pero tuve una mejor idea. Mi
compilador de C Am29000 es una evolución de este mismo compilador, y ya había reemplazado este código feo con structs elegantes. Así que hice un port hacia atrás (backport), cuando tomas código más reciente y lo adaptas a una versión antigua.
El código portado se ve realmente bien, y además, es portable:
struct rotulo { /* DEFINICIÓN DE ESTRUCTURA */
struct rotulo *sig; /* Siguiente rótulo */
int tam; /* Tamaño total de la estructura */
struct miembro *lista; /* Lista de miembros */
char que_es; /* Indica si es un rótulo de struct o enum */
char es_union; /* Indica si es una unión o una estructura */
char nombre[1]; /* Nombre */
};
struct miembro { /* DEFINICIÓN DE MIEMBRO DE ESTRUCTURA */
struct miembro *sig; /* Siguiente miembro */
int posicion; /* Posición dentro de la estructura */
unsigned char *tipo; /* Tipo declarado */
char nombre[1]; /* Nombre */
};
struct enumerador { /* DEFINICIÓN DE ENUMERADOR */
struct enumerador *sig; /* Siguiente enumerador */
int valor; /* Valor del enumerador */
char nombre[1]; /* Nombre */
};
Al mismo tiempo trasladé todos los mensajes de error de español a inglés, porque mis archivos de código fuente seguían usando un codepage de Windows y clang se quejaba de codificaciones de caracteres ilegales.
Mi compilador no tenía originalmente una librería C estándar, así que algunas funciones como isxdigit, strlen, strcpy, y strcat se replicaban en todos los programas. Puse estas en mi programa principal cc2.c, mientras que cc.c incluye las librerías estándar strings.h y ctype.h.
Solucioné unos pocos errores más que venían de versiones anteriores cuando los nodos de expresión de cambiaron de int a struct nodo * como este:
int izq;
izq = ultimo_nodo;
Donde int izq debía ser struct nodo *izq.
Unos pocos casos más de arreglos usados en lugar de structs (de nuevo del tiempo cuando struct no estaba disponible)
/*
** Sentencia "break"
*/
void s_break()
{
/* Ve si hay un bucle abierto */
if (ultimo_bucle == NULL) {
error("No loops open");
return; /* No */
}
desp_pila(ultimo_bucle[B_PILA]); /* Si, arregla la pila */
salto(ultimo_bucle[B_FIN]); /* Salta a la etiqueta de salida */
}
Y ahora la nueva versión:
void s_break()
{
/* Ve si hay un bucle abierto */
if (ultimo_bucle == NULL) {
error("No loops open");
return; /* No */
}
desp_pila(ultimo_bucle->pila); /* Si, arregla la pila */
salto(ultimo_bucle->fin); /* Salta a la etiqueta de salida */
}
25 errores hasta el éxito
Después de todos estos cambios, un montón de tiempo ¡y mucho café! Logré reducir los errores a 25 relativos a usar apuntadores y enteros. Estos pueden ser categorizados de la siguiente forma:
- Guardar/leer un apuntador para datos de struct data (3 ocurrencias)
- Leer el tipo de una variable, argumento o función (5 ocurrencias)
- Usar un apuntador de nodo de expresión para guardar un entero (17 ocurrencias)
No estoy particularmente orgulloso de este código, pero veamos un ejemplo de lo que hice:
if (nodo_temp->oper == N_RESULTA) { /* Función que retorna estructura */
pals += (req_res = nodo_temp->der);
nodo_b = -1;
req = NO;
} else {
nodo_b tiene tipo struct nodo * pero es asignado -1 sin siquiera una conversión de tipos. Solo asigné nodo_temp a nodo_b, y en lugar de comparar con -1, cambié la comparación a node_b->oper == N_RESULTA.
Podía pasar el día completo pensando en formas de no expandir struct nodo, pero la perfección es enemiga de hacer algo funcionar, así que solo agregué las franjas para manejar los datos extra. No hubiera podido hacer esto en los antiguos tiempos, ya que el espacio de memoria era importante; probablemente se pudo haber solucionado con una union, pero unas pocas franjas más en los árboles de expresión no usan demasiada memoria.
Las franjas agregadas a struct nodo fueron: struct nodo *tri y int extra_val.
Tuve que jugar con algunas franjas y vi oportunidades para optimizar, pero no optimicé para evitar introducir más errores.
Lo reduje a 8 errores. Unos pocos sucedieron porque eran apuntadores a tipos de estructura, así que los reemplacé con un indice de estructura (contando desde el principio de una lista lineal).
Ahora había bajado a 5 errores al acceder definiciones de variables globales y locales:
/* Define formato de los nombres */
#define NOMBRE 0
#define IDENT 17
#define CLASE 18
#define NIVEL 19
#define TIPO 20
#define POSICION 24
De nuevo una estructura codificada para hardware, TIPO apunta a la definición de tipo, y de nuevo no portable a 64 bits. Hagámoslo de nuevo, creando un struct para esto:
#define NUM_GLBS 608
#define NUM_LOCS 32
struct nombres {
unsigned char nombre[17];
unsigned char ident;
unsigned char clase;
unsigned char nivel;
unsigned char *tipo;
int posicion;
};
struct nombres globales[NUM_GLBS];
struct nombres locales[NUM_LOCS];
Remover las viejas definiciones creó una multitud de errores y advertencias, pero lentamente reemplacé cada uno con el acceso correcto a las nuevas estructuras. ¡Y finalmente cero errores! Solo unos pocos mensajes acerca de usar
gets (una función de entrada ilimitada que permitió el
gusano de 1988 creado por Richard Morris), pero podía vivir con eso.
¿Pero funciona?
Comencé a ejecutar el compilador consigo mismo, y generó errores porque inserté accidentalmente algunas secuencias de llamada ANSI C en funciones, y tuve que reemplazarlas con sintaxis K&R.
También encontré algunos pequeños errores en el manejo de variables (restos de los ajustes), y finalmente logré una compilación completa del compilador mismo.
Cualquier con experiencia en compiladores sabe que esto es no suficiente. La salida puede estar defectuosa, y tuve que probarlo en mi
sistema operativo de transputer.
Después de correr tasm, mi ensamblador de transputer, descubrí que NULL estaba sin definir, y que estaba invocando fputs y stdout (no disponible en mi sistema operativo).
void mensaje(cad)
unsigned char *cad;
{
fputs("\n", stdout);
fputs(cad, stdout);
}
Por supuesto, esto era un artefacto del port a DJGPP. Corregí con el código correcto de mi compilador funcional.
void mensaje(cad)
unsigned char *cad;
{
puts("\n");
puts(cad);
}
Después de correr de nuevo tasm cc2.a cc2.e stdio.len, obtuve el siguiente mensaje exitoso:
Transputer assembler v0.1. Feb/01/2025
by Oscar Toledo G. https://nanochess.org/
0 error(s) detected.
28254 line(s) assembled.
Generó un ejecutable cc2 con un tamaño de 35997 bytes. Para referencia mi compilador previo era de 38176 bytes. Es más corto porque usar struct evita largos códigos para acceso por bytes. Ahora ¿puede funcionar en mi sistema operativo de transputer? *badum-tss*
Antes de hacer la prueba, recordé el parche de valor temporal de punto flotante que descubrí después de 30 años
en el ray tracer para mi sistema operativo de transputer, y lo agregué.
Después hice dos pequeñas pruebas con HOLA.C y PRUEBA.C en el mismo directorio donde puse la versión de DJGPP, que afortunadamente contenía también los archivos ensamblador generados. Los compilé con la nueva versión ¡y éxito! El código ensamblador generado era exactamente el mismo.
Intentándolo en mi sistema operativo
Contruí la imagen de disco con todos los archivos para el compilador. El objetivo era que se compilara a si mismo. Esperaba que todavía cupiera en los 128 KB. de RAM.
Mi línea de comandos para construir esta imagen de disco fue:
./buildboot -fd -v2 .floppy.img . tree/SOM.32.bin tree/Halt.p CC.c CC2.c CCanasin.c CCexpr.c CCgencod.c CCinter.c CCvarios.c CCvars.c cc2.a cc2.e
Una vez dentro del sistema operativo (run_os_v2.sh), corrí c:/ejecutable.p para entrar el archivo cc2.e, y puse una pila de 8192 bytes, más cero bytes para los datos extra, y esto creó el ejecutable cc2.p.
Lo ejecuté y mostró basura en la pantalla ya que estaba preparado para emitir sequencias de escape ANSI para color en las órdenes, pero mi sistema operativo no lo admite. Lo alimenté con su propio código fuente, y falló intentando compilar inicializa()
La depuración acaba de comenzar
Como esto es emulación, inmediatamente di un vistazo a la imagen de disco, y pude ver que el compilador en mi emulador de transputer estaba generando exactamente el mismo código que el ejecutable de macOS. Esta es una captura de pantalla de lo que mi compilador logró generar antes de trabarse.
Fracción del código generado por mi compilador de C para transputer.
Ciertamente prefiero que mi programa falle con un crash o que genere basura en los archivos, porque estas son pistas. ¿Pero un bug de trabado?
Activé la salida de depuración de mi emulador de transputer, y obtuve un gigantesco archivo de 8.14 gigabytes que no pude abrir en XCode ni en TextEdit. Utilicé Hex Fiend y pude ver inmediatamente un valor equivocado en un registro. Ahora tenía que revisar millones de líneas de código tratando de encontrar donde había fallado.
Un valor equivocado en un registro es obvio por las siguientes razones: Uno, el código no usa constante "grandes", y dos, las direcciones del sistema operativo son bien conocidas.
Fui capaz de rastrearlo a la función etiqueta() (etiquetado de árbol con uso de registros), donde comenzaba a hacer cosas raras después de procesar un nodo de expresión con el operador 40 (N_ASIGNA) para un nodo de asignación de variable. Y entonces vi que el valor de Wptr (la pila de transputer) continuaba creciendo hacia direcciones más bajas, y ese era el problema. Sobreescribía el código del compilador y fallaba.
Salida de depuración mostrando Wptr (pila del transputer) chocando con Iptr (apuntador de instrucciones)
Después de dos horas, fui capaz de encontrar el problema en el nodo N_ASIGNA, cuyo nodo izquierdo apunta a si mismo. Ahora, necesitaba encontrar donde se hacía el nudo dentro del compilador.
Después de otra media hora descubrí que mi función malloc ¡Siempre retornaba la misma dirección! ¡Vaya! Un error inesperado en mi sistema operativo. Cuando el espacio de memoria esta lleno, la rutina malloc simplemente falla en retornar un apuntador NULL.
Modifiqué mi emulador de transputer para agregar 128 kb. de RAM extras, y me las arreglé para compilar el compilador de C sin ningún error. Mejor todavía, generó el mismo código ensamblador byte por byte. Puedes ver que los archivos cc2.a y cc3.a son iguales. cc2.a fue generado en macOS, mientras que cc3.a fue generado en el emulador de transputer.
Listado de archivos mostrando que cc2.a y cc3.a tienen el mismo tamaño de archivo.
Sólo me tomó tres días para lograr que mi compilador pasara de ser no portable a un estado usable. Estoy bastante contento de que ahora puedes construir los mismos ejecutables de transputer en cualquier máquina.
Repositorio
Consideré que la historia de desarrollo sería más clara usando los commits del repositorio de gits como una forma de ver los cambios entre versiones. El orden de commits fue como este:
- El compilador de C de mi sistema operativo de transputer.
- El primer respaldo del disco flexible (cc0 en mi repositorio transputer)
- El segundo respaldo del disco flexible (cc1 en mi repositorio transputer)
- El compilador portado final que puede funcionar en máquinas de 64 bits.
Recomiendo mirar los cambios entre versiones. Es realmente ilustrativo.
¿Sabías que los
desarrolladores de open source pagados crean mejor software? Nutre mi creatividad
con un café ☕. O unete al viaje y da un apoyo mensual, tendrás incluso mejor contenido ¡y buen karma! También puedes comprar mis libros en
Lulu.com, mis ebooks y juegos en
mi tienda digital.
Ligas relacionadas
Última actualización: 21-sep-2026