Skip to content

pycc-next v0.6.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 16:32
365f644

Drop-in for real projects. pip install builds a project with pycc through a PEP 517
backend shipped in the release archive; strict mode gets a real int | float type with
CPython's semantics for every operation; a differential fuzzer compares generated
programs against CPython in four build modes (0 findings in its last 30-minute nightly
run, 18 mismatches found and fixed on the way); untyped classes compile under
--non-strict, so pypng decodes the PngSuite images its tests use 3.18x faster than
CPython (v0.5: ~1.6x). Also shipped, ahead of v0.7: a Windows x86_64 archive (MSVC
toolchain, abi3 .pyd modules, win_amd64 wheels), method-level fallback (only the
smallest enclosing function or method of a construct pycc cannot compile runs in
CPython) and classes inheriting library classes, which take pydantic 2.13.5 to 789 of its
1249 functions compiled with 5716 of its 5740 tests passing (its speed is not measured
yet), and pypng to 78 of 85. Not met: the real-world corpus target (ROADMAP v0.6 accept
5) -- strict compiles 188/200 (target 190) and --non-strict keeps 1 of 1191 top-level
statements in CPython (target 0); carried over to v0.7. Prebuilt archives: Linux x86_64,
Linux aarch64, macOS arm64 and, new, Windows x86_64.

Benchmarks (python3 bench/run.py, best of 7, shared 4-core Xeon 2.1 GHz, load average
6-11 from other jobs; process start included except for the numpy kernels):

program CPython pycc-next Rust pycc vs CPython pycc vs Rust
collections_bfs 0.900s 0.144s 0.049s 6.3x faster 2.92x slower
fib 0.282s 0.013s 0.007s 21.7x faster 1.91x slower
mandelbrot 1.545s 0.043s 0.042s 35.5x faster 1.03x slower
nbody 2.530s 0.067s 0.025s 37.7x faster 2.68x slower
prefix_sum_np (kernel) 2.798s 0.029s 0.013s 95.4x faster 2.19x slower
sieve 1.000s 0.048s 0.021s 20.7x faster 2.35x slower
spectral_norm 0.836s 0.018s 0.011s 45.5x faster 1.69x slower
stencil2d (kernel) 3.751s 0.007s 0.003s 530.5x faster 2.17x slower

Against 0.5.0's table (same machine type, different load): fib, sieve and the numpy
kernels read lower (fib 21.7x against 26.5x, sieve 20.7x against 37.5x, prefix_sum_np
95x against 135x), nbody and mandelbrot higher; this run shared the box with other
builds, so treat the differences as noise until re-measured on a quiet machine.

Added

  • Windows x86_64 (v0.7 accept 1): pycc-vX.Y.Z-x86_64-windows.zip (bin\pycc.exe,
    lib\pycc\pycc_rt.lib, the backend's wheel) built, packaged and smoke-tested by the
    release workflow on windows-2025 with the MSVC toolchain, and verified by its publish
    job with the other archives. Native programs, abi3 .pyd extension modules (linked
    against python3.lib, CPython 3.13+) and programs embedding CPython; pycc.build
    tags wheels cp313-abi3-win_amd64. pycc drives link.exe through clang.exe
    (PYCC_CLANG, PATH, Visual Studio's clang component, or the LLVM installer's);
    PYCC_PYTHON defaults to python.exe. CI runs the test suite, the non-strict corpus
    gate and the real-package checks (bench/package_check.py, bench/wheel_check.py) on
    windows-2025; LLVM is LLVM's own 22.1.8 Windows release, pinned by sha256
    (scripts/windows_llvm.sh). The memory tests measure the peak working set there.

  • macOS CI also builds the real packages into wheels with pip wheel
    (bench/wheel_check.py).

  • Method-level fallback (v0.7 accept 3): under --non-strict, a construct pycc cannot
    compile sends only the smallest enclosing function or method to CPython, not its
    whole class or module statement. A class statement CPython runs keeps each method's
    body compiled (its def forwards to a compiled function; self crosses as the
    instance, super(), __class__ and private-name mangling as in CPython); a
    decorated top-level function keeps its def in CPython and its body compiled; in an
    --ext module a compiled class keeps its native type, a method that cannot compile
    attached to it as a CPython function (src/fallback/lift.rs, src/fallback/attach.rs,
    --report-fallbacks reports each).

  • Classes from libraries (v0.7 accept 2): --non-strict compiles the methods of classes
    inheriting CPython classes (threading.Thread, collections.Counter,
    argparse.Action, a namedtuple, library exceptions, class M(lib.Base)), with
    metaclasses, __init_subclass__, descriptors, class attributes of any type,
    @property, @staticmethod, @classmethod, functools.cached_property and
    functools.wraps-style decorators, all applied by CPython; annotations naming
    library types (re.Match, mod.Type), general unions and X | None of objects
    (objects); the with statement (PEP 343; async with still runs in CPython), and
    starred expressions in displays, assignment targets and calls. A compiled generator
    handed to CPython supports send(None), throw() and close() (as
    contextlib.contextmanager needs). A class Python code in the package subclasses
    (its own fallback code or a sibling module) is created by CPython, since a published
    native class is not subclassable; private module functions are published by
    non-strict --ext modules, so sibling modules can import them; an --ext function
    whose signature the boundary cannot bind (**kwargs, positional-only parameters of a
    public function) runs in CPython. from __future__ import annotations as x and
    augmented assignment to a computed target (f()[k] += 1) compile.

  • bench/pydantic_check.py builds pydantic 2.13.5's Python part (pinned by sha256)
    module by module with --ext --non-strict, counts the functions that compile, and
    optionally runs pydantic's own test suite against the compiled tree: 789 of its 1249
    functions compile and every module builds (33 and 55 modules failing to build
    before this release's work); 5716 of its tests pass, 24 fail (monkeypatched module
    attributes compiled code does not see, sys._getframe from compiled frames, and
    four subprocess tests that fail in CPython too).

  • pip install builds with pycc (v0.6 accept 1): pycc.build, a PEP 517 (and PEP 660)
    build backend in python/, shipped in the release archive as
    python/pycc_build-X.Y.Z-py3-none-any.whl. A project selecting it compiles each of
    its modules with pycc build --ext (strict by default; [tool.pycc] sets
    --non-strict, per-module overrides, modules kept in Python, modules allowed to fall
    back to Python, package discovery for src/ and flat layouts, extra pycc flags) and
    gets a cp313-abi3-<platform> wheel (manylinux_<glibc>_<arch>, or linux_<arch> when
    a module links a library manylinux does not allow; macosx_<N>_0_arm64) with METADATA
    from static PEP 621 metadata, entry points and a sha256 RECORD; also sdists and
    editable installs (uncompiled). A module pycc refuses fails the build with pycc's
    diagnostics. It finds the compiler as $PYCC or pycc on PATH, needs no network,
    and runs no project code. README: "Use pycc in your project".

  • CI builds diff-match-patch, tomli and pypng into wheels with pip wheel and the
    release archive's backend (bench/wheel_check.py, Linux x86_64 and aarch64), installs
    each into a fresh venv and runs the package's own test suite against it; the release
    smoke test (scripts/smoke.sh, all three platforms) builds and runs a small project's
    wheel the same way.

  • A differential fuzzer (v0.6 accept 4, fuzz/): generated typed programs run under
    CPython 3.13 and as native strict, --non-strict, --ext and --ext --non-strict
    builds; stdout, the uncaught exception and the exit status must agree. Refusals are
    counted per feature and mode, compiler crashes and mismatches are findings, each
    reduced to a minimal program. CI runs it for 2 minutes per push and 30 minutes
    nightly (.github/workflows/fuzz.yml, findings uploaded as an artifact).

  • Untyped code compiled natively by --non-strict (ROADMAP v0.6 accept 2, in progress):

    • bytes literals, as CPython bytes objects (strict mode still refuses them, C0001);
    • calls unpacking arguments into a CPython callee -- f(*args), o.m(a, *xs, b, k=v, **kw), zip(*rows) -- bound at run time with CPython's checks and TypeError
      messages; a builtin called with keyword or unpacked arguments is CPython's own;
    • except clauses whose type is a CPython object (except struct.error:, except mod.Error as e:, e the CPython exception), tested in order with the other clauses;
    • an object tested against a tuple, list or set of scalars (kind in (1, 2, 3, 4));
    • a standard-library function pycc compiles (math.ceil, ...) called with a CPython
      object is CPython's own;
    • builtin exception and warning classes, bytes, bytearray, getattr, ... as values
      (warnings.warn(msg, RuntimeWarning));
    • a builtin call inside an --ext module's public generator is routed through CPython
      like one anywhere else (its diagnostic is now placed);
    • untyped classes: attributes a method creates outside __init__ (self.width = ...
      in a header parser) are object slots, an unset one raising CPython's
      AttributeError; a method's keyword calls of its own methods (self.chunk(lenient= True)), generator methods, a method read as a value (self._scaled(self.size, k)),
      nested functions defined under an if (each branch's def newarray()), slice stores
      on objects, and private methods CPython code looks up (getattr(self, "_process_" + kind)), published in non-strict mode when their parameters are all objects;
    • a module-level function used as a value crosses into CPython as a callable (fn = (None, sub, up)[k]; fn(...)), one object per function;
    • a helper parameter defaulting to None and called with an int is an optional
      int; one rebound to an object is an object;
    • a CPython object compared with a list, tuple or set of scalars, indexing a tuple of
      one element type, or put in a display together with a generator;
    • conditional expressions joining two generators;
    • bytearray(...) locals are native byte buffers (items, len, iteration, append
      and extend of ints run natively), and the guarded native specializations cover
      private methods too, iteration of a byte buffer (for o in bs:), and the integer
      attribute a method reads first (self.bitdepth), passed in as an int;
    • a constant set operand of a comparison (t <= set(range(65, 91)) | set(range(97, 123))) is built once, as a module-level frozenset; standard-library functions an
      object call reaches are looked up once.
    • a guarded specialization also guesses bytearray for a parameter every call of the
      function in the module passes a bytearray(...) (or a local bound only to one):
      pypng's _deinterlace(self, raw) used to be guessed str from raw[i]/raw[i:j]
      alone, so its specialization never ran.
  • pypng 0.20220715.0: 64 of its 85 functions compile natively (15 in 0.5.0; 78 with
    method-level fallback, above), its own suite passing; bench/package_check.py --time also times decoding every PngSuite
    image its tests use.

  • int | float (v0.6 accept 3): a real union type in strict mode. A name bound to an
    int and to a float (a float('inf') sentinel min replaces with an int, an
    accumulator that starts at 0 and receives floats, a branch binding each), a list
    display or .append of both, a dict display's values, an int | float annotation,
    parameter, return, instance attribute, dict key or --ext parameter/result. Every
    operation is CPython's for whichever value it holds: arithmetic including //, %
    and **, exact mixed and big-int comparisons, abs, round, int(),
    float(), math.floor/ceil, str/repr/f-strings/format specs, hash, sum,
    min/max, sorted, membership. The value is one machine word extending the int
    encoding: a small int, a heap big int, or a float stored inline when its
    magnitude is between about 1e-77 and 1e77 (boxed otherwise), so pure int and
    pure float code is unchanged. Tests: tests/num_union.rs (strict, --non-strict,
    --ext, leak probe).

  • Strict mode compiles constructs the fuzzer counted among its most frequent refusals
    (tests: tests/strict_coverage_v06.rs, each fixture native strict, --non-strict and
    --ext): break/continue out of a try with a finally: clause (the finally
    block runs first, through handlers, else: and nested trys); the f-string = debug
    specifier, with a conversion or format spec; itertools.takewhile/dropwhile with a
    lambda or function predicate; a match on a tuple subject mixing literal, capture and
    starred patterns (it was refused as T0021 "literal pattern str does not match subject
    type int"); an empty list display passed straight to a builtin that cannot observe its
    element type (print([]), len([]), sorted([]), [] * n); str() of tuples,
    sets and optionals and bool() of every native value; cls(...) in a @classmethod,
    constructing the receiver's class also when a subclass inherits the classmethod (whose
    return type is then that subclass, as for type(self)(...)); and a class attribute
    the program assigns through its class (Base.count += 1), as long as every access
    spells Base.count (docs/DEVIATIONS.md); and f(**{'name': 'c', 'z': 3}), a dict
    display of identifier keys unpacked into a call of a module function, bound as the
    keyword arguments it spells (whatever its value types), raising CPython's TypeError
    at run time when it cannot bind.

  • Real-world corpus (v0.6 accept 5, strict 183 -> 188/200, --non-strict fallback
    statements 2 -> 1 of 1191), each tested native strict, --non-strict and --ext
    against CPython:

    • containers own big ints and boxed int | float floats: a list, tuple, dict or set
      holds them (they raised OverflowError, and a literal was T0051); tuples whose
      numbers differ in kind compare by value, and a float key finds the equal int
      key of a dict[int, ...] (tests/num_union.rs);
    • T | None (tests/optional_values.rs): in strict mode str | None, container and
      instance optionals as locals, parameters and returns; lists and dict values of
      int | None and pointer optionals (parents = [None] * n then parents[v] = u,
      ["a", None]); x == None/x != v; while x is not None: and if x: narrowing.
      An unnarrowed int | None used as an int stays a compile error;
    • tuple(xs) of a list or str: a native tuple[T, ...] that a list can hold,
      unpacked with CPython's length check (tests/tuple_of_list.rs);
    • --non-strict: a dict comprehension with a CPython object key or value, or over a
      CPython object, builds the CPython dict natively (tests/object_dict_comprehension.rs).

Removed

  • The v0.5 int infinity: float('inf') in a variable an int later replaces is now
    an int | float, so x // 2, x - x and storing it into a container give
    CPython's results instead of OverflowError.

Changed

  • CI runs cargo clippy --release --workspace --all-targets -- -D warnings (Linux
    x86_64), and the tree is clippy-clean. The runtime's closure, generator and exception
    incref/decref entry points and pycc_rt_hash_str are now unsafe extern "C" with
    documented pointer contracts (they dereference their argument); their symbols and ABI
    are unchanged.
  • Faster int code: a function's int locals whose every value is proven a smallint
    (an interval analysis over its assignments, range() loops, byte buffer items,
    & k, % k, abs(), ...: pycc_codegen's int_range) compute on the tagged words
    directly -- no tag tests, overflow checks, runtime fallbacks or bigint releases --
    and abs() of a number no longer checks for a pending exception. pypng's
    undo_filter_paeth loop runs about 3x fewer instructions.
  • Faster non-strict code calling CPython: a module function used as a value is boxed
    without rebuilding its closure each time, and called through CPython's vectorcall
    protocol (no argument tuple); a constant tuple or list a membership test reads
    (kind not in (1, 2, 3, 4)) is built once per site; a standard-library function
    called with a CPython object (math.ceil(o)) is found again by its site;
    bytearray.extend of a native list stores its bytes in one pass, and
    b.extend([e for v in xs]) reuses one list per site when e runs no other code.
  • Faster runtime helpers: a list comprehension over a named list allocates its result at
    the source's length, list.append stores in place when there is room, freeing a list
    of smallints releases nothing, int ** int of machine-sized operands and a bare %d
    of an inline int skip the bigint and general formatting paths.

Fixed

  • int | float memory safety: a float needle or key for an int | float container
    (1e100 in xs, d[1e-100], d.get(x, ...), and a refused store) leaked the boxed
    float it was converted to; the runtime's refcount paths read a boxed float through a
    bigint-sized reference (undefined behaviour), and an int-only reader handed one would
    have read its f64 and the memory past it as bigint limbs -- it now panics instead.
  • Non-strict: compiled code reading a module global that fallback code binds, before it
    is bound, raised KeyError: 'name'; it now raises CPython's NameError: name 'name' is not defined.
  • float floor division and modulo by zero raised ZeroDivisionError: float floor division or modulo by zero; the messages are now CPython 3.13's float floor division by zero and float modulo by zero.

Found by the fuzzer, each with a regression test in tests/fuzz_regressions.rs:

  • A --non-strict native program under the C/POSIX locale: code running in the
    embedded CPython printed non-ASCII text as UnicodeEncodeError (its locale is now
    configured as python3's, with UTF-8 Mode).
  • deque.extend, deque += ... and deque.insert ignored maxlen.
  • An --ext --non-strict module's compiled except AppError: missed an exception of
    a subclass running in CPython (one defining its own __init__): the bridge into
    compiled code now maps a CPython exception to the most derived exception class the
    module publishes.
  • A KeyError for an int or tuple key leaving an --ext module (or the native
    program embedding CPython) printed KeyError: '0' instead of KeyError: 0.
  • repr() of a str left characters str.isprintable() rejects beyond Latin-1
    unescaped ('\xa0', '​', '\U000e0001').
  • pow(a, b, m) with a bigint operand and round() of a float beyond 63 bits raised
    "not supported yet".
  • float ** float overflow raised OverflowError with pycc's own text instead of
    CPython's (34, 'Numerical result out of range').
  • A refused f(**mapping) call near the end of a file panicked the diagnostic renderer.
  • --ext --non-strict: a public exception class with its own __init__, methods or
    class attributes was published without them (SubErr(5) from Python skipped
    __init__); it now runs in CPython.
  • A set display of three or more constants iterated in insertion order instead of the
    order of CPython's folded frozenset constant ({-9, 6, 2, 7} prints {2, -9, 6, 7}).
    The same held for such a display built as a CPython object (--non-strict:
    str({...}), {...} | o).
  • repr() of a float with two shortest round-tripping strings picked Rust's instead of
    CPython's (nearest the exact value, ties to even: 137931034482758.62).
  • for a, b in zip(xs, ys) read the lengths once: a body shrinking a list raised
    IndexError, one growing it stopped early.
  • --non-strict: a str method run through CPython inside another call's arguments
    (xs.append(s.removeprefix(p))) was called as the outer method on str.
  • match: case [x, *rest] bound the whole subject to rest; an or-pattern inside a
    sequence, mapping or class pattern required every alternative; a mapping pattern
    raised KeyError for a missing key. A star before the end of a sequence pattern and
    **rest are now refused instead of miscompiled.
  • print(d, d.popitem()) printed a dict or set argument before later arguments changed
    it.
  • --non-strict: a set display meeting a CPython object crossed as a frozenset.
  • --ext --non-strict: a twinned function was defined at the end of the module when no
    blank line followed its def, so CPython code right after it raised NameError.
  • int() and float() of a str refused Unicode decimal digits (int('١٢')).
  • A for ... else over a str or tuple ran its else: clause after break.
  • A function returning a generator without yielding itself (return lengths(rows)) ran
    as a generator that ended at once, so its caller saw no items.
  • The guarded specializations of an --ext module were all dropped when any function
    bound the name type locally (length, type = struct.unpack(...)).

Known gaps

  • Real-world corpus (ROADMAP v0.6 accept 5, not met): strict 188/200 (target 190);
    --non-strict 200/200 with 1 of 1191 top-level statements in CPython (problem 188's
    mat_pow). The strict refusals are heterogeneous containers, an int | None used as
    an int without a None test, path-dependent rebinding and iter/range objects as
    values. Carried over to v0.7 (issues #5, #6, #7).
  • Windows x86_64 is new: a native program's stdout is \n-terminated UTF-8 whatever the
    console code page, --static-libpython is refused, and the compiler uses the system
    allocator (docs/DEVIATIONS.md). The benchmark table above is Linux x86_64 only.
  • pydantic under --ext --non-strict: 24 of its 5740 tests fail (monkeypatched module
    attributes compiled code does not see, sys._getframe from compiled frames, four
    subprocess tests that fail in CPython too); 460 of its 1249 functions still run in
    CPython.
  • Open rows of docs/DEVIATIONS.md, among them: an int beyond 63 bits as a slice bound
    and int ** negative int; container element and key types strict mode refuses
    (T0034-T0039); unsupported constructs refused in strict mode (C0001) or run in
    CPython under --non-strict; a published native class is not subclassable from
    Python outside its package; async with runs in CPython; a compiled generator handed
    to CPython accepts only send(None); an uncaught exception in compiled code prints
    only the last traceback line; raise X from None keeps __suppress_context__ false;
    reference cycles are never freed and __del__ is never called; a big int held by a
    tuple is never released; enum members at the --ext boundary (C0003). See
    docs/DEVIATIONS.md for every known difference from CPython 3.13.