March Docs

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 must derive 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

fnacceptsaccepts(d : Delivery, witness) : Bool#

@[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).

fnaccepts_tagaccepts_tag(d : Delivery, witness) : Bool#

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.

fnenqueueenqueue(q, to : Pid, msg, policy : NodeQueue.Policy) : Result(Int, NodeQueue.EnqueueError)#

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
end
fnenqueue_taggedenqueue_tagged(q, to : Pid, type_tag : String, json, policy : NodeQueue.Policy) : Result(Int, NodeQueue.EnqueueError)#

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

fnenqueue_tagged_schemaenqueue_tagged_schema(q, to : Pid, type_tag : String, schema : String, json, policy : NodeQueue.Policy) : Result(Int, NodeQueue.EnqueueError)#

enqueue_tagged with the message type's schema hash [schema] in the frame (what enqueue lowers to; `\

fnpayloadpayload(d : Delivery) : Result(JsonValue, String)#

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)
end
fnschema_isschema_is(d : Delivery, schema : String) : Bool#

The resolved form of schema_matches: does [d] carry exactly schema hash [schema]? (A delivery with no schema carries `\

fnschema_matchesschema_matches(d : Delivery, witness) : Bool#

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.

fnschema_ofschema_of(witness) : String#

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.

fnsendsend(peer : Peer, to : Pid, msg) : Result(Int, SendError)#

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
end
fnsend_taggedsend_tagged(peer : Peer, to : Pid, type_tag : String, json) : Result(Int, SendError)#

The 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
end
fnsend_tagged_schemasend_tagged_schema(peer : Peer, to : Pid, type_tag : String, schema : String, json) : Result(Int, SendError)#

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

fntag_istag_is(d : Delivery, tag : String) : Bool#

Does [d] carry type tag [tag], exactly? (What accepts_tag resolves to; the schema is not looked at.)

fntag_is_schematag_is_schema(d : Delivery, tag : String, schema : String) : Bool#

The resolved form of accepts (build step 9): [d]'s tag must equal [tag], and its schema hash must equal [schema] unless either is "". A sender built before schema hashes sends none and is taken at its tag.