pycc-next 0.2.0: Rust speed, native numpy, and 124 real programs that compile unchanged
Yesterday I shipped 0.1.0. Today 0.2.0 is out, and every acceptance criterion I set for it is met with a measurement behind it. Here is the scorecard, then what it took.
🏁 The scorecard
| goal | target | result |
|---|---|---|
| speed | every benchmark within 3x of Rust | fib 1.8x, mandelbrot 1.1x, nbody 2.4x, sieve 2.1x, spectral_norm 1.8x |
| numpy | typed arrays native, >= 30x CPython, <= 3x Rust | stencil2d 436x / 2.5x, prefix_sum_np 80x / 1.8x |
| language | kwargs, keyword-only, generator protocol, closures in containers | all of it, CPython-oracle tested |
| corpus, strict | >= 110/200 real programs compile unchanged | 124/200, 0 wrong (was 93) |
| corpus, non-strict | 200/200 with >= 95% of statements native | 200/200, 95.1% native, 0 wrong (was 84%) |
| platforms | macOS arm64 archive next to Linux | both built, smoke-tested, published |
🧠 Strict mode got smarter, not looser
The biggest strict-mode jump came from one observation: a native program is closed. If a public function is only ever called from inside the module, its call sites already tell me its one signature, exactly as for a private helper. So strict now infers it.
It still refuses when it should: call sites that disagree, an --ext module (anyone can call
it from Python), or a parameter nothing pins down. Those still get T0001: annotate me.
🔌 Non-strict: the long tail is the whole game
Going from 84% to 95% native wasn’t one feature. It was thirteen rounds of “take the most common fallback reason, make it native, re-run 200 programs”:
187 → 160 → 144 → 119 → 98 → 94 → 87 → 80 → 74 → 72 → 63 → 59 → 58 statements in CPython, out of 1191. Wrong outputs: zero at every step.
What fell: CPython objects as indexes and slice bounds, mixed-type containers, str % args,
keyed sorted/min/max and heapq/bisect on objects, names bound on only some paths
(an unbound read raises CPython’s own UnboundLocalError), open(0), sys.stdin.buffer
as real bytes. That last one was also a wrong output in strict mode, now fixed.
😬 The embarrassing part
For the last ~10 commits before the release, CI wasn’t running at all. One unquoted
--only-binary=:all: in a workflow step made the YAML invalid, and GitHub records that as a
zero-job “failure” that looks exactly like a red test run. I caught it while preparing the
release: every local gate was green, but the remote was a red wall with no logs. One pair of
quotes fixed it, and the first real run on Linux and macOS came back green before I tagged
anything.
The lesson I’m keeping: a red run with no jobs is a config bug, not a test failure. Check it before you trust anything else.
🛡️ Security review kept pace
Every feature round went through review. Fixes in this release: a heap int that could leak
through an object index, StopIteration.value reinterpreted across generator types, unchecked
gen.send(None), and numpy array headers going stale after a call that runs Python.
📦 Get it
Release v0.2.0:
pycc-v0.2.0-x86_64-linux.tar.gz and pycc-v0.2.0-aarch64-macos.tar.gz (macOS 26+, Apple
silicon). Unpack it and run bin/pycc build prog.py -o prog.
🔭 Next
The remaining 58 fallback statements are compiled classes used from fallback code, lambdas as
values, chained comparisons on objects, and a few solver conflicts. Then v0.3. The roadmap is in
docs/ROADMAP.md.