Engineering 8 min

The macro that never compiled

For weeks this project had a C++ engine, a test suite, and a CI badge. None of it had ever run. Here is how that happens and what it costs.

There is a particular kind of software failure that produces no error message, no failing test, and no visible symptom, because the code in question was never executed by anyone. This project had one for weeks, at its foundation, and the way it was eventually caught is more interesting than the bug.

The bug

The C ABI has a rule: no C++ exception may cross the boundary. An exception unwinding into C is undefined behavior, so every entry point wraps its body in a guard that catches everything and converts it to a status code.

The guard was a macro. It looked roughly like this:

#define MARY_FFI_GUARD(body)      \
  try { body }                    \
  catch (const std::bad_alloc&) { return MARY_E_ALLOC; } \
  catch (const std::exception& e) { return record(e); }  \
  catch (...) { return MARY_E_INTERNAL; }

One parameter. And the bodies it wrapped were function bodies — full of top-level commas, in variable declarations and function calls. The C preprocessor splits macro arguments on top-level commas. Every use of that macro passed the preprocessor more arguments than it declared.

The fix is one character of intent:

#define MARY_FFI_GUARD(...)  \
  try { __VA_ARGS__ }        \
  /* ... */

Variadic, so everything after the opening parenthesis arrives as __VA_ARGS__ regardless of commas. Trivial. The interesting part is that it survived so long.

Why nobody noticed

Because nobody had compiled it. Not once.

The MCP server ran on the TypeScript reference engine, which needs no C++ toolchain, so day-to-day development never touched the core. The C++ code was written, reviewed, committed, and documented. There were tests. There was a CI workflow with a build matrix across GCC, Clang, and MSVC. Everything looked like a working native engine.

The CI workflow had never actually run — it was configured but not triggering on the paths that were changing. And the local development machine had no C++ compiler installed, so there was no moment where someone typed cmake --build and got a wall of errors.

The result was a project describing itself, in its own documentation, as a dual-engine system with parity enforced by 401 golden vectors, where one of the two engines did not build. Every statement about it was written in good faith and every one was unfounded.

The moment it broke

It came apart when the engine was finally built for real, in WSL with GCC 13.3. The first compile produced hundreds of errors, all downstream of the same macro. Fixing it took minutes. What it revealed took longer to sit with: the entire native half of the project had been in a superposition of “probably fine” for weeks, and nothing in the process was designed to collapse it.

That is the actual failure. Not the macro — the macro is an ordinary preprocessor gotcha that any C programmer hits eventually. The failure is a workflow in which it was possible to believe, with documentation and tests and a CI badge as evidence, that code was working when it had never been compiled.

What changed

Three things, and they are all about making belief expensive.

Build before claiming. The project’s own agent instructions now carry it as a hard rule with the exact command, so there is no ambiguity about what “it works” requires. Claiming a native result without a build is now a rule violation, not a judgment call.

CI that demonstrably runs. A workflow that has never executed is worse than no workflow, because it emits the same green reassurance as one that has. Every workflow here is checked for actual runs on actual commits, and the build matrix reports which compilers it covered.

Tests that assert against the source of truth. A related bug in the same area: several tests pinned the ABI version to a literal, like asserting the minor version equals 3. When the ABI moved to 0.4 those tests failed — not because anything was wrong, but because they encoded a constant instead of a relationship. They now assert mary_abi_version() == (MARY_ABI_MAJOR << 16) | MARY_ABI_MINOR, comparing the runtime value against the compiled header. That is a test of consistency, which is what was actually meant. It cannot go stale.

The general shape

Untested code is a known risk and everyone budgets for it. Unbuilt code is a different thing, and it is easy to miss because it fails an assumption so basic that nobody checks it: that the artifact exists at all.

The tell is a project where one language’s toolchain is not part of the daily loop. If you can do a full day’s work without invoking a compiler, that compiler’s output is not part of your feedback, and anything behind it is running on faith. The remedy is unglamorous — install the toolchain, run the build, make CI prove it ran — and it is the cheapest bug-per-hour trade in this entire project.

The macro cost minutes to fix. The habit it exposed was worth the weeks.