T27.AI

Blog

Six and a half years in one discarded return value

2026-08-14 · 9 min read

Four merged fixes turned the openXC7 demo CI green. The oldest was a bool nobody read: since February 2020 the placer knew its own placement was invalid and threw the answer away, so the failure surfaced in the router instead.

FPGAopenXC7nextpnrPlace and route

Last week the openXC7 demo-projects CI went green: every project builds. That is the headline and the least interesting thing that happened. The interesting part is what three of us found underneath it, including a defect that had been sitting in the placer since February 2020, quietly throwing away the one piece of information that would have caught it.

openXC7 is a fully open toolchain for Xilinx 7-series parts — yosys, nextpnr-xilinx, and the prjxray bitstream database, with no Vivado anywhere in the path. Roads like this have potholes, and some of them are old.

The oldest one: a return value nobody read

nextpnr’s HeAP placer finishes analytic placement and then runs a simulated-annealing refinement pass, placer1_refine(). It returns a bool, and returns false when its final post-placement validity check fails. The call site looked like this:

placer1_refine(ctx, placer1_cfg);

The result went nowhere. Because that check’s log_error is caught inside placer1_refine, nothing else escaped either. A placement already judged invalid by the tool’s own checker went straight on to the router, where it reappeared as an unreadable intra-site arc failure — an error message pointing at routing, for a fault decided during placement.

The fix is five lines, four of them a comment explaining why:

if (!placer1_refine(ctx, placer1_cfg))
    return false;

git blame puts that call site at commit 1b587cb5, David Shah, 2020-02-13 — "HeAP: pass through parameters to refinement". Merged 2026-08-13. The file has 59 commits, half of them from this summer, so the file’s date proves nothing; only the line’s does.

This is not a story about a careless author. The placer was correct when written and the refinement pass rarely failed. The bug becomes reachable only when something else starts producing placements that fail validity — which is exactly what the rest of this is about. Latent defects in old code are activated by new code, and the blame line points at the wrong year.

The one that still bites

The second old defect is an omission rather than a mistake. In a 7-series slice each letter position has exactly one selectable output pin — the xMUX — besides the dedicated O6 and the flip-flop’s Q. One pin, one claimant. The xc7 validity checker never budgeted it, so the packer was free to co-locate a 5-LUT whose O5 must reach the fabric, a carry whose sum feeds off-position, and a carry-out going somewhere other than the chain. Three claimants, one pin. The placer said yes; the router died with Failed to route arc ... CARRY4_O3 to AFFMUX_OUT.

#146 adds the per-position budget: count the claimants, reject the position if there is more than one, and let the legaliser keep searching instead of handing the router an impossible site. The checker it patches dates to David Shah’s xc7 legality work in late 2019 and early 2020. It was never there to be broken; it simply was never written.

The same slice geometry has a nastier relative that is still open. #134, filed by cheungxi, describes a bitstream that places, routes and meets timing, and then does not work on the chip — the board never answers the first UART command. #146 does not fix it. A bug that survives placement, routing, timing and CI, and shows up only as silence from a board, is the expensive kind.

The young ones

Not everything was ancient. Two of the four fixes were in code from this spring, and both are LUTRAM packing — which is why LiteX designs, leaning hard on distributed RAM for cache tags, were the ones that fell over.

FixWhat it wasAge of the defect
#145placer1_refine result discarded6 years 6 months (2020-02-13)
#146no OUTMUX budget in the xc7 validity checknever written; checker dates to 2019–2020
#142RAM256X1S mux tree built into the SPO halfabout 10 weeks (2026-05-29)
#144RAM128X1S scalar A0..A6 outside the DRAM control setabout 10 weeks

#142 changes one value: m256 ? 4 becomes m256 ? 0.

Three people, three days

The work ran 2026-08-12 to 2026-08-14. Carlos Venegas Arrabé (@cavearr) wrote #144 and #146. I wrote #142 and #145, and the #141 reproduction that started it. Hans Baier (@hansfbaier) reviewed every one, merged them, and kept the demo CI honest enough for the failures to be visible in the first place.

The most useful paragraph is about a mistake. We first attributed the picosoc failure to #146. It was #142’s class. We could not reproduce it because our lab trees had already been carrying #142’s one-line fix since an earlier campaign — every build in the sweep silently included it. We were sweeping the wrong variable, and the experiment could not have told us so.

Once that was seen the picture came out clean: with #146 alone both failing seeds die at the LUTRAM address placement and never reach the carry stage; with #142 and #146 together both pass, zero validity firings, zero route failures. Order matters, and we learned the order by getting it wrong first.

What a green CI is worth

That does not mean that the bitstreams work. Which is what we have to tackle next.

That is the maintainer, and it is the right bound on the claim. Next target is litex-ddr-arty-s7: a LiteX DDR design not merely built but running on the board. #134 is the sharpest illustration of the gap — a design that passes every automated gate we have and does nothing on hardware. Until a board answers, the gates are measuring the toolchain, not the design.

So the lesson is not "check your return values", true as that is. It is that new features are how you audit old code, and a toolchain becomes trustworthy only by being driven hard enough to fail in new places. A green CI is not the end of that process; it is the point where the next class of bug becomes visible.

What this does not settle

Receipts

Every figure above is measured, and the limits are named with it.