Skip to content

Wintermute 0.3.7 — tagless + multi-type type switch

Latest

Choose a tag to compare

@bsmr bsmr released this 17 Jul 15:48

Wintermute 0.3.7 closes the type-switch story — the seventh feature step of the
0.3.x transpiler line. It adds the two remaining clean forms and a correctness
fold.

  • Tagless form switch M.(type) (no alias) — detection-only cases in both
    the value form (case M of …) and the receive form (receive …). A struct
    case matches the tag with wildcard fields ({ping, _}); a primitive case
    guards a throwaway variable (_X when is_integer(_X)). The body references
    neither the value nor its fields (there is no variable).
  • Multi-type cases case Ping, Pong: — one Go clause expands to several
    Erlang clauses (one per listed type) sharing a duplicated body. Fields are
    never bound (Go keeps v at the interface type in a multi-type case), so
    v.Field there is rejected loudly rather than emitted as an unbound or
    silently-outer-bound variable. Bare v (the whole value) is bound via the
    0.3.6 whole-alias machinery (V = {ping, _} / V when is_integer(V)).
  • Nested-alias foldbodyUsesBareAlias now stops at a nested type switch
    reusing the enclosing alias name, fixing a false "collides with an
    already-bound name" rejection (the 0.3.6 Copilot-gate MINOR).

default: stays REQUIRED for the value form (tagless and multi-type alike) — a
value matching no case falls through in Go, which a total Erlang case cannot
express. As a side effect of the multi-type refactor, a single primitive case
whose body never uses the alias now guards a throwaway _X instead of an unused
V, incidentally removing the erlc "unused variable" warning that clause used to
produce.

Still rejected loudly (0.3.8+): wider primitive types (int32, float32,
byte, []byte, named types) — Erlang has no fixed-width integers, so they
would collapse onto the same guard as int/float64/string and make two
statically-distinct Go cases share one guard: a silent mis-transpile. Also
deferred: nil cases, an init statement, and gen_server callback completion.

Built subagent-driven/TDD: four tasks (implementer + spec/quality review each) +
a whole-branch opus review + the Copilot release gate. The gate caught a real
silent mis-transpile the internal reviews (including opus) missed: the multi-type
v.Field reject only matched a literal v.Field, so deriving a new variable from
the value first — directly (W := v; W.Field) or threaded through a call
(X := f(v); X.Field) — bypassed it and lowered the later field access to a bare
field name that silently resolved to an unrelated outer binding. The fold rejects
binding any variable derived from the matched value in a multi-type case (the RHS
is scanned transitively), so the alias stays usable only as a whole value.