Skip to content
HN On Hacker News ↗

How I developed an Am29000 C compiler and web browser

▲ 92 points 22 comments by nanochess 1w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,738
PEAK AI % 0% · §1
Analyzed
Aug 17
backend: pangram/v3.3
Segments scanned
1 windows
avg 1738 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,738 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

by Oscar Toledo G. Aug/16/2026 If you have read my previous article, you’ll know that I developed a windowed operating system in 32-bit machine code for a homebrew computer based on the Am29000 processor. In this article, I’ll talk about the development of my C compiler for these processors, and a web browser. The time period was between Christmas 1998 and my birthday in 1999. I was age 20, Internet was spreading like fire in Mexico, Bruce Willis just saved the Earth from a giant asteroid, new careers emerged for the nascent Internet (it was a gold year for graphic designers), people was scared that the year 2000 bug would trigger a digital armageddon (even the Simpsons ran an episode where Homer forgets updating the computers), and Arnold Schwarzenegger was killing demons with bullets in End of Days. Find me a C compiler Along 1997, I developed some utilities, printer drivers (I had an HP DeskJet 500, and managed to print in color in the Epson Stylus 600), and even managed to send and receive fax using the modem card. It was a time when everyone asked if you had a fax machine to send you advertisements, or to get information. We even bought a fax machine, and the next year, no one asked again for a fax. Welcome to the e-mail! Anyways, working in machine code was hard, and it was like doing a deep dive in muddy water. Unless you get a dive mask to see under (the notes about addresses and some documentation), you’ll get more and more lost. Even with all my teen energy, I started to get tired, because I couldn’t code new functions without devising a careful memory planning, how to move the code to make space, or worst, relocate several jumps and introducing unexpected bugs because I missed one change. At some point, I just thought “this could grow” and inside the code you can find sequences of 5 to 10 NOP instructions for further expansion. Another thing you can find is routines out of place, because these didn't fit the original place. As I was already a regular visitor to an Internet café (or more known in Mexico as cybercafé). One of the first Internet café was located just crossing the street from the now defunct Bazar Pericoapa, and it also served coffee. We browsed the Internet at the rhythm of "Ciega, sordomuda", "Amor de papel", “Laura no esta” and “Barbie girl”. Of course, they soon recognized their mistake when cappuccinos and expressos were spilled into keyboards, and coffee was never served again. I was searching anything about the Am29000 processor, and I found about the High-C 29k compiler, and GNU C compiler v2.8.1 with support for Am29000. I had no way of buying the High-C 29k compiler, so I could download only the GCC sources, and I found it required at least 2 mb. of RAM in the computer (and probably more if we think in the virtual memory), when my computer only had 512 kb of RAM. Worst, it required two more programs: Flex and Bison. Also it required a lot of support from the underlining operating system, that I barely had (plus an assembler and a linker). I needed to bootstrap the compiler somehow, but I was completely unwilling to port two big programs for a single use. So, I resorted to a closer galaxy: my C compiler for the transputer. My main problem is the completely different architecture of the Am29000 processor with many registers. I couldn’t figure how to assign the registers in my single pass compiler. It was pretty important that normal variables could be kept in local registers, but if a single indirection appeared (for example, &a) then that variable should be kept in memory. My first try was a port of the Small-C compiler to Am29000, I know I did it because I made a note in my daily log in December 1997. Probably it was an utter failure and lacking usefulness, because there’s no further mention of it. Again in February 2, 1998 I mention i needed urgently a C compiler, and I installed DJGPP (a GCC compiler ported to MS-DOS) on a 80486 PC to help with development. I couldn’t use the transputer as it only had 128 KB of onboard RAM. DJGPP is the abbreviation of DJ G++, I cannot say how so much DJ Delorie helped to developers all around the world when the compilers were still sold for big prices, and this guy created a version of the GNU C++ compiler for DOS that worked right away. Growing a compiler in the tree It was until May 6, 1998 when I took the source code of my C compiler for transputer, and managed to compile it with DJGPP as a test. This means I had to replace my non-standard input/output functions with standard C library functions. My daily log didn’t include any further information, but while searching for more data, I found I preserved all the steps of the Am29000 C compiler creation in a floppy disk. Here is a picture of the floppy disk with my C compiler progression. I had a vague idea of source code control because I had read about SCCS (Source Code Control System), and my approach was “copy all the daily files into a floppy disk”. This floppy disk contains two enhanced transputer C compilers, and the first version of my Am29000 C compiler. This transputer C compiler now worked in a PC machine the same as in the original transputer. The tree expressions were preserved in arrays. One array for pointing to left nodes, another array for pointing to right nodes, another array for node value, and another array for node type. Of course, this means you couldn’t create complex expressions without expanding the array as needed. You can find this compiler in my transputer git in the directory cc0. This is a code excerpt of the expression tree as an array (function crea_nodo): ++ultimo_nodo; if(ultimo_nodo == TAM_ARBOL) { error("Expresión muy compleja"); cancela(); } nodo_izq[ultimo_nodo] = izq; nodo_der[ultimo_nodo] = der; oper[ultimo_nodo] = op; esp[ultimo_nodo] = val; regs[ultimo_nodo] = 0; regsf[ultimo_nodo] = 0; I slowly created a plan: There was a single way of creating an Am29000 code generator. I needed to parse the whole function into memory, then I would know how many local registers were required, detect references to local variables, and then I could build a register allocator. Next, I redesigned the expression tree generator using dynamic memory (malloc/free), and using struct. It was still made for the transputer (see the cc1 directory). Per my notes, on breaks I was also playing a demo of Tomb Raider 2. This is a code excerpt of how the node creation code changed: ultimo_nodo = malloc(sizeof(struct nodo)); if (ultimo_nodo == NULL) { error("Expresión muy compleja"); cancela(); } /* ... */ ultimo_nodo->izq = izq; ultimo_nodo->der = der; ultimo_nodo->oper = op; ultimo_nodo->esp = val; ultimo_nodo->regs = 0; ultimo_nodo->regsf = 0; This code is far more legible than the original one, and also it is only limited by the total of memory available. In May 13, 1998, I finally bite the bullet, and I started to work in the main parser to save all of the code in an intermediate representation in trees with linked lists. A sequence of statements became a linked list, and any nested statement became a branch in the list. I got a cold this time, I watched “The Jungle Book” with Jason Scott Lee in Laserdisc, and after I recovered I went directly to create the code generator for the Am29000 processor. The whole port took me well over two weeks, and I had to make several small tests for the code generator. For example, this is the code generator in the transputer: /* ** Codigo para cada operador binario, y algunos unarios. */ gen_oper(oper, rev) int oper, rev; { if (oper == N_NULO) return; if (oper == N_CUENTA) emite_linea("wcnt"); else if (oper == N_OR) emite_linea("or"); else if (oper == N_XOR) emite_linea("xor"); else if (oper == N_AND) emite_linea("and"); else if (oper == N_IGUAL) { emite_linea("diff"); emite_linea("eqc 0"); } else if (oper == N_SUMA) emite_linea("bsub"); else if (oper == N_MUL) emite_linea("prod"); And this is the same fragment for the Am29000 processor: /* ** Codigo para cada operador binario, y algunos unarios. */ gen_oper(oper, inmediato, reg1, reg2, constreg, control) int oper, inmediato, reg1, reg2, constreg, control; { int reg; if (oper == N_OR || oper == N_AOR) { gen_inst1("or", inmediato, reg1, reg2, constreg); } else if (oper == N_XOR || oper == N_AXOR) { gen_inst1("xor", inmediato, reg1, reg2, constreg); } else if (oper == N_AND || oper == N_AAND) { gen_inst1("and", inmediato, reg1, reg2, constreg); } else if (oper == N_CD || oper == N_ACD) { gen_inst1("sra", inmediato, reg1, reg2, constreg); The transputer with its stack architecture takes care of the register usage, but in the Am29000 the compiler controls how each register is used. And now for just an example of the complexity of the processor, this is the code for starting a C function: /* ** Prologo de función: ** ** o Asigna las variables virtuales a los registros o a la memoria. ** o Asigna el espacio requerido. ** o Copia los argumentos de la entrada (si es requerido) */ prologo_funcion() { int variable, temp, por_copiar = 0, posicion, registro; /* ** Asignamos los registros (por el momento no se sabe si van a ser locales ** o globales), también asignamos espacio en la pila pero aún falta ** determinar si va a ser corrida para hacer espacio a argumentos que ** deben ser copiados. */ variable = 0; while (variable < variables_virtuales) { switch (virtuales[variable] & 3) { case 0: /* Variable para asignar como se pueda */ if (virtuales[variable + 1] != 0) { /* ¿ Necesita apuntador ? */ virtuales[variable] = (pila << 2) | 1; pila += virtuales[variable + 2] ? 8 : 4; } else { /* No, queda en registro */ if (virtuales[variable + 2]) /* Alinea punto flotante */ pila_regs = (pila_regs + 1) & ~1; virtuales[variable] = pila_regs << 2; pila_regs += virtuales[variable + 2] ? 2 : 1; } virtuales[variable + 1] = 0; break; case 1: /* Variable que debe quedar en memoria */ temp = virtuales[variable] >> 2; virtuales[variable] = (pila << 2) | 1; pila += temp; virtuales[variable + 1] = 0; break; case 2: /*