v2026.7.21: SPEC-236 Phase B — ActorRef.call() Promise Sugar
v2026.7.21: SPEC-236 Phase B — ActorRef.call() Promise Sugar
Section titled “v2026.7.21: SPEC-236 Phase B — ActorRef.call() Promise Sugar”The :cluster profile now ships the ergonomic half of local request/response.
Where Phase A made Promise[T] and PromiseResolver[T] safe and complete,
Phase B makes the common pattern a single expression:
let p: Promise[u64] = typed_ref.call(QueryMsg.Get {})That is the CapTP-style answer without the ceremony of manual promise construction.
What’s new
Section titled “What’s new”.call() on typed actor and grain references
Section titled “.call() on typed actor and grain references”Per SPEC-236 [PROM:8.1], .call(...) on an ActorRef[Msg] or GrainRef[Msg]
returns a Promise[T] when the message variant declares a
PromiseResolver[T] reply field.
The compiler desugars:
let p = typed_ref.call(QueryMsg.Get {})into the same mechanism as the manual Phase A path:
let p = promise.new[u64]()let reply = promise.resolver[u64](p)_ = typed_ref.send(QueryMsg.Get { reply: reply })// expression value is pYou do not pass the reply field yourself. The compiler injects it.
Passing reply: explicitly is rejected with P236-E007.
Diagnostic P236-E007
Section titled “Diagnostic P236-E007”| Situation | Diagnostic |
|---|---|
Variant has no PromiseResolver[T] field | P236-E007 — no result type |
User supplies the reply field in .call() | P236-E007 — reply is auto-injected |
Receiver is not a typed ActorRef/GrainRef | P236-E007 — typed ref required |
Example:
file.jan:37:28: error: P236-E007: .call() requires a message variant with a PromiseResolver[T] reply field; variant has no result type 'Ping'Fire-and-forget unit variants (Ping, Stop) still use .send(), not
.call().
End-to-end shape
Section titled “End-to-end shape”{.profile: cluster.}
use std.cluster.local as clusteruse std.cluster.promise as promise
message QueryMsg { Get { reply: PromiseResolver[u64] }, Stop,}
@mailbox(capacity: 2)actor Store(msg: QueryMsg) do var value: u64 = 41
receive do QueryMsg.Get { reply } => do _ = promise.resolve[u64](reply, value + 1) end, QueryMsg.Stop => do return 0 end, else => do end, endend
pub func main() -> i32 do let sys = cluster.local_new(1 as u64, cluster.STRATEGY_ONE_FOR_ONE, 1 as u64) let actor_ref = Store_start_supervised_ref(sys, 0 as u64, cluster.POLICY_PERMANENT) let typed_ref: ActorRef[QueryMsg] = actor_ref
let p: Promise[u64] = typed_ref.call(QueryMsg.Get {}) _ = cluster.local_ref_drive_once(actor_ref) let answer = promise.await_or[u64](p, 0 as u64, 0 as u64) // answer == 42 return 0endPhase A reminder (already on unstable)
Section titled “Phase A reminder (already on unstable)”Phase A remains the floor under Phase B:
| Feature | Diagnostic / surface |
|---|---|
Promise[ref T] cannot cross boundaries | P236-E003 |
Affine PromiseResolver (no copy) | P236-E002 |
| Failed / Cancelled terminals | state / signal_fail / signal_cancel |
| Await does not spin | nanosleep backoff ([PROM:7.3]) |
See also the Phase A release notes.
Verification
Section titled “Verification”./scripts/zb test-cluster-promise-call-sugar./scripts/zb test-cluster-promise-call-no-result-reject./scripts/zb test-cluster-promise-request-responseLanded on unstable as e5f4655e (2026-07-21).
Migration
Section titled “Migration”- Existing
.send(...)code is unchanged. - Prefer
.call()for request/response variants that carryPromiseResolver[T]. - Keep
.send()for fire-and-forget unit variants.
Scope and what’s next
Section titled “Scope and what’s next”| SPEC-236 phase | Status |
|---|---|
| A — local Promise slots | ✅ Complete |
B — .call() sugar (local) | ✅ Complete (this release) |
| C — SBI PromiseId layout | ✅ Complete — Phase C notes |
| D — pipelined remote calls | Deferred (distributed runtime) |
Promise-on-promise pipeline segments ([PROM:8.2]) are Phase D, not Phase B.
Reference
Section titled “Reference”- std.cluster reference — Promise and
.call()section - :cluster profile — Sanctum overview
- Stateful Actors tutorial
- Monastery: Promise Call Sugar (
content/teaching/tutorials/06-promise-call-sugar.md)