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