LOOP_OVER, Explained
In a recent discussion about what it would take to port Inform 7 to another language, one line of analysis drew a reasonable reaction of “I’m sorry… but WHAT???” — the claim that the compiler uses “giant linked lists of every allocated object, traversed by macros.” It sounds unhinged. It is actually one of the most deliberate design decisions in the codebase, and it deserves a proper explanation.
The mechanism
In Inform’s source, you don’t declare a compiler data structure with a plain
typedef struct. You declare it with the classdef keyword provided by
inweb, the literate-programming system the compiler is written in. Here is the real
declaration of instance — the structure behind every
“there is a red ball in the attic”-style object in your story:
classdef instance {
struct noun *as_noun; /* the name of the instance */
...
struct parse_node *creating_sentence;
int enumeration_index;
...
}
When inweb tangles this literate source into C, it doesn’t just emit the struct. For each such class it also generates a couple of hidden bookkeeping fields (an allocation ID and next/previous pointers), an allocator, and a global doubly-linked list threading through every object of that class ever created. Allocation goes through a macro:
instance *I = CREATE(instance);
which takes memory from a large arena pool — which is never freed; the process exits instead — and appends the new object to the class’s global list. And then, from literally anywhere in the compiler, the code can say:
instance *I;
LOOP_OVER(I, instance) {
/* visits every instance in the entire program, in creation order */
}
That is the whole trick. LOOP_OVER expands to a for-loop that
walks the class’s global list from head to tail.
The right mental model
The right way to think about this isn’t “linked lists” — it’s
an in-memory relational database. Each classdef is a table.
CREATE is INSERT. LOOP_OVER is
SELECT *. The scale of the pattern in the current v10 source:
Nobody passes collections around. Nobody owns anything. Nobody frees anything. When the compiler wants to compile all rules, index all instances, or check every table, it just… loops over the table.
Why it’s a coherent choice
For this program, the design holds together. Inform is a one-shot batch compiler: it reads your source, builds a world model, emits code, and exits. Memory is reclaimed by process termination, so the question “who frees this?” never needs an answer. There is no long-running server to leak, no concurrency to guard.
And the payoff is real: the code reads almost declaratively. Large parts of the compiler are, in effect, “for every X satisfying Y, do Z” — which happens to be exactly how the Inform language itself thinks. The implementation style mirrors the language’s own worldview.
Why it resists a Rust port
The reason this came up in a porting discussion is that this pattern is precisely what Rust’s ownership model exists to forbid: every object is permanently aliased from a global registry, mutated freely from anywhere in the program, with no owner and no lifetime shorter than the process itself. None of that can be translated mechanically. Each of those 337 “tables” would have to be re-designed — arenas with index handles, or some other owned structure — one class at a time, with every access site reconsidered.
That isn’t a criticism of the original design. It’s simply from a different universe than the borrow checker — a universe where the process boundary is the garbage collector, and where making a hundred-thousand-line compiler read like prose mattered more than making its memory graph a tree.
Provenance, in the interest of transparency. This page was drafted
with the assistance of Claude, an AI tool, working directly from a checkout of the
ganelson/inform v10 source tree. The
classdef excerpt, file reference, and the counts above (337 / 348 /
1,156) come from searches run against that repository, not from the AI’s memory.
One caveat: the CREATE/LOOP_OVER macro machinery itself is
defined in inweb’s foundation module, which lives in a separate repository that
was not examined — the description of its behavior reflects Inform’s own
documentation of the pattern rather than a line-by-line reading of the macros. Posted
and edited by David Cornelson; any errors are his.