Repository navigation
v0.3.6 — Multi-defimpl Phase 2 — function fan-out across sibling impls
Closes the Phase-2 deferral from 4136844 (multi-type defimpl extraction, v0.2.x). defimpl Proto, for: [T1, T2, T3] produces N sibling impl modules sharing one body. Phase 1 emitted N module entries (each with impl_for set), but attributed the body's functions to the first sibling only — siblings 2..N were left with zero functions and therefore invisible to Pass 7's :protocol_impl edge synthesis. The deferral was explicit in the original commit message: "Phase 2 (function-set duplication across siblings for full graph parity) deferred."
The fix is universal — fires for any project that uses the multi-type defimpl form, regardless of protocol, sibling count, or body shape. The deferral became visible after v0.3.5's topology rollup made Pass 7 edges visible at module level. It surfaced concretely while scanning analytics-master (the FOSS Plausible Analytics codebase used as Giulia's complex-shape test bed) — 5 isolated impl modules from one source file traceable to two multi-defimpl declarations:
# Found in the analytics-master test bed at extra/lib/plausible/audit/encoder.ex —
# the same shape would surface in any codebase that uses multi-type defimpl:
defimpl Plausible.Audit.Encoder, for: [Integer, BitString, Float] do
def encode(x, _opts), do: x
end
defimpl Plausible.Audit.Encoder, for: [DateTime, Date, NaiveDateTime, Time] do
def encode(x, _opts), do: to_string(x)
endInteger and DateTime (first siblings) had encode/2 and Pass 7 fired. The other 5 had nothing and were silent.
Fix
Stack representation in Extraction.function_traverse_pre/2 now uses a tagged {:multi, [name1, name2, name3]} for multi-defimpl scopes. The function emitter pattern-matches on the stack head: when it's {:multi, names}, one function record is fanned out per sibling. Single-defimpl scopes unchanged. function_traverse_post/2 still pops one stack entry regardless of shape.
Multi-defimpl fixture (test/fixtures/extraction/protocols_defimpl_multi.expected.exs) updated to require all expected encode/1 records (Integer, BitString, Float, then Atom). The prior fixture only expected Integer + Atom and locked in the bug — that was the test contract that allowed Phase 1 to ship without Phase 2 noticing.
Empirical impact (analytics-master test bed)
Force-rescan after deploy:
| Metric | After v0.3.5 | After v0.3.6 |
|---|---|---|
| Edges | 3631 | 3636 |
| Project unreached | 24 | 19 |
The 5 expected siblings (BitString, Float, Date, NaiveDateTime, Time) all reconnected. The remaining 19 isolates are honest static-analysis blind spots that would surface in any codebase that uses the same patterns — mox-registered mocks, mix data-migration scripts, plugins registered in config/runtime.exs, LiveView .heex callees (slice-a class), exception structs, and template helpers. None are extraction bugs.
Universality
The fix is shape-agnostic — fires for any project that uses defimpl X, for: [...] regardless of N, protocol, or body shape. Per the no-hardcoded-data rule, no library allowlist; no codebase-side opt-in. The analytics-master file just happened to be the first one that exercised the deferred path on a real codebase; the same shape is valid Elixir and could appear in any project.
Tests
59/59 pass across the focused subset (extraction + store + warm_restore). The fixture comparison verifies exact function-record parity per sibling.