Making portable my unportable transputer C compiler
by Oscar Toledo G. Sep/21/2026
A few weeks ago, I found a floppy disk containing my latest C compiler for transputer. It was enhanced to be compiled with Delorie's G++ or DJGPP (circa 1998). Of course, it generated a lot of warnings in compilation, but it worked just fine because it was a 32-bit compiler being compiled over a 32-bit Intel 80486 processor.
This was an intermediate step before converting it to generate code for an
AMD Am29000 processor, and this version was further improved removing vestigial Small-C constructs to have a proper lexical analyzer, ANSI C syntax, and full preprocessor.
As a throwback exercise, I tried to compile the 1998 version of my transputer C compiler in a modern 64-bit computer, a Macbook Air M1, and I found a lot of difficulties. For example:
- The code uses integers and pointers interchangeably (both were 32-bit)
- The type FILE * wasn’t used, instead int was used.
- There are no prototypes for the compiler functions, so clang complained for good reason because pointers are 64-bit, and integers are 32-bit.
Some of these portability problems were inherited from Small-C. As it didn’t had the struct keyword, all required structures were created in a byte pool (char), and words were divided in two bytes (for the 8080 processor), and as I expanded it for a 32-bit platform, these word now were 4 bytes. And worst, pointers are converted to the int type. But now it is impossible to convert a 64-bit pointer into a 32-bit integer. A design change is required!
Small-C code fragment where a value is saved as two bytes in an array.
First steps
This wouldn't be the first time that an old program cannot be compiled in a modern 64-bit system. However, instead of letting this compiler stand as a curiosity working only in emulation, let's make it to work on your modern laptop. Spoiler: It wasn't easy.
I put myself an objective: I wanted to modify the compiler just enough to compile in a modern 64-bit platform, but still be able to compile itself for transputer. This means I could use only things implemented in my compiler.
The compiler is divided in several source files, and all are called from a single driver file called cc.c.
My DJGPP port created two different files: cc.c and cc2.c. The first one meant to be compiled with DJGPP, and the second one meant to be compiled with my C compiler directly on the transputer.
I started by moving the variables entrada, salida, and entrada2 to these main files. I modified the one meant for the modern machine to use FILE *, also I started adding Kernighan&Ritchie function prototypes. K&R prototypes are only meant to indicate the correct return type. For example, unsigned char *expresion();.
Let’s start with the fun
The #include directive saves the current state of the processing inside the source file, and it does it this way:
incl[nivel_incl++] = entrada;
incl[nivel_incl++] = funcion_actual;
incl[nivel_incl++] = comienzo_funcion;
incl[nivel_incl++] = linea_actual;
incl[nivel_incl++] = dentro_funcion;
Where incl is an integer array. The first line is for the current file, the next one is a pointer to the function definition, all the other three are integers. Can you see the big problem? A 64-bit machine has 64-bit pointers, while int is still 32-bit.
Also all the compiler source code is still using Spanish comments and variable names.
I rewrote the code in terms of a struct:
struct {
FILE *entrada;
unsigned char *funcion_actual;
int comienzo_funcion;
int linea_actual;
int dentro_funcion;
} incl[MAX_INCL];
And the new version of the code is portable, and considerably more clean:
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 the redesign progressed, I made more prototype functions. Just when I was thinking it was a piece of cake...
It isn’t so easy
The first attention call comes from the expression processing subroutine:
/*
** 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;
}
This function does compilation of a C expression, and returns a pointer to the type. However, the almacena_expresion function does this:
/*
** 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];
}
The type pointer is saved into an int array. I started to rewrite the full ccexpr.c file to change int info[1] to unsigned char *info;.
In expresion_constante I had to replace int origen with struct nodo *origen. As apparently my compiler gave no warnings about assigning a pointer to integer and vice-versa.
Soon I found that the type processing had its own pointer problems. For example, it has a type copy subroutine, and it goes like this:
/*
** 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++);
}
This subroutine does a copy of a C type description. It is useful when setting the same type for multiple variables like int a, b, c;. The function guarda_tipo simply saves a byte into the type pool.
However, the array type (MATRIZ) contains the array length (32-bit saved as four bytes), and the struct type (STRUCT) points to the structure definition. A 32-bit pointer converted to an integer and saved as four bytes. Again completely non-portable to a modern 64-bit architecture.
The easiest way is extending the calls to eight guarda_tipo function calls (64-bit into eight bytes). But fortunately I found another way.
Also, there is the problem that structures are allocated in the same byte pool, just like this (embarrassing code appears below):
/*
** 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;
}
And then each member in the structure is also allocated into the type pool.
Furthermore, I tracked it to three groups of macros where each group defined a pseudo-structure:
/* 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 */
There are five pointers embedded there. I could work a conversion of this to structures, but I had a better idea. My
Am29000 C compiler is an evolution of this same compiler, and I had replaced already these ugly definitions with elegant structs. So I could do a backport, this means I took more recent code and fitted it into an old code.
The backported code looks really nice, and better, it is 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 */
};
At the same time, I also translated all the error messages from Spanish to English, because my source code files still were using a Windows codepage, and clang kept complaining of illegal character encodings.
My compiler originally didn’t had a standard C library, so some functions like isxdigit, strlen, strcpy, and strcat were replicated in each program. I put these in my cc2.c driver program, while cc.c includes the standard libraries strings.h and ctype.h.
I solved a few more errors that came from earlier versions when the expression nodes changed from int to struct nodo * like this one:
int izq;
izq = ultimo_nodo;
Where int izq should be struct nodo *izq.
A few more cases where arrays where used instead of structs (again coming back from the time when struct wasn’t available)
/*
** 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 */
}
And the new version:
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 errors till success
After all these changes, a lot of time, and coffee! I managed to get down to 25 errors of using pointers and ints. These could be categorized in the following way:
- Saving/reading a pointer for struct data (3 occurrences)
- Reading the type of a variable, argument or function (5 occurrences)
- Using an expression node pointer to save an integer (17 occurrences)
I’m not particularly proud of this code, but let’s see an example of what I did:
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 has type struct nodo * but it is assigned -1 without even a type cast. I just assigned nodo_temp to nodo_b, and instead of checking for -1, I checked for node_b->oper == N_RESULTA.
I could pass the whole day thinking in ways to not expand struct nodo, but perfection is the enemy of "it works", and instead I just added fields to handle the extra data. I wouldn’t have done this in the old times, as memory space was important; probably I would have solved it with an union, but a few more fields in expression trees don’t use too much memory.
The added fields to struct nodo were: struct nodo *tri, and int extra_val.
I had to do some fields juggling, and I noticed chances for optimization, but I didn't optimize to avoid introducing further bugs.
I was down to 8 errors. A few of these happened because there are pointers to struct types, so I replaced these with a struct index (counting from the start of a linear list)
Now down to 5 errors accessing global and local variable definitions:
/* Define formato de los nombres */
#define NOMBRE 0
#define IDENT 17
#define CLASE 18
#define NIVEL 19
#define TIPO 20
#define POSICION 24
Again a hard-coded structure. TIPO points to the type definition, and again unportable to 64-bit. Let's do it again, creating a struct for this:
#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];
Removing the old definitions created a multitude of errors and warnings, and I slowly replaced each one with the correct access to the new structures. And at last zero errors! Only a few messages about using
gets (an unlimited input function that allowed the
1988 worm created by Richard Morris), but I could live with it.
But can it work?
I started running the compiler against itself, and it generate errors because I inserted accidentally some ANSI C calling sequences in functions, so I had to rework these as K&R syntax.
Also I found some small bugs in variable handling (left over of the adjustments), and I finally got a full compilation of the compiler itself.
Anyone with compiler experience knows this isn't enough. The output could be completely buggy. I had to test it in my
transputer operating system.
After running tasm, my transputer assembler, I discovered that NULL was undefined, and that it was invoking fputs and stdout (not available in my operating system).
void mensaje(cad)
unsigned char *cad;
{
fputs("\n", stdout);
fputs(cad, stdout);
}
Of course, this was an artifact of the DJGPP porting. I got the correct code from my working compiler.
void mensaje(cad)
unsigned char *cad;
{
puts("\n");
puts(cad);
}
After running again tasm cc2.a cc2.e stdio.len, I got this successful message:
Transputer assembler v0.1. Feb/01/2025
by Oscar Toledo G. https://nanochess.org/
0 error(s) detected.
28254 line(s) assembled.
It generated an executable cc2 sizing up at 35997 bytes. For reference, my previous compiler was 38176 bytes. It is shorter because using struct avoids lenghty byte access code. Now, could it work in my transputer operating system? *badum-tss*
Before doing the test, I remembered the floating-point temporary value patch I discovered after 30 years
in the ray tracer for my transputer operating system, and I added it along.
Also I did two small tests with the HOLA.C and PRUEBA.C in the same directory from where I put the DJGPP version, which fortunately contained also the generated assembler files. I compiled these with my new compiler, and success! The emitted assembler code was exactly the same.
Trying it in my operating system
I built the floppy image with all the files for the compiler. The objective was getting it to compile itself. I expected it still fitted into the 128 KB of RAM.
My command-line for building the floppy disk was:
./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
Once inside the operating system (run_os_v2.sh), I ran c:/ejecutable.p to input the cc2.e file, and setup a stack of 8192 bytes, plus zero bytes of extra data, and this created the cc2.p executable.
I then executed it. It displayed trash on the screen as it was prepared to emit ANSI escape sequences for color in commands, but my operating system doesn’t support it. I feed it with its own source code, and it crashed trying to compile inicializa()
The debugging has just started
As this is emulation, I immediately gave a look to the floppy disk image, and I could see the compiler on my transputer emulator was generating exactly the same code as the macOS executable. This is a screenshot of what my compiler managed to compile before getting stuck.
Fraction of the code generated by my transputer C compiler.
I certainly prefer when programs crash in flames or create trash in the files, because these are clues. But a fully stuck bug?
I enabled the debug output in my transputer emulator, and I got an 8.14 GB file *clown face* that I couldn’t open in XCode nor TextEdit. I used Hex Fiend and I could see immediately a wrong value in a register. I now had to revise some millions of lines trying to find where it went nuts.
A wrong value in a register is obvious because the following reasons: One, your code doesn’t have big constants, and two, the operating system addresses are well-established.
I was able to track it to the function etiqueta() (tree labeling with register usage), where it started doing weird things after processing an expression node with operator 40 (N_ASIGNA) for a variable assignation node. And then I saw the Wptr value (the transputer stack), it continued growing toward lower addresses, and that was the problem. It overwrote the compiler code and crashed.
Debugging output showing Wptr (transputer stack) crashing over Iptr (Instruction pointer)
After two hours, I was able to find that the problem is in the N_ASIGNA node, whose left node points to itself. Now, I needed to find where the compiler made this knot in the tree.
After another half-an-hour I found that my malloc function always returned the same address!!! Wow! An unexpected bug in my operating system. When the memory space is full, the malloc routine simply fails to return a NULL pointer.
I modified my transputer emulator to add a further 128 kb. of RAM, and I managed to compile the C compiler without any error. It is even better, it generated the same assembler file byte per byte. You can see the cc2.a and cc3.a files. Where cc2.a was generated in macOS, while cc3.a was generated by the transputer emulator.
File listing displaying cc2.a and cc3.a with the same file length. cc2.a was compiled in macOS, and cc3.a was compiled in the transputer emulator.
It only took me three days to get my compiler from an unportable status into an usable state. I’m pretty happy that now you can build the same transputer executables in any machine.
Repository
I considered the development history would be more clear by using the git repository commits as a way to see the changes between versions. The order of commits is this:
- The C compiler from my transputer operating system.
- The first backup in the floppy disk (cc0 in my transputer repository)
- The second backup in the floppy disk (cc1 in my transputer repository)
- The final ported compiler that can work with 64-bit machines.
Feel free to give a look of the changes versus versions. It is really enlightening.
Did you know that
paid open source developers create even better software? Fuel my creativity
with a coffee ☕. Or join the journey and become a monthly supporter, you’ll get even better content, and good karma! You can buy also my books in
Lulu.com, my ebooks and games in
my digital store.
Related links
Last modified: Sep/21/2026