# 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 `.ez` documents to
Markdown) meant first decoding ATK's `raster` inset 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.c` carries 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_GetPartialBody` has essentially the signature of IMAP's partial
`FETCH BODY[]<offset.size>`. Both use a two-phase delete: mark, then
purge (`AMS_ATT_DELETED` and `MS_PurgeDeletedMessages`; `\Deleted` and
`EXPUNGE`). 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 `open` to `dbg_open` with 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's `fcntl.h`, the macro rewrote the C library's *own declaration*
of `open()` — producing a declaration of `dbg_open` with `open`'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 parses
`fcntl.h` itself 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 `FreeMessage` call.
- **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 *) &w` cast, where `w` was a plain `int`
local. On the 32-bit hosts this was written for, `int` and `long`
were both four bytes, so the cast changed nothing — a stylistic
choice, not a mistake. On this LP64 port `long` is 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-types` warnings, 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 does `unlink(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 3501 `n:*` 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 `=20` where 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 `.do` modules 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 with `nm -m` that 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
`regcomp` and `regexec` to 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 named `reg_comp` and `reg_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-in `regcomp`/`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 `-pe` typed-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 to
`int`) while the real function was `void`, 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-`.c`
disagreement 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:
1. **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 genuinely `long`-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.
2. **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.
3. **A sentinel value corrupted by zero-extension.** The value `-1` is
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.
4. **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.
5. **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 an
`int` were 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 plain `unsigned` value where the actual,
pointer-carrying implementation needed something wide enough to hold a
full address, silently truncating a `self` pointer 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`/
`calloc` are 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 `help` window 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.h` already in place before the piece that actually installs
`class.h` runs. 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.
- **`metamail` has never successfully run in this environment.** Invoking
it — directly, or indirectly through `messages`' MIME-attachment
handling — raises `SIGTTOU` inside the routine that spawns a
mailcap-entry viewer subprocess: ordinary 1980s terminal job-control
code performing an `ioctl()` 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 happened
- `porting-assessment.md` — the underlying bug-class analysis, strategy
decisions, and the full ANSI C conversion plan
- `roadmap.md` — current status, open issues, and near-term plans
- `version-comparison.md` — why 6.3.1 (the last C release) was chosen over
the later, unfinished C++ rewrite
- `quickstart.md` — how to build and run this on a Mac