# Bounded affine dependency bundles with typed concurrent operations and explicit close. import Base type Counts is Data: Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat} type Outcome<-E: Data, -O: Data> is Data: Success{value: O} Expected{error: E} Exhausted{} Stopped{} type CloseResult is Data: Closed{} Busy{counts: Counts} type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: Submit{principal: P, input: I, reply: Chan(Outcome)} Returned{resource: R} Inspect{reply: Chan(Counts)} Shutdown{reply: Chan(CloseResult)} # Copy only this context, never the resources. The inbox bounds queued commands; # callers must count blocked submissions inside their transport admission limit. type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data: Context{messages: Chan(Message)} def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome: match result: case Fail{error}: Expected{error} case Done{value}: Success{value} # The resources return through the private owner protocol BEFORE a disposable # caller reply. A closed caller reply cannot erase a handle or strand capacity. def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, messages: Chan(Message), reply: Chan(Outcome), result: R & Result<&2, &2, E, O> ) -> IO(Unit): (returned, outcome) = result do IO: accepted : Bool <- Chan.send(Message, messages, Returned{returned}) sent : Bool <- Chan.send(Outcome, reply, outcome.value(E, O, outcome)) return Unit{} def work(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), resource: R, principal: P, input: I, messages: Chan(Message), reply: Chan(Outcome) ) -> IO(Unit): do IO: result : R & Result<&2, &2, E, O> <- handler(resource, principal, input) work.return(R, P, I, E, O, messages, reply, result) def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List) -> IO(Unit): match resources: case Nil{}: IO.pure(Unit, Unit{}) case Con{resource, rest}: do IO: destroy(resource) dispose(~R, ~destroy, rest) # Closing Base channels retains buffered messages. Drain them and answer stopped # callers, rather than stranding accepted commands behind the close command. def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~destroy: R -> IO(Unit), message: Message ) -> IO(Unit): match message: case Submit{principal, input, reply}: do IO: sent : Bool <- Chan.send(Outcome, reply, Stopped{}) return Unit{} case Inspect{reply}: Chan.close(Counts, reply) case Shutdown{reply}: do IO: sent : Bool <- Chan.send(CloseResult, reply, Closed{}) return Unit{} case Returned{resource}: destroy(resource) def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~destroy: R -> IO(Unit), pending: Maybe<&1, Message>, next: Unit -> IO(Unit) ) -> IO(Unit): match pending: case None{}: IO.pure(Unit, Unit{}) case Some{message}: do IO: drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message) next(Unit{}) # The closed inbox contains at most room buffered messages. def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message) ) -> IO(Unit): match fuel: case 0n: IO.pure(Unit, Unit{}) case 1n+p: do IO: pending : Maybe<&1, Message> <- Chan.recv(Message, messages) drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages)) type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: Waiting{resources: List, counts: Counts} Received{message: Message, counts: Counts, resources: List} # IO.join closes a one-shot channel; the owner inbox must remain reusable. def receive.value(-A: Type, value: Maybe<&1, A>) -> IO(A): match value: case None{}: IO.die(A, 1, "camber internal channel closed unexpectedly") case Some{message}: IO.pure(A, message) def receive(-A: Type, channel: Chan(A)) -> IO(A): do IO: value : Maybe<&1, A> <- Chan.recv(A, channel) receive.value(A, value) # Only idle close ends this loop. Busy close never waits on a stalled handler. # @unsafe is solely the lifecycle receive loop, not a termination/proof claim. @unsafe def owner(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit), +messages: Chan(Message), +room: U32, step: OwnerStep ) -> IO(Unit): match step: case Waiting{resources, counts}: do IO: message : Message <- receive(Message, messages) owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Received{message, counts, resources}) case Received{message, counts, resources}: match message: case Returned{resource}: Counts{capacity, in_use, completed, rejected} = counts owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resource <> resources, Counts{capacity, Nat.sub(in_use, 1n), 1n+completed, rejected}}) case Inspect{reply}: Counts{+capacity, +in_use, +completed, +rejected} = counts do IO: sent : Bool <- Chan.send(Counts, reply, Counts{capacity, in_use, completed, rejected}) owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, in_use, completed, rejected}}) case Shutdown{reply}: Counts{+capacity, in_use, +completed, +rejected} = counts match in_use: case 0n: do IO: dispose(~R, ~destroy, resources) Chan.close(Message, messages) sent : Bool <- Chan.send(CloseResult, reply, Closed{}) drain(~R, ~P, ~I, ~E, ~O, ~destroy, U32.to_nat(room), messages) case 1n+ +p: do IO: sent : Bool <- Chan.send(CloseResult, reply, Busy{Counts{capacity, 1n+p, completed, rejected}}) owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 1n+p, completed, rejected}}) case Submit{principal, input, reply}: Counts{capacity, in_use, completed, rejected} = counts match resources: case Nil{}: do IO: sent : Bool <- Chan.send(Outcome, reply, Exhausted{}) owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{Nil{}, Counts{capacity, in_use, completed, 1n+rejected}}) case Con{resource, rest}: do IO: IO.spawn(Unit, work(~R, ~P, ~I, ~E, ~O, ~handler, resource, principal, input, messages, reply)) owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{rest, Counts{capacity, 1n+in_use, completed, rejected}}) def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit), result: Result<&2, &1, E, R>, resources: List, next: List -> IO(Result<&2, &1, E, List>) ) -> IO(Result<&2, &1, E, List>): match result: case Fail{error}: do IO>>: dispose(~R, ~destroy, resources) return Fail{error} case Done{resource}: next(resource <> resources) def build(~K: Data, ~R: Type, ~E: Data, ~create: K -> Nat -> IO(Result<&2, &1, E, R>), ~destroy: R -> IO(Unit), remaining: Nat, +config: K, resources: List ) -> IO(Result<&2, &1, E, List>): match remaining: case 0n: IO.pure(Result<&2, &1, E, List>, Done{resources}) case 1n+ +p: do IO>>: result : Result<&2, &1, E, R> <- create(config, p) build.step(~R, ~E, ~destroy, result, resources, next => build(~K, ~R, ~E, ~create, ~destroy, p, config, next)) def start.ready(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit), capacity: Nat, +room: U32, result: Result<&2, &1, E, List> ) -> IO(Result<&2, &2, E, Context>): match result: case Fail{error}: IO.pure(Result<&2, &2, E, Context>, Fail{error}) case Done{resources}: do IO>>: channel : Chan(Message) <- Chan.new(Message, room) +messages : Chan(Message) = channel IO.spawn(Unit, owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 0n, 0n, 0n}})) return Done{Context{messages}} # The initializer closes partial handles within a failed bundle; start closes # all prior whole bundles. No workers exist until the complete set opens. def start(~K: Data, ~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, ~create: K -> Nat -> IO(Result<&2, &1, E, R>), ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit), config: K, +capacity: Nat, room: U32 ) -> IO(Result<&2, &2, E, Context>): do IO>>: result : Result<&2, &1, E, List> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{}) start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result) def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context, principal: P, input: I, reply: Chan(Outcome) ) -> IO(Bool): Context{messages} = context Chan.send(Message, messages, Submit{principal, input, reply}) def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome)) -> IO(Outcome): match accepted: case False{}: do IO>: Chan.close(Outcome, reply) return Stopped{} case True{}: do IO>: result : Outcome <- IO.join(Outcome, reply) Chan.close(Outcome, reply) return result def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context, principal: P, input: I ) -> IO(Outcome): do IO>: channel : Chan(Outcome) <- Chan.new(Outcome, 1) +reply : Chan(Outcome) = channel accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply) call.accepted(E, O, accepted, reply) def inspect.value(value: Maybe<&1, Counts>) -> Maybe<&2, Counts>: match value: case None{}: None{} case Some{counts}: Some{counts} def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>): match sent: case False{}: do IO>: Chan.close(Counts, reply) return None{} case True{}: do IO>: value : Maybe<&1, Counts> <- Chan.recv(Counts, reply) Chan.close(Counts, reply) return inspect.value(value) def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(Maybe<&2, Counts>): Context{messages} = context do IO>: channel : Chan(Counts) <- Chan.new(Counts, 1) +reply : Chan(Counts) = channel sent : Bool <- Chan.send(Message, messages, Inspect{reply}) inspect.sent(sent, reply) def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult): match sent: case False{}: do IO: Chan.close(CloseResult, reply) return Closed{} case True{}: do IO: value : CloseResult <- IO.join(CloseResult, reply) Chan.close(CloseResult, reply) return value # Stop external admissions before close. Busy preserves live context/capacity; # retry after admitted operations finish. Stalled operations remain observable. def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(CloseResult): Context{messages} = context do IO: channel : Chan(CloseResult) <- Chan.new(CloseResult, 1) +reply : Chan(CloseResult) = channel sent : Bool <- Chan.send(Message, messages, Shutdown{reply}) close.sent(sent, reply)