Robert Wittams

Porting the RAD Debugger to macOS

The port itself, with a branch-by-branch breakdown, is at robert.wittams.com/raddebugger.

The RAD Debugger stopped at a source breakpoint on a line that reads: delete your_account;
One of the example targets, stopped at a source breakpoint. Click for the full window.

Why do this?

I wanted to:

I don’t really expect this to be upstreamed and that is fine! I didn’t quite realise how strong the anti LLM sentiments were amongst some in the community, so this is provided as is.

How the port was made

I broke down the work into architectural layers that could be proven end to end. Most of the work was produced using GPT-5.3-codex and GPT-5.5 in codex cli.

Some work was done by hand:

These parts just turned out to be easier to think about/specify in code at the time. Probably totalling less than 1500 lines.

The initial x86 port took about five days; ARM64 about three more - in May 2026. Everything since: debugging, and maintenance against a moving upstream, takes maybe 0.5-1 days a month.

Reading the code

For some projects or stages in development, code reading is no longer strictly required: prototyping, ‘code as content’. Possibly, if you have good enough testing or formal verification in place this can be extended to more involved projects. For this particular project, I find it necessary to read the code - to notice what is going wrong - especially for things that don’t yet have a verifiable signal.

Common agent mistakes on this project:

Deterministic checks for some of these were either added or are planned.

Of course, more ‘one off’ bugs/mistakes are found by reading the code too.

Rippling the DAG

The port is maintained as a DAG of subject specific branches, somewhat like a traditional patch set. They culminate in an integration branch. The branches are prefixed by a generation - e.g. mac8. I have some Python tooling oriented around maintaining this DAG through two operations:

How a ripple actually runs

When an earlier branch changes, the change ripples through every branch after it; the generated joins are recreated from their inputs, never committed to. A manifest records the intended graph, and tooling checks Git ancestry against it before and after each session. A generation move (usually an upstream refresh) is the same operation: the whole DAG is rebuilt against a new pinned upstream base (the previous generation is archived intact).

Fixes found at the integration tip are batched there and rippled deliberately, and a ripple must end with an empty diff against the tested tip.

Testing is then done on various machines by agents through the ipc interface.

Patch-set history is not currently ideal. A branch keeps its identity across generations - mac7/mac/appkit-shell and mac8/mac/appkit-shell are the same idea, but with unrelated commits. git range-diff can compare two versions of a series; jj change IDs survive rebases within a repository, but not a re-derivation.

This work I have found doable in a factory style, as it is quite a repeatable pattern that is being followed.

Planned:

Assessment

My finger in the air assessment is that this would have taken me around 2 months without agents. Totally doable - but would I have done it? I doubt it. Humble-braggingly I will concede this is mainly a tick in the ‘agents amplify those who already know what they are doing’ column.

On the debugger - it is good, and fun to use. I still think Java debuggers are slept on by many though, in terms of features/ease of use! There are still a lot of gaps here vs every feature of gdb/lldb, but it is much nicer to use than those and the wrappers I’ve used recently. The one feature I do miss from IntelliJ is dependent breakpoints - I may well add that (portably of course). Remote debugging and separation of debugger and target architecture are clearly possible, but will need quite a bit of surgery - not something for this port!

What I might do differently if re-running this port:

References

← All writing