Status assessment and forward plan for the Module.Types (native set-theoretic
typesystem) integration. Written 2026-07-04, at the point where PR #334 is
feature-complete and green across the Elixir 1.16–1.20 CI matrix.
These invariants are the merge-time agreement; the first two are enforced by CI boundary tests, the rest by review:
- Native descriptors are authoritative when available. Where a descr and
a shape disagree, the descr wins (trust ranking in
TypeHints::native_exck > :native_inferred > :spec > :shape). ElixirTypesis the only allowed native adaptor. No module outside it may callModule.Types.*— enforced bytest/elixir_sense/core/elixir_types_boundary_test.exsagainst compiled BEAM import tables.TypeHintsis the only LSP-facing type API. elixir-ls providers must not reach intoBindingorTypePresentation— enforced byinlay_hints_boundary_test.exson the elixir-ls side.- Shapes are an editor approximation for uncompiled/incomplete code and pre-1.18 Elixirs — a fallback, not a parallel type system.
- Translation is lossy by design and must degrade to weaker hints, never
stronger claims.
to_shape/1returnsnilrather than guess; the shape algebra over-approximates rather than fabricate disjointness (checked by the property harness, I1/I2/U1/C1).
The integration deliberately runs two data models:
- Shapes — Binding's runtime-shape grammar (
{:map, fields, tail},{:struct, ...},{:union, ...}, ...). Works on every supported Elixir version, on non-compiling code, and mid-edit. Set operations (combine_intersection,normalize_union,covers?) are a hand-rolled conservative approximation. - Descrs — the compiler's private
Module.Types.Descrdescriptors, available behind version gates (1.18+, full capability on 1.20). Exact, but opaque and API-unstable.
Translation lives in ElixirSense.Core.ElixirTypes (the single adaptor coupled
to compiler internals): to_shape/1 (descr → shape, lossy by design) and
coerce_var_type_public/1 (shape → descr, lossless only for descr_exact?
shapes). Binding's intersection/covers dispatchers already try a descr-backed
path first (with_descr_backing/3) and fall back to the custom algebra when
the backend is off or an operand is not exact.
Known structural debts:
- Data flow is descr→shape-first: precise native information is widened into shapes at most boundaries, then approximate ops run on the widened form.
- The custom set algebra (~1.4k lines in
binding.ex) duplicates what Descr answers authoritatively; it is the highest bug-density code in the integration. - Textual parity between
TypePresentationandDescr.to_quoted_string/2output is churn-prone coupling.
- Descr funnel — all direct
Module.Types.Descrcall sites outside the adaptor now go throughElixirTypes.descr_dynamic/0,descr_dynamic?/1,descr_union/2,descr_intersection/2,descr_empty?/1,descr_subtype?/2. The adaptor is the only module referencing compiler internals, so a future public-API switch (or internals drift) is a single-file change. - Property harness for the approximate algebra
(
test/elixir_sense/core/binding_descr_property_test.exs, StreamData,:requires_native_types-gated). Checks the custom shape algebra against Descr as ground truth: no false disjointness (I1), intersection over-approximation (I2), union coverage (U1),covers?soundness (C1), and crash-freedom on the full shape grammar including domain keys, optional fields, map tail markers, and improper lists (T1). This validates exactly the code paths production uses when native types are unavailable (Elixir < 1.18) or shapes are not descr-exact. Tunable viaPROP_MAX_RUNS. - CI boundary checks — beam-import-table tests enforcing contract items 2 and 3 above (one per repo).
- Short negative-cache TTL in ExCkReader — failures expire after 1s
(
:exck_negative_cache_ttl) so a module compiled seconds after being queried isn't hidden for the full 5-minute success TTL.
- Descr as first-class carrier in the Binding lattice. Allow an opaque
{:descr, d}member instead of eagerlyto_shape-ing at every boundary. Ops between two descr-carriers go straight to Descr (exact); mixed ops coerce via the existing pair. Shrinks the custom algebra's responsibility incrementally — no rewrite. When Elixir ships a public types API, adoption is the wrapper + adaptor only. - Grow
descr_exact?coverage (closed maps already coerce exactly; literal-free structs are candidates) so the descr-backed fast path fires more often — every widened case is one the property harness can't fully pin. - Stop chasing
to_quoted_stringtextual parity inTypePresentation. Treat rendering mismatches as acceptable drift, not bugs; keep parity tests as non-blocking documentation of divergence. - Expand the property harness to lossy cases. The current properties pin
the exact fragment; add explicit tests that lossy translations degrade the
right way —
to_shape/1returningnil(not a wrong shape) for unconvertible descrs, non-exact operands falling to the custom algebra rather than a spurious descr verdict, and trust downgrades to:shapewhere native info is absent. - Split the big files once behavior is pinned (no behavior changes,
separate PRs):
ElixirTypesinto capability probing / descriptor conversion+rendering / apply semantics / local signatures / pattern typing; the Binding shape algebra into its own module. The property harness and boundary tests make these refactors safe to verify. - Deletion map. When Elixir < 1.18 support is dropped: the shape-only
fallback paths, the pre-1.18 signature-tuple shims, and much of the custom
combine algebra become dead. When a public types API arrives: the
version-gated
of_match/applyshims anddescr_exact?gating collapse into direct API calls. Track these so the ~10k production lines shrink instead of ossifying.
- Splitting the PR (evaluated, declined — reviewed as one unit).
- First-class
{:dynamic, inner}in the Binding algebra (display-only today; see the note inbinding.exmoduledoc).