Skip to content

v0.3.6 — Multi-defimpl Phase 2 — function fan-out across sibling impls

Choose a tag to compare

@thatsme thatsme released this 28 Apr 03:24
· 100 commits to main since this release

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)
end

Integer 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.