Skip to content

Types & parameters

Every value has a location — the set of roles that hold it. Locations are checked statically and drive both projection and erasure.

Locations

Shape Meaning Example
_UNIVERSAL agreement, available to every role literals, unannotated parameters
frozenset agreement at those roles {A}, {A, B}
tuple distributed tuple; each element has its own location (A, B)

The Analysis.loc lattice is the same structure that klorpy.check validates branch/loop/return consistency against.

Parameters and erasure

A parameter's location is given by its annotation; an unannotated parameter is treated as agreement (every role gets it).

@choreography
def dist_sum(x: A, y: B) -> A:
    sx = move(x, A, B)     # A -> B
    total = B(sx + y)      # B adds the two addends locally
    total2 = move(total, B, A)
    return total2

KlorPy then erases each parameter from the endpoints that don't hold it: role A binds only x, role B binds only y. play_role for A needs a config with only x; the runtime never puts y into A's state (Choreography.bind_param / role_holds_param).

Erasure, not boxing

Erasure is per-value: a parameter is either bound wholesale (scalar) or element-wise (tuple). There are no nested "annotated tuples of tuples" (structure destructuring is out of scope).

Tuple parameters

A (A, B) annotation denotes a distributed tuple: the caller passes one Python tuple, and the runtime splits it so that A receives element 0 and B receives element 1.

@choreography
def dist_tuple(pair: (A, B)) -> A:
    unpack([x, y], pair)   # x -> A, y -> B
    sx = move(x, A, B)
    total = B(sx + y)
    total2 = move(total, B, A)
    return total2

simulate_chor(dist_tuple, {"pair": (5, 7)})   # A receives 5, B receives 7

Returns

  • A scalar return yields {role: value}; roles without the value get NOOP.
  • A tuple return is assembled into a plain tuple by collecting each element from the role that holds it:
@choreography
def make_pair(x: A, y: B):
    p = pack(x, y)
    return p

simulate_chor(make_pair, {"x": 10, "y": 20})   # => (10, 20)

The type checker (klorpy.check)

Runs at definition time, before projection, and rejects:

  • a variable assigned different locations on different branches;
  • a loop-carried variable changing location inside a while;
  • a return value located differently depending on the path.

These are properties the per-role projector cannot verify by itself (it would silently miscompile), and no mypy plugin can express — see Status & roadmap for why we built a plain pass instead.