The model¶
A KlorPy program is a choreography: a global description of roles, of where values live, and of how they move. KlorPy compiles it into one endpoint program per role, with the communication already woven in.
Roles¶
A role is a symbolic participant. Roles are singletons created by the Role
constructor; A, B, C are predefined. A role names both a player and a
vantage point: expressions written at role A run only on A.
Locations¶
Every choreographic value has a static location — the set of roles that hold it. KlorPy tracks locations in three shapes:
| Location | Meaning | Example |
|---|---|---|
_UNIVERSAL |
an agreement value available to every role | literals, unannotated parameters |
frozenset |
an agreement value at those roles | {A} — A's local value |
tuple |
a distributed tuple; element i has its own location | (A, B) — element 0 at A, element 1 at B |
This is a light-weight analogue of Klor's location type system, and it is what makes the whole thing sound:
narrow/moveshrink a location;copy/liftwiden it.- A
move(x, A, B)requiresxto be located atA— rejected otherwise. - A value can only be used where it is located, so the generated per-role programs never read a value they don't have.
Projection¶
@choreography parses the function source (inspect + ast) and runs three
passes at definition time:
- Analyze — infer every variable's location
(
klorpy.projection.Analyzer). - Type-check — branch/loop/return consistency of locations
(
klorpy.check). - Project — emit each role's program as a list of async closures
(
klorpy.projection.Projector). Communication operators become explicit send/receive steps; each role keeps only the slice of code that concerns it.
The result is a Choreography object holding one endpoint per role, plus the
metadata (roles, params, param_locs, retvar, retloc) the runtime
needs.
Execution¶
The runtime runs the endpoints concurrently over a Transport:
simulate_chor all roles, one process, in-memory queues
play_role one role, user-supplied transport config
run_role one role, async — reuse with long-lived transports (e.g. TCP)
Because every send/receive is determined by the choreography (not by decoding
plausible program order out of per-role code), the projections are
synchronized by construction: matching sends and receives, no deadlock, no
divergence. Distributed tuples are the extreme example — each element already
lives at its role, so pack/unpack, tuple parameters, and tuple returns need
no communication at all.
Choice (knowledge of choice)¶
A branch is only reachable by roles that know the choice:
- a guard located at every role → each role decides locally, no broadcast
(
x % 2 == 0with an agreementx); - a guard located at a subset → one role chooses and broadcasts the decision to the others.
The same principle scales to match/case and loops — see
Choice and Loops.