Artifact cb928fd4f160fce4d4e322a34a308bc28df1db409df2607e4a8f2c7abfb367fd:
- File revival/doc/revival.md — part of check-in [f4180d46d2] at 2026-08-01 23:20:24 on branch andrew-6.4 — docs: M3 complete tree-wide (15 sessions/91 directories) in revival.md status; add m4-rollout-runbook.md — global-flip task breakdown, deliberately no pre-built batch map (user: wdc size: 61795)
AUIS Revival: What Was Done and Why
Last distilled: 2026-08-01. A narrative account of reviving Carnegie
Mellon's Andrew User Interface System (AUIS) on a modern Mac, for readers
who already know ATK and readers encountering it for the first time. For
the complete technical record this document summarizes, see
porting-changelog.md (chronological fix log), porting-assessment.md
(bug-class analysis), and roadmap.md (current status and open items).
What AUIS is
AUIS is the software behind ez — a word processor capable of embedding
spreadsheets, equations, drawings, and animations directly inside a
document — messages, a mail and bulletin-board client with the same rich
embedding, and a dozen smaller applications, all built on a shared toolkit
called ATK (the Andrew Toolkit). The version being revived, 6.3.1
(August 1994), is the last release CMU shipped as plain C before the
project moved to C++. (See version-comparison.md for the reasoning
behind reviving 6.3.1 rather than the later, never-finished C++ line.) The
target platform is macOS on Apple Silicon, using XQuartz to provide the X11
display server this software was originally written against.
For readers who haven't encountered it before, ATK is worth a moment's
context, because its central idea was genuinely ahead of its time. That
idea is the inset: a self-contained, interactive object — a spreadsheet
cell, an equation, a drawing, an animation, a footnote, a scripted
widget — that can be embedded inside a document and can, in turn, embed
further insets inside itself, recursively and without a fixed limit. A
single ez document can contain a spreadsheet nested inside a drawing
nested inside a footnote. Each inset knows how to read and write its own
portion of the file format, draw itself, and handle its own input,
independent of what it happens to be embedded in. General, recursive object
embedding across applications became a mainstream industry goal only later
in the 1990s — Microsoft's OLE and Apple's OpenDoc both pursued versions of
it, with OpenDoc discontinued in 1997 without wide adoption — and ATK had a
working, recursively general implementation of the idea years earlier,
built in portable C on top of a small, purpose-built object system (called,
simply, "Class") that is contemporary with Objective-C. The application
framework itself — windows, menus, dialogs, scrolling, printing — is shared
by every ATK-based program, so ez, messages, and help are less a set
of separate applications than different entry points onto the same
document-and-inset substrate. That shared substrate, and the "any object
can live inside any other object" model it enables, is ATK's most
distinctive and still-interesting contribution.
Nobody had compiled this code in roughly thirty years. Restoring it meant addressing two different kinds of decay at once: the environment changed around the code (new compilers, a new processor architecture, deprecated operating-system interfaces), and the code itself contained real defects that had simply never been exercised in three decades of use. The rest of this document covers both, along with the strategy adopted to manage them.
Modernizing
Not everything changed during the revival was about recovering old, dormant behavior. Some changes were required simply to make 1994 code function in a 2026 environment that behaves differently in specific, load-bearing ways — without them, the software does not run at all. Separately, a smaller amount of work was undertaken by choice, to bring one part of the software's appearance up to a standard users now expect, even though the original approach was still technically functional. Both count as modernization, but they carry different justifications and were undertaken for different reasons.
Required to run at all
- Deleting a character from the middle of a string was commonly done by copying the remainder a few bytes to the left, within the same buffer — technically undefined behavior by the letter of the C standard, but a pattern that produced correct results on essentially every C library for decades. Apple's runtime-hardened library now detects that overlapping copy and aborts the program outright. Fifteen call sites across nine files used this idiom, and it crashed three applications (a file browser, an outliner, and an HTML viewer) under ordinary use. Each site needed only a one-line fix — a function explicitly defined for overlapping ranges — but locating all fifteen required a deliberate, tree-wide search, and none of them were optional: without the fix, those applications do not start.
dlopen()/dlsym(), the standard POSIX dynamic-library interface, replaced AUIS's own hand-built, per-platform dynamic code loader — the component responsible for loading an inset's code on demand. The original loader had to understand the executable file format of every target platform individually and had no notion of the current one; the standard library call accomplishes the same task in roughly seventy lines of code rather than the original's five hundred, and is what actually loads insets in the revived system.- A compiler-leniency strategy, as the default response to the volume of errors modern, stricter compilers raise against 1990s C. Rather than rewriting the source wholesale, the build disables a small, specific set of modern strictness checks corresponding to conventions this code relied on, reserving actual rewriting for places that genuinely require it. Without this, the code does not compile at all under a current compiler. (Discussed further below.)
Bison and flex
The project's vendored bison fork hangs outright on current processor architecture, so the build now uses the system's installed bison instead; flex was already the system tool, but 1990s wrapper code around both generators still assumed specific behavior of the versions in use at the time. An early fix — passing bison's output filename explicitly, since modern bison's default naming convention had changed — flagged that risk directly, since the original code already carried its own accommodations for 1990s bison behavior. That prompted a closer look for other places where old assumptions about generator output might no longer hold, and turned up two:
- A shared code generator assumed every parser lookup table was a fixed storage width, matching 1994 bison's output. Modern bison narrows some tables to a smaller type when the data range allows it; reading a narrowed table through the old, fixed-width assumption merged adjacent entries into garbage — surfacing as a date parser that failed on every input, for reasons unrelated to dates.
- A hand-written reset routine poked an internal flex flag whose meaning had reversed between old and new flex — nonzero used to mean "reinitialize on next call," modern flex treats it as "already initialized." The first call in a process skipped its own setup and wrote through an unset pointer, crashing a mail-address parser on the first address it ever processed.
Adopted by choice: anti-aliased text
X11's original design placed font rendering on the server: the X server itself rasterized glyphs from bitmap font files and returned pre-rendered pixels on request, a scheme known as the core font protocol. This was the normal way to draw text throughout the 1990s, and it is what AUIS was built against — every text-rendering call in the codebase assumes it. Modern desktop environments instead render text on the client side, using libraries such as Xft and fontconfig, which support anti-aliasing, scalable outline fonts, and subpixel hinting — the smooth, high-resolution text users now consider normal.
The old, server-side bitmap path still works: XQuartz, the X server used here, still implements the core font protocol, so nothing required this change. It was made anyway, because 1990s bitmap fonts, while entirely serviceable on 1990s monitors, look noticeably coarse next to the anti-aliased text every other current application renders, and that difference was judged worth correcting. The revival adopted a hybrid: ordinary document text now renders through Xft, while the small set of custom CMU symbol and cursor glyphs (bullets, math marks, cursor shapes) that have no modern equivalent still render through the original bitmap path — replacing only the parts that had a clean modern substitute.
This was a comparatively small amount of new code, and the visual improvement was judged worth the effort — but, being new code rather than old code regaining exercise, it introduced its own new defects rather than exposing dormant ones. The calculator inset's display, once it received real interactive testing under the new rendering path, showed two: text was being "erased" by redrawing it in the background color, which exactly cancels only the fully opaque center of each anti-aliased character, so the partially transparent edge pixels accumulated into a visible ghost after repeated use; and, separately, the X display server sometimes wrote the correct pixels into a window's buffer without promptly making them visible, requiring an incidental redraw — a window focus change, for instance — before the correct text actually appeared. Both were resolved, but both took debugging effort disproportionate to the size of the feature, which is the general cost of this kind of optional modernization: low volume of new code, comparatively high difficulty per defect.
The project's own infrastructure was modernized in a smaller way as well: source control runs under Fossil, a self-contained modern system, in place of whatever distribution mechanism carried the source in the 1990s.
Finding ATK lessons in modern implementations
Not every observation from this project has been about repairing decay. Occasionally, working through thirty-year-old code turns up a design decision that still holds up — quietly validated by a standard that didn't exist yet when ATK was written.
Raster insets and PNG converged on the same bitmap encoding. Adding image support to
ez2md(the tool that converts.ezdocuments to Markdown) meant first decoding ATK'srasterinset format: a one-bit-per-pixel bitmap, packed eight pixels to a byte, most-significant bit first, one bit value meaning "black." PNG's one-bit grayscale mode — standardized in 1996, years after this raster format was written (rasterio.ccarries an IBM copyright of 1988) — packs pixels exactly the same way: MSB-first, one bit each, a bit value mapping straight to black or white. The only real work in the conversion was undoing ATK's own run-length encoding (a compact ASCII scheme that made sense when screen bitmaps had to move over modems and fit on small disks) back into raw packed bits, then flipping which bit value means black, since the two formats picked opposite conventions for that one detail. No resampling, no color-model translation, no coordinate reconciliation — two systems, designed for unrelated reasons the better part of a decade apart, landed on the same underlying representation for a scanline bitmap, because it's simply the obvious correct one.The Andrew Message System's 1988 client-server interface reads like a preview of the modern IMAP extension stack. AMS and IMAP were contemporaries — Crispin's first IMAP drafts and Borenstein's message server were designed in the same few years, for the same problem, at different institutions — and the convergences run deep. Both settled on durable per-message identifiers that survive reconnection (AMS's 18- character unique ids; IMAP's UIDVALIDITY plus UID). Both retrieve message bodies by offset with a bytes-remaining count — AMS's
MS_GetPartialBodyhas essentially the signature of IMAP's partialFETCH BODY[]<offset.size>. Both use a two-phase delete: mark, then purge (AMS_ATT_DELETEDandMS_PurgeDeletedMessages;\DeletedandEXPUNGE). Both maintain a per-user subscription list over a shared folder space. Both send fixed-size summary records in place of messages (AMS snapshots; IMAP's ENVELOPE/FLAGS/INTERNALDATE items). And the AMS spec's engineering doctrine — every call idempotent, a server that "can die at any moment, with the client automatically reconnecting and continuing its work without any loss of context" — is precisely the discipline that makes a modern IMAP client's reconnect logic workable. Where the parallels reach past core IMAP is the striking part: AMS computed reply-chain threading server-side in 1988 (the snapshot's chain field), which IMAP added as the THREAD extension years later; its "master update file" answering what changed since I last looked cheaply is CONDSTORE's job description; its 168 user-defined per-folder attributes are IMAP keywords. In one respect AMS remains ahead: the server computed each message's one-line caption once, for every client — a summary IMAP clients still each rebuild for themselves from ENVELOPE, four decades on. The revival's IMAP work leaned on this kinship directly: mapping the store's operations onto IMAP was mostly transcription, because the two designs had already agreed on what a mail store is.
Old bugs never found till now
Bringing 1994 code onto a 2026 machine did more than require accommodating a changed environment — it also surfaced defects that had existed in the source for decades without ever manifesting. Two changes in particular turned latent mistakes into visible ones: stricter runtime checks in Apple's C library, which refuses certain unsafe patterns outright rather than silently permitting them, and, later in the project, a build configuration that type-checks function calls the original toolchain never checked. Neither change introduced a defect; both exposed defects that had been present, and untriggered, since the code was written. A representative sample:
- A core interface had its arguments in the wrong order for roughly 35
years. The class-definition file for the figure-drawing inset declared
one method as
Build(action, view, ...); every implementation, and every call site, used(view, action, ...). This had no runtime effect for the entire life of the code, because nothing had ever cross-checked the declaration against actual usage — until the ANSI C conversion effort, described below, began doing exactly that. - A class of status message had never once displayed. A single call site in the HTML-editing view passed its arguments in the wrong order — message text and priority transposed — so that category of informational message had never appeared, since the code was first written.
A parser that never checked whether an embedded inset loaded.
ez's plain-text container read each inset via a success-or-failure code it never checked. When a figure's reader failed partway through — on a document with an attribute this older reader didn't recognize — the container kept parsing from wherever the failed read had abandoned the file: the figure's raw internal data appeared on screen as literal text, and nothing after it in the document rendered at all. Fixed by falling back to a generic reader that resynchronizes by counting nested begin/end markers instead of assuming every read succeeds.A leak-tracking macro that could poison the C library's own declarations. A debugging header renames
opentodbg_openwith a preprocessor macro, so that file-descriptor leaks can be traced through a wrapper. In source files that happened to include this header before the system'sfcntl.h, the macro rewrote the C library's own declaration ofopen()— producing a declaration ofdbg_openwithopen's variadic signature, which the wrapper's fixed three-argument definition does not have. For the code's whole prior life this was harmless twice over: K&R compilation had no prototypes to mismatch, and the calling conventions of the era passed fixed and variadic arguments identically anyway. On Apple Silicon they differ — variadic arguments travel on the stack, fixed arguments in registers — so caller and wrapper disagreed about where the file-permission argument was. Files were silently created with garbage permission bits, and code that had just written a temporary file found itself unable to reopen it, an "impossible" permission error that traced back, through the disassembly, to a macro and an include order. Fixed at the root: the debugging header now parsesfcntl.hitself before performing the rename, making the poisoning impossible in any include order.A bet on the direction of filesystem history, quietly lost. The message store names each message's body file after its unique id — an 18-character string over a base-64 alphabet in which uppercase and lowercase letters are distinct values. That scheme silently assumes filenames are case-sensitive. In 1988 this looked less like an assumption than an observation about progress: the case-insensitive filesystems were the older systems — TOPS-10, VMS, CP/M, DOS — and UNIX, the future, distinguished case. History went the other way. The filesystems that won the desktop — HFS+, then APFS, and NTFS as Windows uses it — are case-insensitive (case-preserving, which makes the trap quieter still: names display exactly as written, they just refuse to be distinct). On a Mac, two AMS ids differing only in letter case are different strings and the same file. For thirty-five years nothing noticed, because native ids derive from host and timestamp and differ in many character positions at once. The revival's IMAP mirror found it in an afternoon: it synthesized ids from dense sequential message numbers, adjacent ids collided as filenames every time the encoding walked the alphabet across a case pair, and a "new" message's body file turned out to already exist — containing a different message. The mirror's fix is an id alphabet with no case pairs; the store's native ids remain a documented, if astronomically unlikely, hazard on this platform.
A 35-year memory leak on the duplicate-message path. The message store's append routine reads an entire message into memory, then — if it discovers the message is already present — returns success without freeing it. Every other exit from that function frees the buffer; the duplicate path leaked the whole parsed message. Nobody ever saw it because duplicate appends were rare, one-at-a-time events in short-lived processes. The IMAP mirror made the path hot: a sync recovering from lost state re-appends thousands of already-present messages in one process, which would have transiently leaked approximately the whole mailbox. Found by code review during the mirror work, fixed with one
FreeMessagecall.A cast that used to be true. The font toolkit's bounding-box routine measured a string's width by calling its own sibling method through an explicit
(long *) &wcast, wherewwas a plainintlocal. On the 32-bit hosts this was written for,intandlongwere both four bytes, so the cast changed nothing — a stylistic choice, not a mistake. On this LP64 portlongis eight bytes, and the same cast became a real four-byte stack overflow on every call, silently corrupting whatever local variable happened to sit next to it in the frame — every time a figure-inset text label recomputed its size. It produced no crash, just gradually garbled label text, and it evaded the ANSI conversion's own compiler-warning-driven audit for a specific reason: that audit worked by grepping for-Wincompatible-pointer-typeswarnings, and an explicit cast is exactly what that warning exists to suppress. A census built entirely from compiler diagnostics could not, by construction, see a mismatch the programmer had already cast into silence; only reading the function by hand, then checking the original 1990s source to confirm the cast predated this project, found it.A blocking dialog that outlived its answer. Clicking a folder in the mail overview brings up a "What do you want to do with 'X'?" menu — implemented not as a native modal but as ordinary event dispatch, so background timers and callbacks kept running while the mouse waited over the choices. The function held onto the folder's name strings as raw pointers straight out of the directory cache; if anything freed that cache entry during the wait — another window's rename/remove notification, a periodic refresh — the strings went stale in place, and clicking "Subscribe" afterward walked the freed memory character-by- character and crashed. One sibling path in the very same function, reached by "see the messages," already defended against exactly this by duplicating the strings before doing anything that could invalidate them — a comment even says so — but the defense was never extended to subscribe/unsubscribe or "alter subscription status." For decades this raced against, at most, an occasional slow local change and rarely lost. The revival's IMAP mirror gave the directory cache far more frequent invalidation traffic than the original single-user model ever produced, turning a latent race from 1994 into a reproducible crash the first time someone tried to subscribe to INBOX. Fixed by giving the affected paths the same heap-copy discipline the "see the messages" path already had.
The cleanup that destroyed what it collided with. When the store writes a message's body file, it opens with
O_CREAT|O_EXCL— and if the open fails, the error path doesunlink(File)before returning. That unlink is meant to remove a partially-written file, which is the right cleanup everywhere later in the function, after the open has succeeded. On the open-failure path there is no partial file — and in the one failure mode where the target does exist,EEXIST, the file belongs to an earlier message. So for thirty-five years, any append that collided with an existing message file quietly deleted the existing message. The author suspected something: the line above the open reads/* Bogus -- what if not overwrite? */. It never fired because the store's duplicate check (which keys on the RFC-822 Message-ID header) almost always intercepted true duplicates before the filesystem could, and id collisions otherwise didn't happen. The IMAP mirror lined up the holes: a message with no Message-ID slipped the duplicate check, an RFC 3501n:*range quirk re-presented an already-mirrored message as new, and the mirror watched a message file vanish from a directory nothing was supposed to delete from. One deleted line fixed it — the rare bug you repair by removing code.A header parser that assumed local mail's line endings, forever. The mail reader's rule for finding where headers end and the body begins — read lines until a blank one — tested only for a bare LF (
'\n'). RFC 822's wire format specifies CRLF, and Andrew's own local mail delivery had evidently always normalized incoming mail to bare-LF line endings before this parser ever saw a message, because the check had gone unexercised against genuine CRLF for the code's whole life. Fetching raw messages directly from a live IMAP server, CRLF intact, was new. Against such a message the blank-line check never fires: the parser reads straight through the header/body boundary and keeps going, consuming the entire body as if it were more headers — each line displayed as an unrecognized header in tiny type, undecoded, with a colon-triggered bolding rule occasionally highlighting ordinary prose. The visible symptom (a screenful of raw header lines, then unreadable body text with a literal=20where a quoted-printable space belonged) took real mail, not local mail, to produce — and was found the same way most of this list was: a user looked at real output and described exactly what was wrong. Fixed by recognizing CRLF as well as LF at both header/body boundary checks, plus the same LF-only blindness in the quoted-printable soft-line- break decoder one call downstream of it.A dynamic-loading convention that only ever worked by coincidence. Most of the mail reader's dynamically-loaded
.domodules link with no library list of their own at all — every external symbol resolves at load time against whatever else the process happens to have already loaded (-undefined dynamic_lookup, which lets a missing library pass silently at build time and fail only at the first actual call). One module in particular had never once, in its whole life, called a symbol that wasn't already provided by something else loaded earlier in the process — so the gap in its own link line was invisible for as long as that held. The first new library call it ever made jumped straight to address zero. Fixed by giving that one module an explicit link line, and confirmed withnm -mthat the symbols it actually calls now resolve inside its own binary rather than by chance.A four-function type mismatch that outlived the language it was written for. The mail server's directory-info, new-message-count, subscription-entry, and changed-subscriptions calls were declared in the class layer as returning their results through pointers to a 64-bit-wide integer, while every one of their actual C implementations wrote through pointers to a plain 32-bit integer — a disagreement present in the very first commit that ever brought this source into version control, and certainly older than that. On the machines this code was written for, the two integer sizes were the same, so no data was ever lost no matter which one a caller believed; the mismatch was invisible by coincidence, not by correctness. On a modern 64-bit machine the two sizes differ, and coincidence stops covering for the bug — except this one stayed hidden even here, because the calling code had also, independently, been written expecting the narrower size, restoring the coincidence one layer up. It took a routine sweep fixing exactly the kind of caller/declaration disagreement this bug produces — widening a caller to agree with the class layer's declared type — to remove that second coincidence and let the original one through: a folder's subscription-status line began reporting a nonsensical negative count instead of a real one, the first time in the software's history it had ever actually done what its own interface claimed it did. Corrected by tracing every affected function to its real C body and matching the class layer to that, not the other way around — the class declaration, not the implementation, was the thirty-year-old mistake.
A misspelled function call that silently linked to the wrong library for thirty-five years. Three mail-filtering primitives — regular- expression search and decomposition operations available to the mail system's rule-based filtering language — called functions named
regcompandregexecto compile and run a pattern. No function by either name has ever existed anywhere in this codebase; the regular- expression engine actually built and shipped with the software, a few directories over, is namedreg_compandreg_exec, with an underscore neither call site had. Pre-standard C never checked that a called function actually existed before compiling the call, so the compiler raised no objection, and the linker simply resolved the misspelled names against whatever else provided them — in this case, the operating system's own built-inregcomp/regexec, a wholly different regular-expression implementation expecting a different kind of first argument than the plain string these call sites actually pass. Every call, for the software's entire life, therefore invoked the wrong function with the wrong argument shape — a defect invisible to any compiler or linker, on this platform or any other, because both misspelled names happened to already mean something. It came to light not through an automated check (nothing about it would trip one) but during a close reading of the file while fixing unrelated compile errors as part of the ANSI C conversion effort. Corrected to call the codebase's own, correctly named and typed,reg_comp/reg_exec.A misspelled preprocessor guard that quietly deleted a header's typed half for over thirty years. A menu library's header offered two versions of its own function declarations, selected by
#ifdef: a fully typed set for a standards-conforming compiler, and an older, untyped fallback set for one that predates function prototypes. The guard tested_STDC_— one underscore short of__STDC__, the name every C compiler that defines this macro at all has actually defined since the 1989 standard. No compiler, then or since, has ever defined the misspelled name, so the typed half of the header was dead on arrival: every build silently took the untyped fallback, and one function the fallback branch omitted entirely went undeclared wherever a caller didn't supply its own local declaration. It surfaced only when the ANSI C conversion effort's own-petyped-prototype mechanism, applied to a neighboring directory, made the header's guard load-bearing for the first time — every caller that had been quietly relying on the fallback branch's absence of type-checking now needed the guard to actually pick the typed branch. Flipping the fallback branch on for real then exposed a second, independent latent mistake in that same never-before-compiled typed branch: one declaration's return type had been omitted (silently defaulting toint) while the real function wasvoid, a mismatch nothing had ever checked because the branch had never been live long enough to check it against anything. Corrected both: the guard now reads__STDC__, and the return type now matches the definition.A stray dereference that only became visible once a call's argument types were finally checked. A function managing a window's color table took a pointer to a color-table structure and, in one of its three call sites, passed not the pointer itself but the structure it pointed to — dereferencing it — to two operations that both expect the pointer. Nothing caught this for the entirety of the program's life: the era's compiler performed no argument-type checking across these particular calls at all, so a whole structure handed to a function expecting a pointer to one was simply accepted and misinterpreted. The mistake surfaced only once these calls were finally typed as part of the ANSI C conversion effort, and only then because something forced the file to actually recompile — for over two weeks after the typed calling convention went into effect tree-wide, this file's own object code had never been rebuilt, so the now-real type mismatch had nowhere to raise an error until this session's work finally did. Confirmed against a neighboring, correct call in the very same function, which passed the pointer directly with no dereference — the same argument, spelled two different ways four lines apart, only one of them right. Corrected by removing the stray dereference at both incorrect call sites; the third call in the function, structurally different (a pointer to a pointer, correctly dereferenced once), was already right and left untouched.
Six copy/paste typos in class interface files, each substituting a plausible-looking wrong type name for the right one. A class's interface file declares, for its own constructor and destructor methods, which structure type the object being built or torn down actually is. In six places across two unrelated subsystems (form widgets and a diagram-editing framework), that declared type was wrong — not garbled, but a real, existing, different type: a sibling class one character away in the alphabet, the class's own parent, the short internal filename a class is stored under rather than the class's real name, and in one case a type that does not exist anywhere in the codebase at all. Every implementation was already correct — these were declaration-only mistakes, and the declared type for a constructor happens to be pure documentation to the compiler that built this software originally, checked against nothing, so a wrong entry there had no way to ever be caught. One of the six was not a constructor or destructor but an ordinary method used throughout its own file — its wrong declared type meant every one of that method's own field accesses was, technically, accessing the wrong structure's memory layout by name, silently correct only because the two structures happened to be laid out compatibly by coincidence. Found by systematically checking every such declaration in two directories against the real class each belonged to, once the ANSI C conversion effort's own type-checking made the first of the six impossible to ignore. Corrected all six to name the class they actually belong to.
A code-generation tool that silently contradicted its own generated code, for one particular kind of destructor — invisible until its own typed-declaration option was actually switched on for the first time. This software's class-generation tool automatically writes, for every class, both the declaration of that class's cleanup method and the internal code that calls it. For one specific method name, the tool's two halves disagreed: the code it generated to call the method always passed two pieces of information, but the declaration it generated for that method — whenever a class's own interface didn't spell out the second one explicitly — only promised one. Nothing ever caught this, because the tool's optional feature for emitting these fully spelled-out declarations was essentially never turned on for real, anywhere, until the current compiler-modernization effort began switching it on one part of the software at a time, decades after the tool itself was written. The first time it was turned on for a class using the plain, undecorated form of this declaration, the tool's own generated file contradicted itself and failed to compile. A related complication made the obvious fix wrong: a handful of classes legitimately override this same method with a different kind of return value than the rest, and a fix assuming one universal shape for it would have broken exactly those classes, which had been working correctly the whole time. Corrected by making the tool recognize only the specific shape that was actually broken, leaving every other class's generated code exactly as it already was — verified by regenerating every affected class's generated file and confirming it came out byte-for-byte identical to before, except for the one shape being fixed.
A destructor that never woke up, twice. Two unrelated classes in two unrelated subsystems — one a diagram-editing element, the other part of the text-content machinery — each carry a second cleanup method alongside their real one, its name misspelled by one transposed pair of letters: "Finialize" for "Finalize." Both misspelled versions have empty bodies and were never called, in either subsystem, in the decades since they were written — the class machinery dispatches destructors by exact name, so a misspelled one is simply invisible to it, never wired to anything, never missed. Unlike every entry above, this one has no "corrected by": there is nothing to fix, because nothing was ever broken — the real, correctly-spelled destructor in each class already does the actual cleanup work and always has. It's recorded here anyway because it's the same shape of mistake as the six copy/paste typos above — a plausible-looking wrong spelling that the era's tools had no way to ever flag — just caught this time before it could do any damage, by the same systematic method-by-method reading that the ANSI C conversion effort required everywhere else.
An interface that never mentioned the one argument its own implementation always needed. A tree-widget class declares two measurement methods — one for a subtree's width, one for its height — and describes both, in the file that's supposed to be their authoritative interface, as taking no arguments beyond the object itself. Every real implementation of both methods, from the day they were written, took a second argument: which node in the tree to measure. Nothing before this project ever compared the declared interface against the working code, so a function that could not possibly have worked with the interface as written ran, correctly, for decades — because nothing ever called it through that interface in the first place. Neither method has a single caller anywhere in the source tree; whatever originally needed a subtree's dimensions either used another path or was never finished. Corrected by adding the always-present, never-declared second argument to the interface, matching the implementation that was right all along.
A view-scrolling method whose interface named the wrong structure for its own parameter, from the day it was written. A text view's interface file declares a method that repositions one on-screen line during scrolling, taking a pointer to the line being moved. The declared type was the structure used elsewhere in the same interface for a position within a document — but the method's own implementation, and every one of its seven call sites, always passed a pointer to an unrelated structure: the type used for an on-screen line's own layout bookkeeping (its height, character count, screen position). The two types share no relationship; a correct call compiled and ran fine under K&R only because pre-standard C never checked a call's arguments against the interface it declared. Every real caller agreed with the implementation and always had — only the interface was wrong, invisible until the ANSI C conversion effort's typed-prototype mechanism finally checked it. Corrected by changing the interface's declared type to match the implementation and every caller.
A helper function called with an argument it never had, for the entirety of its life. A text view's internal helper for finalizing a selection takes exactly two arguments: the view itself and the selected length. Every one of the five places in the same file that called it, for as long as the file has existed, passed a third — a true/false flag left over from some earlier shape of the function that its actual body has never referenced. Pre-standard C's calling convention accepted extra arguments silently, so the mismatch cost nothing: the stray value landed nowhere the function ever looked, and the two real parameters arrived exactly where expected. It became a compile error, not a runtime mystery, only once the ANSI C conversion effort gave the function a fixed, checked argument count — the same conversion that exposes an interface's wrong argument type elsewhere in this list, here exposing a caller passing the wrong argument count instead. Corrected by dropping the stray argument at all five call sites, matching the function's own long-unchanging, correct behavior.
Three more copy/paste type typos, found later in a fourth subsystem. The same mistake as the six above, this time in the mail reader's own view classes: the ones that display a message body, a mail object, and a decoded 822-format body each declare, in their own interface file, a constructor and destructor whose object parameter is typed as a different class entirely. Two of the three copied the exact same wrong type from an unrelated sibling class declared earlier in the same file; the third named two different wrong types for its two methods, neither one its own. Every real implementation, as always, used the correct type and always had. The constructor half of all three stayed pure documentation even after this project's type-checking arrived, for the same reason as before — the class-generation tool supplies the real type itself regardless of what a constructor's interface claims. The destructor half was more exposed: for one of the three it stayed a silent disagreement, because the file containing the real destructor never happens to look at the file containing the wrong declaration in the same compile; for the other two, declaration and destructor share a file, the two disagreed inside a single compile, and the build failed until this was fixed. Corrected by retyping all three declarations to the class they actually belong to.
A folder-tree class whose interface file redundantly named an argument its own code generator was already going to supply — wrong in two different ways for two of its three startup/teardown methods. The class-generation tool automatically prefixes every startup and teardown method's generated declaration with a hidden first argument — the class itself — whether or not the interface file spells it out by name; every other class in the same directory leaves it out, as the tool expects. This one class's interface explicitly restated that argument anyway, for two of its three lifecycle methods, doubling it in the generated declaration each time: once for the plain, once-per-class initializer (declared with one named argument where the convention — and the working code — wanted none), and once for the per-object destructor (declared with its own restated first argument plus the required second one, for three total against the real function's two). A third restatement, on the one lifecycle method the tool always re-types from scratch regardless of what the interface says, looked like the identical mistake but never actually mattered. Neither of the two real mistakes was ever caught by the original compiler, which built a call from whatever the interface said and never checked it against the function actually being called. The destructor half surfaced first and most visibly: once this project's typed-header generation finally emitted this particular class's declarations, the generated file contradicted itself internally — its own generated call to the destructor, four lines above, still used the correct two-argument form. Corrected by removing the redundant restated argument from both declarations, restoring the interface every other class in the directory already followed — the same species of code-generator self-contradiction as the destructor described earlier in this list, just caught here for a second time, and this time affecting the class's constructor too.
A drawing library's abstract base class silently dropped an argument every one of its real subclasses used. Five related methods — highlighting, normalizing, exposing, hiding, and printing a figure's selection points — are declared by the drawing library's interface file with two arguments: the figure and the pane it's drawn in. The base class's own versions, meant only as placeholders for figure types that don't override them, took just the figure, silently dropping the pane. Every one of the roughly fifteen real figure types in the library — arcs, rectangles, polygons, arrows, and the rest — override all five and correctly take both arguments; only the never-overridden base placeholders were short. Pre-standard C's calling convention let this go unnoticed for the same reason as the helper function above: a call always supplied both arguments, and the placeholder's body, which does nothing but return a fixed failure code, never looked for the one it lacked. Corrected by giving all five placeholder definitions the same two arguments as the interface and every real override already agreed on.
A status-message variant whose interface never matched the code it was calling, and nothing ever called either one. The same drawing library's status-line class declares two message-issuing methods — one for a raw string, several more for typed variants (a figure, an image, a stream, a pane). The raw-string pair's interface entry took a single string argument, copied from a differently-named sibling method just above it in the same file. Its real implementation instead took two numeric codes and built the string internally, the same shape every one of the typed variants below it uses — not the string-based one it was declared to match. Neither name, under either signature, was ever called anywhere in the source tree: a rare case in this project where a
.ch-vs-.cdisagreement isn't a live bug at all, just two halves of a method that was apparently redesigned once, in code, and never updated in its own interface file. Corrected by retyping the interface to match the real, working implementation, the only side with any evidence of intent behind it.Two more copy/paste type typos, in a fifth subsystem. The same mistake as the six, then three, documented earlier in this list — this time in the drawing library's arrow and polyline figure classes, each declaring its own one-time setup method with its parent class's type instead of its own, both apparently copied from the same line in a shared ancestor file. Both real implementations used the correct type, and each reads a field that only its own class has — which is what confirmed the interface files were wrong rather than the code: neither placeholder type could have compiled against that field at all. Corrected by retyping both declarations to the class they actually belong to.
An HTML-rendering method's interface declared a formatting record as a plain string, and it went unnoticed for decades because both are ordinary pointers of the same size. The method that opens and closes a nested markup region (blockquotes, lists) took its second argument as a bare string in its interface file, but the real implementation and every one of its four call sites always passed a style/formatting record instead — direct field access, calls that only make sense for that record type. Nothing ever caught the mismatch because the interface only checks argument counts, not types, and both a string pointer and a record pointer are the same width — so the wrong-typed interface compiled, linked, and ran correctly by accident for as long as the code has existed. Corrected by retyping the interface to match the record every real caller and the implementation itself already agreed on.
None of these are new mistakes. Each was introduced once, decades ago, and never triggered — because the exercising code path was never run, because nothing had checked a declared interface against its actual usage, or because an earlier C library was more permissive. Restoring old software to working order on current tools amounts, in part, to finally running a test suite that nobody knew existed.
Word size issues
The largest, most systemic category of defect came from a single
architectural fact: this code was written when a C int and a C long
were the same size — 32 bits, on the machines of the early 1990s. On
today's 64-bit Apple Silicon, long is 64 bits while int remains 32.
Code that quietly assumed the two were interchangeable — a reasonable
assumption for decades — now loses or corrupts data at every boundary
where the two are confused.
This is most acute in AUIS's object system, which implements polymorphism (one class of object overriding a method defined by another) in plain C, using function pointers stored in a dispatch table. Historically, every method call went through generated code that cast that function pointer to a completely generic, typeless signature — meaning the compiler, seeing no argument types at the call site, could not insert the instructions needed to correctly widen a 32-bit value to 64 bits, or to preserve its sign. Five distinct, recurring failure patterns followed from this one root cause, each responsible for real, visible bugs during the revival:
- A function returning a pointer — or any other 64-bit value — called
with no declaration in scope. Pre-standard C assumes an undeclared
function returns a plain 32-bit
int. If the function actually returns something wider, the upper half is silently discarded — typically producing a crash the moment a truncated pointer is used, or a silently wrong number when the return is an ordinary integer instead. This was the single most common defect in the codebase: more than twenty separate sites, all the same shape, once the pattern was recognized in one core function used to locate files on disk. The later, tree-wide sweep for exactly this class of gap (M2, below) found the same shape recurring by the hundreds across the rest of the tree — including several genuinelylong-returning (not pointer-returning) library functions, such as a mail client's directory-lookup and session-initialization calls, each confirmed against its real definition, independently, before any declaration was written. - More arguments than the processor's registers hold. Apple Silicon passes the first eight integer/pointer arguments in registers and spills the remainder to the stack — but only if the compiler knows, at the call site, that there are more than eight. Through the generic, typeless dispatch above, it did not, and the ninth and later arguments were silently dropped.
- A sentinel value corrupted by zero-extension. The value
-1is commonly used as a special "unset" or "apply to everything" marker. Passed as a plain 32-bit constant through the same typeless dispatch, it is not sign-extended as it should be — the receiving 64-bit field ends up holding 4,294,967,295 rather than "negative one," and any code checking "is this negative?" or "does this equal -1?" silently takes the wrong branch. This is what made a help-browser list open scrolled to the bottom instead of the top: the "no position set yet" marker for a freshly opened list was corrupted into a large number crossing one of these dispatch calls, and the scroll position duly followed it. - A 64-bit number read with a 32-bit text-parsing format. Numbers
stored as text within document files were read back with
%d(32-bit) into variables that were actually 64 bits wide, leaving the upper half as whatever value happened to occupy the stack. The clearest instance: a drawing's on-page position was parsed this way and came out corrupted by exactly 4,294,967,296 — the figure was being drawn correctly, just roughly four billion pixels off-screen. - A 64-bit parameter fed a corrupted 32-bit argument — the same zero-extension mechanism as pattern 3, but for ordinary values rather than named sentinels, tripped by negative numbers used for scroll positions, indentation, and margins.
Two related but mechanically distinct defects showed up alongside these five, corrupting data across a similar boundary disagreement without being, strictly, width problems:
- A signedness mismatch rather than a width mismatch. One inset type's interface declared a method as returning a plain (signed) character where the implementation actually returned an unsigned one. At low compiler-optimization levels the discrepancy had no effect; at normal optimization, the compiler exploited the declared (incorrect) signedness and generated a comparison that could never succeed, so a "no line width configured" sentinel was silently read as "maximum width," and an entire figure was painted over in solid black. This belongs to the same family as the width bugs above — a value crossing a boundary where the two sides disagree about how to interpret its bits — differing only in that the disagreement is over sign rather than size.
- A pointer stored in a field only wide enough for a plain number. Two
font-style fields in the note and troff-text insets had, since the
1990s, been mistakenly assigned the literal text
"fontdesc_Plain"rather than the symbolic constant of the same name — a copy error that was harmless on the original 32-bit machines, where a pointer and anintwere the same size, and became live pointer corruption only once the same code ran on a 64-bit processor. A related instance: a widget class declared a parameter as a plainunsignedvalue where the actual, pointer-carrying implementation needed something wide enough to hold a full address, silently truncating aselfpointer on every control-panel widget built from that class.
A mechanically unrelated defect, described above in "Modernizing," produced a very similar-looking symptom: a code generator's own choice of table storage width — not the processor's register width — changed underneath old code. It does not belong to this family mechanically, but the underlying lesson is the same: 1990s code that hard-assumed a fixed width is fragile against any later link in the toolchain — compiler, processor, or code generator — making a different, reasonable-at-the-time choice.
Migrating to ANSI C
The oldest parts of this codebase predate the C language standard itself — functions are defined in the pre-standard "K&R" style, with no declared argument types anywhere. An early attempt to correct this wholesale, by running an automated tool across the entire source tree in one pass, made matters considerably worse: the tool's pattern-matching approach to inferring argument types mishandled enough edge cases that a single mass conversion took the build from roughly zero errors to over two thousand, with no way to distinguish genuine bugs from the tool's own mistakes. That attempt was reverted in full.
The approach that ultimately worked rests on one observation: this codebase's class-definition files already describe every method's real argument types in full, for an unrelated reason — they are the input to the code generator that builds the object system's dispatch tables — and that generator had simply been discarding the type information once it parsed it. Teaching the generator to emit what it already knows turns the compiler itself into the auditor: any place where a real implementation disagrees with its own declared interface becomes an immediate, located compile error, rather than a bug waiting to be found by accident at runtime.
The resulting plan runs in four stages:
- M1 — teach the class-definition compiler to emit typed code.
Completed tree-wide in July 2026. This alone eliminated the
sentinel-corruption and too-many-arguments bug families by construction,
and caught dozens of decades-old interface mistakes — including the
backwards
Build()arguments described above — simply by enabling real type checking at every method call site. The rollout itself proceeded one subdirectory at a time, verified by a clean full rebuild and a runtime check after every step — the discipline the failed mass-attempt had lacked. - M2 — sweep for missing function declarations throughout the rest of
the tree, not just class methods, closing the undeclared-function
pointer-truncation pattern for good. The sweep surfaced two
complications of its own along the way:
malloc/free/realloc/callocare compiler built-ins, so a call to one with no declaration in scope anywhere never triggers the diagnostic being swept for — invisible to the compiler-driven pass, closed only by a second, deliberate search across every file regardless of how clean a directory's compile already looked; and one class's method-dispatch macro, an optimization that calls its internal implementation directly instead of through the usual indirect table, turned out to declare that implementation inside a header guard whose actual purpose was unrelated — avoiding duplicate method tables across a multi-file loadable module — which had the side effect of hiding a real, correctly-typed declaration from the one caller that needed it, harmlessly, for over thirty years. - M3 — convert the function definitions themselves from pre-standard to standard C, one subsystem at a time, using a purpose-built tool that looks up each function's real signature from the class-definition files rather than inferring it — the safer approach the original mass- conversion attempt should have taken. Completed tree-wide August 2026, 15 sessions across all 91 active directories. Consistent with M1's experience, real type/arity checking surfaced numerous decades-old interface bugs along the way — several are recounted below — and the conversion tool itself needed two rounds of fixes for K&R declaration shapes its own parser had missed.
- M4 — enable full compiler strictness tree-wide, once every subsystem has been converted, which closes the remaining scanf-format-code bug family as a side effect. Not yet started.
As of this writing, M1, M2, and M3 are all complete across the entire active codebase; M4 has not yet started.
Where things stand today
ez (the word processor), help, and messages (mail, running against a
local mailbox rather than the original 1990s shared-filesystem delivery
system) all run and render correctly, including most embedded inset types:
text, equations, tables, drawings, animations, footnotes, spreadsheets, and
more. The class-definition typing project (M1, above) is complete across
the entire active tree.
One deliberate scope decision from early in the project is worth noting: the Console (terminal-emulator) subsystem is not part of this build. Console's interprocess-communication layer would need a rewrite of its own, and the project set that aside rather than take it on alongside everything else. A small side effect: two icon fonts used by an unrelated animation inset happen to live inside Console's own build directory, so they go silently unbuilt too, unless Console — or just its font directory — is built on its own.
A handful of smaller, known issues remain open, described below in "Open
issues" and tracked in full in roadmap.md.
Open issues
A few problems remain unresolved, either because the root cause itself isn't confirmed or because a workaround is in place without a real upstream fix.
- A live deadlock, cause unconfirmed. While chasing an unrelated
defect, a
helpwindow occasionally froze permanently. A debugger traced this to the process's single thread trying to reacquire a lock on the X display connection it already held indirectly, triggered by a keyboard-layout-change notification arriving mid-operation. Whether this is a genuine defect in how the toolkit calls back into the X client library, or a quirk specific to this environment's X server, hasn't been determined. - A completely empty build directory fails on the very first build.
The class-system directory everything else depends on builds its own
pieces in a fixed order, and that order builds one piece needing
class.halready in place before the piece that actually installsclass.hruns. Any build directory that already has a copy sitting around from an earlier build never hits this; only a genuinely empty one does. The current fix is a manual step — copying the header into place before the first build — and a build-ordering problem is the leading explanation, though not a settled one: this step wasn't always necessary earlier in the project, and it isn't yet certain whether the analysis has found the real root cause or only a reliable workaround. metamailhas never successfully run in this environment. Invoking it — directly, or indirectly throughmessages' MIME-attachment handling — raisesSIGTTOUinside the routine that spawns a mailcap-entry viewer subprocess: ordinary 1980s terminal job-control code performing anioctl()a foreground process doesn't expect. Confirmed unrelated to the ANSI C conversion work happening alongside it — the crash lives entirely in terminal/process-group handling, untouched by any declaration or typing fix — and predates this project; nobody has reported metamail working here at any point. Root cause identified; not yet fixed.
Further reading
porting-changelog.md— the complete, dated history of every fix, in the order it happenedporting-assessment.md— the underlying bug-class analysis, strategy decisions, and the full ANSI C conversion planroadmap.md— current status, open issues, and near-term plansversion-comparison.md— why 6.3.1 (the last C release) was chosen over the later, unfinished C++ rewritequickstart.md— how to build and run this on a Mac