Node
Node: the typed remote send (distributed actors, last piece of 1/4).
Node.send(peer, to, msg) is what a user writes; everything under it exists already (NodeSend carries bytes, PeerReader reads them, the control/data split, GlobalPid). What this layer adds is the codec contract, and the compiler enforces it:
msg's type mustderive Json. A missing codec is a TYPECHECK error
at the call site, naming the type -- not the runtime panic a generic
`to_json` gives.
* The wire `type_tag` is the message type's name as the compiler resolves
it (module-qualified where the type is declared inside a module), the
same name `derive Json for T` registers its codec under, so a sender and
a receiver agree on the tag by construction. No string to keep in step.
* A `Pid` anywhere in the type is refused by `derive Json` itself: a local
pid means nothing on another node. Carry a `GlobalPid.Pid`.Mechanism. send below is an ordinary stdlib function whose BODY is never reached: the typechecker records, per Node.send call site, the resolved message type (Typecheck_caps.check_node_send_sites, after the whole module is solved -- the argument's type is usually pinned later than the call), and both backends rewrite the call to send_tagged_schema(peer, to, "<tag>", "<schema>", JsonTo$T.to_json(msg)) -- the interpreter in Eval's EApp arm, the compiler in Lower_expr, both off the March_ast.Json_dispatch table that from_json already uses. An UNCHECKED interpreter run (the stdlib March test harness never typechecks) has no table; there the interpreter names the type from the value itself, the way the generic to_json does, and the tag is that runtime name. The body below is reached only by a value no table and no runtime shape can name; it says so loudly.
Node.enqueue(q, to, msg, policy) is the same contract through a peer's NodeQueue (credit-based flow control): same codec check, same minted tag, rewritten to enqueue_tagged_schema(q, to, "<tag>", "<schema>", JsonTo$T.to_json(msg), policy).
Receiving is the caller's: read the ACTOR_MSG (NodeSend.serve_one or a PeerReader), compare d.type_tag with the tag the sender's compiler minted (a receiver hosting T in the same module tree writes the same qualified name), and decode with Node.payload(d) pinned to T by the result type -- from_json dispatch is the typechecker's already. Design: specs/progress/2026-09-14-remote-send-to-a-global-pid.md.
Functions
@[remote]'s tag test (a generated <Actor>_Remote.dispatch calls it): does [d] carry a message of the type of [witness]'s parameter, as in Node.accepts(d, fn (_ : T) -> ())? The compiler rewrites the call to tag_is_schema(d, "<tag>", "<schema>") with the tag and schema hash Node.send mints for that type. Panics if reached at run time (an entry point skipped that rewrite).
Does [d] carry T's tag, ignoring the schema, for accepts_tag(d, fn (_ : T) -> ())? Tells a same-named type of ANOTHER shape. The compiler rewrites it to tag_is; panics if reached at run time.
The typed send through a peer's queue [q] under [policy]: msg's type must derive Json and the compiler mints the tag, exactly as for send, rewriting the call to enqueue_tagged_schema. Returns Ok(seq) once the frame is admitted to the queue (not yet written). Panics if reached at run time (a pipeline that skipped the compiler's rewrite).
match Node.enqueue(q, to, Ping({ n: 1 }), NodeQueue.DropNew) do
Ok(seq) -> seq
Err(NodeQueue.Backpressure) -> -1
Err(_) -> -2
endsend_tagged through a peer's outbound queue [q] (NodeQueue, credit-based flow control): the frame is admitted under [policy] and written by the queue's writer when the receiver's credit covers it. Ok(seq) means admitted, not written, and counts as one message sent; Err(Backpressure) / Err(NoConnection) are the queue's. enqueue lowers to this exactly as send lowers to send_tagged.
enqueue_tagged with the message type's schema hash [schema] in the frame (what enqueue lowers to; `\
The receiver's half of the codec: [d]'s payload parsed as JSON, ready for a from_json pinned to the hosted type. Err with the parser's message when the payload is not valid JSON.
match Node.payload(d) do
Ok(j) -> handle(j)
Err(e) -> println("bad payload: " ++ e)
endThe resolved form of schema_matches: does [d] carry exactly schema hash [schema]? (A delivery with no schema carries `\
Does [d] carry T's schema hash, for schema_matches(d, fn (_ : T) -> ())? Picks the migrate_msg input type an old shape decodes as. The compiler rewrites it to schema_is; panics if reached at run time.
T's schema hash as a String, for schema_of(fn (_ : T) -> ()). The compiler replaces the call with the hash; panics if reached at run time.
The typed remote send: deliver [msg] to the process [to] on [peer]. msg's type must derive Json (a typecheck error at the call site otherwise); the compiler rewrites every call to send_tagged_schema with the type's qualified name as the tag (see the module comment). Returns Ok(seq) once the frame is written, the seq a later DELIVERY_FAILED echoes; a failed write is Err(NoConnection). Panics if reached at run time: only a pipeline that skipped the compiler's rewrite gets here.
match Node.send(peer, to, Ping({ n: 1 })) do
Ok(seq) -> seq
Err(_) -> -1
endThe explicit form of send: the tag and the JSON value in hand. send lowers to this; a receiver-side helper or a test may call it directly. Takes the next number of this process's send sequence, serializes [json] as the payload and writes one ACTOR_MSG with NodeSend.cast on the peer's data connection (its only connection when it has no data one). Returns Ok(seq) once the frame is written (a later DELIVERY_FAILED echoes the seq), or NodeSend.cast's error: NoConnection when the write fails, Refused when certificate mode forbids the raw send.
match Node.send_tagged(peer, to, "Chat.Msg", Json.Str("hi")) do
Ok(seq) -> seq
Err(_) -> -1
endsend_tagged with the message type's schema hash: the form a typed send lowers to (build step 9). [schema] is computed by the compiler; it rides the frame (NodeSend.cast_schema) so a receiver can tell a same-named type of another shape. "" sends no schema, exactly as send_tagged.
Does [d] carry type tag [tag], exactly? (What accepts_tag resolves to; the schema is not looked at.)