How do you test whether a tool really understands Classic Mac code — or merely C? You hand it a program that compiles, looks like a hundred others from that era, and still does not work. The interesting bugs are not the ones the compiler finds.

The source below was written as a test case for the Code Assistant in ClaudeBridge 2.0. It contains five deliberate bugs, graded from “fails immediately” to “crashes eventually, under load”.

Deutsche Fassung: Fünf Fehler in Think C

The test case ships as BuggyWindow.c in the source repository: github.com/LoetLuemmel/ClaudeBridge

The test case

/* BuggyWindow.c -- Think C 7.0 / System 7.5
 *
 * Oeffnet ein Fenster, legt einen Textpuffer in einem Handle ab
 * und zeichnet ihn. Kompiliert -- laeuft aber nicht sauber.
 */

#include <Types.h>
#include <QuickDraw.h>
#include <Fonts.h>
#include <Windows.h>
#include <Menus.h>
#include <TextEdit.h>
#include <Dialogs.h>
#include <Events.h>
#include <Memory.h>

#define kBufSize 256

void InitToolbox(void);
void DrawContent(WindowPtr win, Handle textH);

void InitToolbox(void)
{
    InitGraf(&thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(0L);
}

void DrawContent(WindowPtr win, Handle textH)
{
    char *p;

    // Master-Pointer holen und Text ausgeben
    p = *textH;

    MoveTo(20, 40);
    DrawString("Puffer-Inhalt:");

    MoveTo(20, 60);
    DrawText(p, 0, 15);
}

void main(void)
{
    WindowPtr win;
    Rect      bounds;
    Handle    textH;

    InitToolbox();

    SetRect(&bounds, 50, 50, 350, 200);
    win = NewWindow(0L, &bounds, "\pTestfenster", true,
                    documentProc, (WindowPtr)-1L, false, 0L);

    textH = NewHandle((long)kBufSize);
    BlockMove("Hallo vom 68000", *textH, 15L);

    DrawContent(win, textH);

    while (!Button())
        ;

    DisposeWindow(win);
    DisposHandle(textH);
}

If you want to play along, stop reading here.

Bug 1 — The line comment

// Master-Pointer holen und Text ausgeben

THINK C 7.0 knows only /* */. Line comments with // arrived with C99, six years after this compiler version. The build stops immediately.

The easiest bug — and still one modern tools like to skip, because // is so ordinary today that nobody notices it any more.

Bug 2 — C string instead of Pascal string

DrawString("Puffer-Inhalt:");

The Mac Toolbox expects Pascal strings: the first byte is the length, the characters follow. THINK C writes them as "\pText".

What stands here is a C string. That is particularly nasty because it compiles — the pointer is type-compatible. At run time DrawString reads the first byte as a length: 'P' is 0x50, so 80. The program draws 80 bytes from that address — the rest of the string, then whatever happens to sit behind it in memory.

No crash, no warning. Just garbage on screen.

The correct form:

DrawString("\pPuffer-Inhalt:");

Bug 3 — No SetPort

NewWindow creates a window and its GrafPort — but it does not make that port current. Every QuickDraw call keeps drawing into whichever port was set before.

So the window appears and stays empty. The text lands somewhere else, usually invisibly behind it.

SetPort(win);

One line, without which nothing becomes visible. For anyone who worked on this platform it is muscle memory. For a tool without platform knowledge it is invisible, because the code is formally perfectly correct.

Bug 4 — The dereferenced handle

This is the real benchmark.

p = *textH;          /* take the master pointer */

MoveTo(20, 40);
DrawString("...");   /* may compact the heap */

MoveTo(20, 60);
DrawText(p, 0, 15);  /* p may now point at nothing */

Classic Mac OS manages memory through handles — pointers to pointers. The reason is fragmentation: the Memory Manager is allowed to relocate blocks in the heap to close gaps, updating the master pointer as it goes. Copy that master pointer into a local variable first, and afterwards you are holding a stale address.

Almost every Toolbox call is allowed to compact the heap. DrawString is one of them.

What makes this bug vicious is that it almost always works. With plenty of free memory nothing moves and the program runs for months. Then the heap gets tight, a block relocates, and it crashes somewhere that has nothing to do with the cause.

The fix is to nail the block down:

HLock(textH);
p = *textH;
/* ... draw ... */
HUnlock(textH);

The same bug sits one level up in BlockMove("Hallo vom 68000", *textH, 15L) — dereferenced there too, without locking.

To someone who knows only modern C, p = *textH; is a harmless assignment. It is the most classic of all Classic Mac traps.

Bug 5 — Unchecked allocation

textH = NewHandle((long)kBufSize);
BlockMove("Hallo vom 68000", *textH, 15L);

Under memory pressure NewHandle returns a NULL handle. Dereferencing *textH then touches address 0 — and on a 68000 without memory protection that means a system crash, not a program crash.

On a machine with four megabytes and no virtual memory this is not a theoretical case.

textH = NewHandle((long)kBufSize);
if (textH == 0L)
    return;

Bonus — No update event

Drawing happens exactly once, right after the window opens. As soon as the window is covered and exposed again, the system sends an updateEvt — and nobody is listening. The content is gone.

A real program draws only between BeginUpdate and EndUpdate, driven by the event loop. The while (!Button()) construction here is not event handling at all, just a spin loop.

What the test case separates

BugWhat it requires
1 — //knowing the language standard of 1993
2 — Pascal stringknowing the Toolbox conventions
3 — SetPortknowing QuickDraw
4 — handleknowing about heap compaction
5 — NewHandleordinary diligence, platform-independent

Bugs 1 and 5 are visible to any reasonably attentive reader. Bug 2 comes quickly with a little Mac experience. Bugs 3 and 4 are the dividing line: both are formally correct C. No compiler warns, no linter complains, and no test run on a modern system would surface them.

Finding them requires knowing how this machine handles memory and graphics contexts — knowledge nobody has needed for thirty years, and which no current training corpus still carries in quantity.

That is exactly what makes it a good benchmark.


The run

The test case did not stay theoretical. On 25 July 2026 it went through the Code Assistant of ClaudeBridge 2.0 — typed into Netscape Navigator 3 under System 7.6.1, on the very machine the code targets.

The framing is worth noting. It was deliberately narrow:

Please help, this code does not compile

The question asked only about the build error. Everything beyond that came unprompted.

The Code Assistant with the test case pasted in — the question targets only the compiler

The result

BugFound
1 — // commentyes
2 — C instead of Pascal stringyes
3 — missing SetPortyes
4 — handle without HLockyes
5 — unchecked NewHandleno

Four out of five — including the hardest one. For bug 4 the reasoning read:

Handles must be locked before dereferencing to prevent memory compaction from moving the data

That is the right explanation, not just the right change. The second site in BlockMove was caught as well and wrapped in HLock/HUnlock — it sat elsewhere in the program and could not be derived from the first.

The answer with its list of applied corrections

What it missed was the unchecked NewHandle — the bug requiring the least platform knowledge, and the one left behind. A hint that attention follows the conspicuous: where a Toolbox quirk needs explaining, it looks closely; a plain return-value check slips through.

Two corrections that were not

Two further changes arrived unasked, and both looked questionable:

thePortqd.thePort. That applies to the Universal Headers, where the QuickDraw globals are gathered in a struct. THINK C 7.0 with its own headers accesses thePort directly — exactly as the test case has it.

DisposHandleDisposeHandle. The same pattern in reverse. DisposHandle is the classic spelling from Inside Macintosh and correct in THINK C 7.0; the longer DisposeHandle arrived with the Universal Headers, where the old form survives as a compatibility macro.

Both times a newer convention is applied to older code. The obvious explanation would be that the names blur in the training data — the answer speaks of “Symantec C++ 7.0” throughout, a related but different product.

That explanation was wrong, and the truth is less comfortable.

The prompt was at fault

A look at prompts.py settled it: the Code Assistant’s system prompt named “Symantec C++ 7.0” five times. Not THINK C. Symantec had acquired THINK C and shipped Symantec C++ as its successor — with newer headers.

So the assistant did not slip. It answered precisely for the compiler it had been given. For Symantec C++, qd.thePort and DisposeHandle are correct. The compiler in the prompt simply was not the one this project builds with; every other place in the repository names THINK C 7.0.

That turns the two “wrong” extras into a correct answer to a wrong question. The prompt now targets THINK C 7.0 and spells out which symbol forms belong to it.

What this means

The Toolbox semantics are solid: Pascal strings, GrafPort, heap compaction. That is the hard part, and it was found.

What the run actually exposed was a fault in the tool chain, not in the tool. On a platform whose compiler generations differ by individual symbol names, the target stated in the prompt governs every line of the output — and a wrong target produces code that looks plausible and does not build.

Two practical conclusions, then. The diagnosis is usable; the patch wants reading. And before blaming the model, read the prompt.

Quellen

  1. Source on GitHub (MIT): https://github.com/LoetLuemmel/ClaudeBridge
  2. ClaudeBridge 2.0 — Claude on a Macintosh from 1996: /claudebridge/claudebridge-2-0-on-a-1996-macintosh/
  3. Fünf Fehler in Think C (German original): https://pit.390er.de/68000/claudebridge-2-0/think-c-fuenf-fehler/
  4. Inside Macintosh: Memory — handles, master pointers and heap compaction
  5. THINK C 7.0 User Manual, Symantec 1993