Skip to content

Wintermute 0.3.4 — type-switch receive

Choose a tag to compare

@bsmr bsmr released this 13 Jul 14:17

Fourth step of the 0.3.x transpiler-language line: the type switch over a
received message. Go's other type-branching form now maps to Erlang's central
process construct — a multi-clause receive.

  • switch v := otp.Receive().(type) → multi-clause receive — a process
    waits for a message and dispatches on its type:

    switch v := otp.Receive().(type) {
    case Ping:
        otp.Print(v.Data)
    case Pong:
        otp.Send(v.To, v.Payload)
    default:
        otp.Print("unknown")
    }

    receive
        {ping, Data} -> io:format("~p~n", [Data]);
        {pong, To, Payload} -> To ! Payload;
        _ -> io:format("unknown~n", [])
    end

    Only struct-typed cases are supported: each case names one declared struct,
    lowered to the tagged tuple {tag, Field…} via the same pattern builder the
    0.3.1 single-clause receive uses (tag = lowercased type name, each field bound
    to its capitalized Erlang variable, in declaration order). case *Ping equals
    case Ping — Erlang has no pointers — but two cases that would share a tag
    (both forms of one type, or names differing only in case) are rejected, since
    the second clause would be unreachable. Field access v.Field lowers to the
    bound variable Field; a bare use of the alias v is rejected. Each clause
    runs in its own binding scope (bound snapshotted and restored), so a field
    collision with an outer binding is still rejected — the fourth
    bound-set-integration context.

  • default: is optional, and the choice is semantic: with a default: the
    receive gets a trailing catch-all _ -> (matches any other message
    immediately); without one it is a selective receive — non-matching messages
    stay in the mailbox and the process blocks until a listed type arrives. Both
    are idiomatic Erlang. Accordingly, terminates() counts a type-switch receive
    as terminating once every clause terminates, with NO default required (a
    receive cannot fall through), so it may be the then-branch of a bare if.

Correctness is proven end-to-end by a real-toolchain rung: a dispatcher fixture
transpiles, compiles with erlc, and runs — a {ping, …} message hits the
ping clause and a {pong, …} the pong clause, each returning its checked
payload.

Deliberately deferred to 0.3.5+ (all still error): a type switch over a plain
value (→ a case), non-struct cases (→ Erlang type guards), multi-type cases
(case Ping, Pong:), passing the whole aliased value, and after-timeout /
fallthrough / init / tagless forms.

Built subagent-driven and TDD: six tasks (fresh implementer + spec-and-quality
review each), a whole-branch opus review returning "ready to merge" after an
adversarial silent-mis-transpile hunt found none, and two post-review cleanups
(unified struct-type check, removed a dead statement case) folded before release.