200/200 corpus programs now run. Most of their code still runs in CPython, and I'll show you exactly how much.
Yesterday’s non-strict mode compiled untyped Python… until it hit one construct I
haven’t implemented. One lambda anywhere, and the whole program was a compile error.
On the 200-program competitive-programming corpus that meant 2 of 200 built.
Today: per-function fallback. 🧩
🔧 What happens now
In --non-strict mode, a top-level function, class or statement pycc can’t compile yet
is kept as Python source. At module init the compiled module hands it to CPython’s own
exec, in the module’s namespace, at the exact position it had:
$ pycc build --non-strict --report-fallbacks m.py -o m
fallback: function `describe` at m.py:11-14 runs in CPython: C0001 expression kind not supported yet: a `lambda`
fallback: statement at m.py:48 runs in CPython: C0001 expression kind not supported yet: a generator expression
Everything else stays native. The two sides talk through plain Python:
- compiled code calls a fallback function as a CPython object;
- fallback code calls compiled functions as the module’s published callables
(an embedded
__main__now publishes them too, like an--extmodule); - a module global that fallback code reads or binds lives in the module dict, and
compiled references to it become dict lookups, so
global counter; counter += 1in a lambda-using function is seen by the compiledprint(counter)right after; - exceptions cross both ways with CPython’s type and message, and fallback tracebacks
keep their real line numbers (the source is padded before
compile).
Strict mode is untouched: same diagnostics, same refusals.
📊 The numbers (fresh run, python3 bench/corpus.py -j 3 --flags=--non-strict)
| before | after | |
|---|---|---|
| pass | 2/200 | 200/200 |
| wrong output | 0 | 0 |
| compile errors | 198 | 0 |
Now the part that keeps me honest: 881 of 1,191 top-level statements (74%) ran in
CPython, and 102 programs ran entirely in CPython. Only the same 2 programs as
before have no fallback at all. The corpus is scripts that read input() at module
level, and input is still a gap, so a lot of them are “compile a preamble, exec the
rest”. That’s not a speed story yet. It’s a correctness floor: every program now
runs with CPython’s semantics, and every fallback line is a named, countable gap.
The top reasons, straight from --report-fallbacks over the corpus: builtin input
(184 statements), list ⊕ object arithmetic (97), int/object inference conflicts (71),
lambda (49), subscript assignment targets (42), map (38). That’s the to-do list,
ranked.
🐍 Also: no more pinned CPython
The embedded interpreter used to be pinned to the 3.14 line (tests to exactly 3.14.7).
Now everything that needs CPython (--ext headers, embedded builds, the oracle in the
tests) uses PYCC_PYTHON or python3: any CPython ≥ 3.13, matching the 3.13
oracle. CI now runs the ~500 hosted tests that used to sit behind #[ignore].
⏭️ Next
Shrink that 74%: input, list/object arithmetic and subscript targets alone account for
over a third of the fallbacks. Each one that lands moves statements from the CPython
column to the native one, and the corpus runner will say so.