Skip to content

L2 call graph is nondeterministic run-to-run (PyCG fixpoint under --pycg-max-iter) #99

Description

@rahlk

PROBLEM

Two back-to-back runs of canpy -i test/fixtures/whole_applications/requests -a 4 --no-ray with fresh caches produce different call graphs: 1027 vs 1057 edges. Every divergent edge carries prov:["pycg"]. Example instability: the same call resolving to requests.compat.MutableMapping.get in one run and collections.abc.MutableMapping.get in the other.

Separately, --ray vs --no-ray disagree on Jedi type inference for external symbols (e.g. pytest.main(...) inferred int in-process vs ExitCode in the Ray worker), shifting ~12 call-graph edges and ~35 param edges on the same fixture — the worker's interpreter environment resolves stubs differently than the parent process.

Observed 2026-07-14 on feat/v2-stage5-docs-release @ 3d39bbf during the stage-5 release gates (determinism gate: same-flags output must be byte-identical).

LIKELY MECHANISM

PyCG's fixpoint is capped (--pycg-max-iter, default 50) and its iteration order over the access-path domain is not stabilized, so a capped run stops at a different frontier each time. The cap itself is a documented sound-but-incomplete trade-off (CHANGELOG 0.3.0); the run-to-run variance is not.

IMPACT

  • The v2 determinism gate cannot pass at L2+ as specified.
  • Downstream monotonicity/superset comparisons across runs are noisy.
  • Not a regression from the schema-v2 work — PyCG landed in 0.3.0 and the behavior predates the v2 chain. Shipping 0.4.0 with this as a known-limitation release note is defensible; fixing it means stabilizing PyCG's iteration order (sorted worklists) or emitting only the converged subset.

REPRO

canpy -i test/fixtures/whole_applications/requests -a 2 --no-ray -c /tmp/c1 -o /tmp/o1
canpy -i test/fixtures/whole_applications/requests -a 2 --no-ray -c /tmp/c2 -o /tmp/o2
diff <(jq -S .application.call_graph /tmp/o1/analysis.json) <(jq -S .application.call_graph /tmp/o2/analysis.json)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions