AUIS revival

revival.md at tip
Login

File revival/doc/revival.md from the latest check-in


AUIS Revival: What Was Done and Why

Last updated: 2026-08-08

This document is 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:

Doc Description
roadmap.md Current status, active projects, and open issues
porting-changelog.md Chronological log of every fix
porting-assessment.md Bug-class analysis and the full ANSI C conversion plan
version-comparison.md Why 6.3.1 (the last C release) was chosen over the later, unfinished C++ rewrite
quickstart.md Build and run instructions

What AUIS is

AUIS is the latest name for the user-interface toolkit work that came out of the IBM-funded Information Technology Center at Carnegie Mellon University in the 1980s and 1990s. It was originally the "Andrew Base Editor," then a second version, "Base Editor II" (BE2 for short), then the "Andrew Toolkit" (ATK for short). As the project began to be shown outside CMU and adopted by others, the more ambitious name "Andrew User Interface System" (AUIS) was adopted.

The applications from AUIS of primary interest now are ez and messages. ez is a word processor capable of embedding spreadsheets, equations, drawings, and animations directly inside a document. messages is a mail and bulletin-board client with the same rich embedding. In addition to ez and messages, AUIS is the basis for a dozen smaller applications, including bush, a filesystem browser.

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.

ATK's central idea is the inset: a self-contained, interactive object — a spreadsheet cell, an equation, a drawing, an animation, a footnote, a scripted widget — embedded inside a document, able to 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 reads and writes its own portion of the file format, draws itself, and handles its own input, independent of what it's embedded in. The model predates the industry's later attempts at the same idea — Microsoft's OLE and Apple's OpenDoc, both later in the 1990s, OpenDoc discontinued in 1997 — built in portable C on a small object system called "Class," contemporary with Objective-C. The application framework — windows, menus, dialogs, scrolling, printing — is shared across every ATK program, so ez, messages, and help are entry points onto the same document-and-inset substrate rather than separate applications.

This code hadn't been compiled in roughly thirty years. Restoring it required addressing two things: the environment around the code had changed (new compilers, a new processor architecture, deprecated operating-system interfaces), and the code itself contained real defects that had never been exercised in three decades of use. The rest of this document covers both, along with the strategy used to manage them.

Modernizing

Some of what changed was required just to make 1994 code run in a 2026 environment. A smaller amount was optional: bringing part of the software's appearance up to a standard users now expect, even though the original approach still worked. Both count as modernization, for different reasons.

Required to run at all

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:

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 is about a defect. A few times, working through thirty-year-old code turned up a design decision that still holds up, validated later by a standard that didn't exist yet when ATK was written.

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 sample of the most illustrative follows. The fuller catalog — several dozen more of the same species — is not yet consolidated in one place: some are in porting-assessment.md's bug-class writeups, some only in porting-changelog.md's dated log or in claude-history/roadmap-old.md's retired detail, and a few live only in fossil's commit history. Bringing them all into porting-assessment.md as the single technical reference is a natural next step, not yet done:

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. There was no test suite to find them: what did was a stricter compiler, real use, and weeks of manual, heuristic-driven work — grepping for a suspect pattern, rebuilding with make -k to surface every hit at once, then reading each one by hand. This development and repeated toil was done by Claude Code, primarily with the Sonnet model managing delegated sessions following an evolving sonnet-playbook.md, with the Fable model occasionally used for reviews and for difficult problems.

One exception, caught the same day it was made. A drawing editor's on-disk format for a figure's "mode" attributes packs three single- character flags onto one line; the code that writes that line built all three characters but only ever wrote two, silently dropping the third — "halo" — on every save, a plain thirty-year-old bug fixed like the rest above. Fixing it exposed a second, previously dormant bug in the very same function: the three flag buffers were declared static char foo[2] = "?", scratch space seeded with a literal ? and only ever overwritten when their flag was on, never reset when it was off. Because halo had never actually appeared in the output before, its stale ? sentinel was invisible; the moment the fix started printing it, every figure with the halo flag off — effectively all of them — got a literal ? appended to its Mode line, desynchronizing the reader for the rest of the document. Caught within hours by manual round-trip testing (open, edit, save, reopen), not by any compiler warning — nothing about a string that compiles cleanly but holds the wrong content trips a diagnostic. Fixed by dropping static and letting the buffers default to empty, which is arguably a second correction to the same original mistake: true K&R C never allowed a stack variable to carry an initializer at all, so the 1988 author most likely reached for static only to make the = "?" syntax legal, not because persistence across calls was ever wanted.

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.

Three related but mechanically distinct defects showed up alongside these five, corrupting data across a similar boundary disagreement without being, strictly, width problems:

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:

As of this writing, M1 through M4 are complete across the entire active codebase, each having turned up several of the decades-old defects described above. A follow-on tree-wide census closed the one remaining blind spot the four milestones couldn't see on their own — stale argument-less forward declarations shadowing correctly-typed real definitions — and is described in its own entry above.

Where things stand today

ez (the word processor), help, and messages (mail, running against a local mailbox and, now, a live IMAP mirror, 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. bush, org, chart, and layout all run cleanly too. The ANSI C conversion described above (M1 through M4) is complete across the entire active tree — every method call and function definition in it is now compiler-checked. A live, itemized table of what's fully working versus still rough is in roadmap.md, rather than repeated here.

One deliberate scope decision from early in the project: 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.

Further reading