Open concurrent processes with boundary traffic #
This file defines the semantic bridge between the closed-world concurrent
process core (ProcessOver, StepOver) and the open-world layer needed for
UC-style composition.
The key idea is simple: a closed concurrent step already records controller paths and local views at each node. An open concurrent step additionally records how each node may receive traffic from, or emit traffic to, an external boundary.
The design follows the layered approach from the UC design notes:
BoundaryAction Δ Xrecords, at one protocol node with move spaceX, whether the node is externally activated and what outbound packets the chosen move contributes.OpenNodeProfile Party Δ Xextends the existingNodeProfile Party Xby oneBoundaryActionfield.OpenNodeContext Party Δis the resulting realized node context.OpenProcess Party ΔspecializesProcessOverto that open context.
The closed-world layer is recovered by the canonical forgetful projection
OpenNodeContext.forget, which drops the boundary action and retains only
the NodeProfile. This means every OpenProcess can be viewed as a plain
closed Process by ProcessOver.mapContext.
Boundary actions are structurally mappable along PortBoundary.Hom via
BoundaryAction.mapBoundary. The isActivated flag is invariant under
boundary adaptation (only emit transforms), which ensures functoriality.
The query-level decoding of how input messages determine node moves belongs
in the runtime/execution layer, not the structural boundary action.
The emit field is presented as a PFunctor.Trace Δ.Out X (the free-monoid
trace on Δ.Out-events indexed by the node's move space X). This makes the
structural boundary operations land directly in the generic Trace API:
mapBoundary is Trace.mapChart on the output chart, and wireLeft /
wireRight are Trace.mapPartial on the appropriate sum projections. The
empty-emission default is the trace-monoid unit 1, which is definitionally
the constant-[] trace.
BoundaryAction Δ X records the boundary traffic associated with one
protocol node whose move space is X, against an open boundary Δ.
Fields:
isActivatedflags whether this node is driven by external boundary input (true) or by the internal protocol dynamics (false).emitis thePFunctor.Trace Δ.Out Xrecording, for each chosen movex : X, the finite ordered list of outbound packets contributed onΔ.Out. The default is the monoid unit1, which is definitionally the constant-[]trace.
The activation flag is a structural marker. The query-level information
about how an input message determines the node's move belongs in the
runtime/execution layer: charts (used by PortBoundary.Hom) can map
packets but cannot map queries, so the structural boundary action records
only the fact of activation, not the decoding.
The emit field records only the protocol's own direct output. Runtime-level
concerns (buffering, duplication, scheduling, delivery) belong in a separate
Runtime layer and are not encoded here.
- isActivated : Bool
- emit : PFunctor.Trace Δ.Out X
Instances For
A purely internal node: not externally activated and no outbound packets.
Instances For
A boundary-activated node that emits no outbound packets.
Instances For
An internally driven node that emits outbound packets.
Instances For
Transform a boundary action along a boundary adaptation.
The activation flag is preserved (it does not depend on the boundary
presentation). The emitted-trace is pushed forward along the output chart
φ.onOut via PFunctor.Trace.mapChart.
Instances For
Embed a boundary action on the left factor into the tensor boundary.
The trace is pushed forward along the left-injection chart
Interface.Hom.inl Δ₁.Out Δ₂.Out via PFunctor.Trace.mapChart. The
activation flag is preserved.
Instances For
Embed a boundary action on the right factor into the tensor boundary.
The trace is pushed forward along the right-injection chart
Interface.Hom.inr Δ₁.Out Δ₂.Out via PFunctor.Trace.mapChart. The
activation flag is preserved.
Instances For
Transform a boundary action on tensor Δ₁ Γ into one on tensor Δ₁ Δ₂
by keeping only the left-summand (Δ₁) packets and re-injecting them
into the left summand of the combined output. Right-summand (Γ) packets
are dropped (they become internal traffic handled by the runtime).
Implemented as PFunctor.Trace.mapPartial on the appropriate index-level
partial map.
Instances For
Transform a boundary action on tensor (swap Γ) Δ₂ into one on
tensor Δ₁ Δ₂ by keeping only the right-summand (Δ₂) packets and
re-injecting them into the right summand of the combined output.
Left-summand (swap Γ) packets are dropped (internal traffic).
Implemented as PFunctor.Trace.mapPartial on the appropriate index-level
partial map.
Instances For
Close a boundary action by dropping all external traffic.
The result lives on PortBoundary.empty and has no activation or emission.
This is used by plug to internalize all boundary interactions. The
emitted-trace is the monoid unit 1, which is definitionally the constant-[]
trace.
Instances For
OpenNodeProfile Party Δ X extends NodeProfile Party X with one
BoundaryAction Δ X recording the node's interaction with an external
boundary.
This is the per-node data that distinguishes an open process from a closed
one: the closed part (controllers, views) describes internal control and
observation, while boundary describes the node's interface with the outside
world.
- controllers : X → List Party
- views : Party → Multiparty.ViewMode X
- boundary : BoundaryAction Δ X
Instances For
Build an OpenNodeProfile from a closed NodeProfile by marking the node
as purely internal (no boundary traffic).
Instances For
Transform the boundary action of an open node profile along a boundary adaptation, preserving the closed-world node profile.
Instances For
The open-world node context for processes with boundary Δ.
At a node with move space X, the context value is
OpenNodeProfile Party Δ X: the usual controller-path and local-view data,
plus a BoundaryAction describing the node's external traffic.
Polynomial reading #
OpenNodeContext Party Δ is, up to the named-field-vs-pair identification
exhibited by OpenNodeContext.equivProductView, the non-dependent context
product
Spec.Node.Context.prod (StepContext Party) (fun X => BoundaryAction Δ X)
That is, the open node context is the polynomial product of the closed
StepContext Party and the boundary-action context fun X => BoundaryAction Δ X,
with both factors carried independently at each move space X. The
hand-rolled context-homs below (forget, embed, map, inlTensor,
inrTensor, wireLeft, wireRight, close) are concrete instances of
the universal projection / pairing maps for this product, specialized to
the particular boundary-action transformations they perform. The structure
form OpenNodeProfile extends NodeProfile is preserved as the working
API because it gives clean { toNodeProfile := ..., boundary := ... }
construction sites and definitional projections used pervasively below.
Instances For
Polynomial-product bridge #
Exhibit OpenNodeContext Party Δ as the non-dependent polynomial product
of the closed StepContext Party and the boundary-action context, and
prove that the bridge is a definitional isomorphism (round trips reduce
to rfl by Prod.mk.eta and structure eta). The product view lets one
phrase universal-property arguments without repeatedly pattern-matching
on OpenNodeProfile literals; the structural API below is the working
form.
The polynomial-product view of OpenNodeContext. Lives in the same
universes as OpenNodeContext Party Δ itself: the first universe is the
move-space universe w, and the second is whatever Lean infers for
NodeProfile Party X × BoundaryAction Δ X.
Instances For
Forward direction of the polynomial-product bridge: read off the
(NodeProfile, BoundaryAction) pair from an OpenNodeProfile.
Instances For
Inverse direction of the polynomial-product bridge: reassemble an
OpenNodeProfile from a (NodeProfile, BoundaryAction) pair.
Instances For
The forgetful map from the open-world context to the closed-world context.
This drops the BoundaryAction and retains only the NodeProfile
(controllers and local views).
Instances For
The embedding from the closed-world context into the open-world context.
This marks every node as purely internal (no boundary traffic).
Instances For
The context hom induced by a boundary adaptation.
This transforms every node's boundary action along φ while preserving
the closed-world node semantics.
Instances For
Embed the left factor's open-world context into the tensor boundary context.
This injects emitted packets into the left summand of the combined output interface while preserving the closed-world node semantics.
Instances For
Embed the right factor's open-world context into the tensor boundary context.
This injects emitted packets into the right summand of the combined output interface while preserving the closed-world node semantics.
Instances For
Wire the left factor: transform OpenNodeContext Party (tensor Δ₁ Γ) into
OpenNodeContext Party (tensor Δ₁ Δ₂) by filtering out internal (Γ) packets
and keeping only external (Δ₁) packets.
Instances For
Wire the right factor: transform
OpenNodeContext Party (tensor (swap Γ) Δ₂) into
OpenNodeContext Party (tensor Δ₁ Δ₂) by filtering out internal
(swap Γ) packets and keeping only external (Δ₂) packets.
Instances For
Close the boundary: transform OpenNodeContext Party Δ into
OpenNodeContext Party empty by dropping all boundary traffic.
Used by plug to internalize all external interactions.
Instances For
Existing context-homs as polynomial-product operations #
The following identities exhibit the hand-rolled OpenNodeContext
context-homs above as concrete instances of the universal projections,
pairing maps, and product maps for the polynomial product productView.
They are documentation rather than a refactor: the named API forms remain
the working surface, and the equalities below let one switch between the
two presentations on demand.
forget is the first projection of the polynomial product, postcomposed
with the bridge toProductView.
embed is the pairing of the identity on StepContext with the
constant internal boundary action, transported back along the bridge.
map φ factors as the polynomial-product map of the identity on
StepContext and the boundary-action transport
fun X => BoundaryAction.mapBoundary φ.
The open-world specialization of StepOver.
Here the node context carries OpenNodeProfile Party Δ, so every node
records both the usual controller/view data and its boundary traffic against
Δ.
Instances For
An m-parametric open concurrent process exposing boundary Δ.
OpenProcess m Party Δ bundles:
Proc— the residual state space of the process;step— for each state, the protocol step observed by the open world (move-treespec, node-local semantics, and continuationnext);stepSampler— a per-stateSpec.Sampler m (step s).specresolving each move of the step protocol in the intermediate monadm.
Each state of an open process carries its own nodewise-monadic
sampler as first-class data. openTheory m compositionally assembles
the per-step samplers of composite processes through par, wire,
and plug via Spec.Sampler.interleave, and the runtime layer
(processSemantics, processSemanticsAsync) reads the sampler off
the process rather than taking it as a separate argument.
Universe conventions #
The spec / move-type universe is pinned at 0 because m : Type → Type
only operates on Type and the Sampler m spec abbrev therefore
requires spec : Spec.{0}. The Party universe u and the state
universe v remain free.
Recovering the structural layer #
op.toProcess : ProcessOver ... projects onto the underlying
ProcessOver, feeding the structural lemmas in Concurrent/Process.lean
and the bisimulation infrastructure below.
- Proc : Type v
Residual state space of the process.
- step : self.Proc → Concurrent.StepOver (OpenNodeContext Party Δ) self.Proc
Protocol step observed at state
s. - stepSampler (s : self.Proc) : Spec.Sampler m (self.step s).spec
Per-state nodewise-monadic sampler that resolves each move of the step protocol in the intermediate monad
m.
Instances For
Structural projection onto the underlying ProcessOver, dropping
the per-state sampler. The closed-world ProcessOver lemmas from
Concurrent/Process.lean apply through this projection.
Instances For
Forget the boundary layer and view an open process as a plain closed-world process.
This is the canonical projection: it drops all BoundaryAction data from
every node while preserving the process structure, controller paths, and
local views. The sampler is also discarded, so the result is a bare
ProcessOver over the closed-world context.
Instances For
Embed a closed-world process as an open process with no boundary traffic, using a user-supplied per-state sampler.
Every node is marked as purely internal: isActivated = false and emit
produces no packets. The caller must supply the nodewise sampler because
the closed-world process carries no monadic information.
Instances For
Adapt the exposed boundary of an open process along a structural boundary morphism.
This transforms every node's boundary action along φ (translating emitted
packets, preserving activation flags) while leaving the process structure,
closed-world node semantics, and per-step samplers unchanged. The sampler
carries over verbatim because StepOver.mapContext preserves step.spec.
Instances For
Binary-choice interleaving of two open processes, targeting a common
boundary Δ via structural injections f₁, f₂ and a scheduling node
context schedulerCtx : OpenNodeContext Party Δ (ULift Bool).
Structure on Proc and step is delegated to the underlying
Concurrent.ProcessOver.interleave; the per-state sampler is assembled
by Spec.Sampler.interleave, which threads the scheduler sampler
schedulerSampler : m (ULift Bool) above the per-branch samplers so that
the resulting step carries a well-typed nodewise-monadic sampler.
This is the single ingredient shared by openTheory.par, openTheory.wire,
and openTheory.plug: those operations differ only in which injection
pair f₁, f₂ they supply.
Instances For
OpenProcess.System augments an open process by the standard verification
predicates used throughout VCVio. The verification predicates are about
the structural ProcessOver layer, so OpenProcess.System is monad- and
sampler-agnostic and refers to the underlying ProcessOver.System.
Instances For
Silent steps and weak bisimulation #
A transcript path through a decorated open-process spec is silent when
every visited node is not externally activated (isActivated = false).
Checking only isActivated (rather than also requiring emit x = [])
ensures the predicate is invariant under all context morphisms, including
wireLeft / wireRight which filter shared-boundary packets via
List.filterMap.
Instances For
A complete step of an open process is silent when every node along the chosen transcript path has boundary-internal semantics.
Instances For
IsSilentDecoration is invariant under context morphisms that preserve
isActivated. All morphisms in the open-process framework (including
mapBoundary, embedInlTensor, embedInrTensor, wireLeft, wireRight,
and closed) preserve isActivated.
IsSilentStep is invariant under OpenProcess.mapBoundary: remapping
the boundary does not change which transcripts are silent, because all
boundary maps preserve isActivated.
OpenProcessIso: weak bisimulation equivalence for open processes #
Two open processes with the same boundary are weakly bisimilar when there exists a relation on their state types satisfying:
- Totality / surjectivity: every state on each side has a related partner.
- Silent forward/backward: a silent step can either be matched by some step on the other side (maintaining the relation), or absorbed (the other side stays put and the relation is maintained with the successor).
- Visible forward/backward: a visible (non-silent) step must be matched by a visible step on the other side that preserves the relation.
This is the appropriate equality notion for openTheory monoidal laws,
where the internal scheduler structure differs (e.g., left-nested vs.
right-nested interleaving) but the observable boundary traffic is the same.
The scheduler nodes introduced by ProcessOver.interleave are always silent,
so they can be absorbed by the weak bisimulation.
Instances For
Every open process is weakly bisimilar to itself.
Weak bisimilarity is symmetric.
Weak bisimilarity is transitive. The composite relation witnesses p₂ as
an intermediate: ∃ s₂, r₁₂ s₁ s₂ ∧ r₂₃ s₂ s₃. For silent steps, the
intermediate state can advance or stay, using Classical.em to case-split
on whether the intermediate step is itself silent.