Why stable instruction addresses beat numeric indexes in a C# compiler
The author of the C# interpreter HydraScript explains why numeric instruction indexes broke code generation and how switching to stable IAddress objects fixed it. Addresses now stay with their instructions when code is inserted, removed or replaced, so jumps keep pointing to the right place. It is a concrete case for developers building compilers or interpreters with the Visitor pattern.
Key points
- A numeric instruction index shifts whenever code is removed or inserted, which breaks jump targets.
- IAddress gives each instruction a position-independent identity, and each address links to the next one through Next.
- AddressedInstructions is built from a LinkedList plus two Dictionary maps for fast lookup.
- Replace hands the old address to the new instruction, so existing jumps still land correctly.
- After Remove, the predecessor's Next must be relinked, and Goto instructions pointing at the removed address need separate handling.
- AddRange merges a generated fragment while keeping its internal references, so no offset arithmetic is needed.
The HydraScript interpreter, a C# project, had a code generator that depended on numeric instruction positions. Removing or inserting instructions shifted those numbers, so jumps led to the wrong place and the virtual machine could loop or crash. The author separated the tree walk from instruction storage while moving logic into Visitors.
The fix gives each instruction a stable IAddress object instead of an array index. A collection called AddressedInstructions combines a linked list with dictionaries, and each address exposes a Next link that the virtual machine follows. Remove and Replace keep existing addresses valid, and adding a generated fragment preserves its internal jumps without offset arithmetic. The article shows the design with a small C# example. It is a useful read for anyone writing a compiler or interpreter in .NET.
Real-world use
- A team building a domain-specific language in C# that needs its own bytecode generator and virtual machine.
- A compiler front end that adds optimization passes, such as removing dead assignments, without corrupting jumps.
- An interpreter that walks the AST with the Visitor pattern and emits control-flow instructions.
Why it matters
Numeric addressing forces every transformation to renumber jumps by hand, and mistakes stay hidden until runtime. Stable addresses remove that bookkeeping and make compiler passes safer to change.