Actors
Cure 0.25.0 turns the language into a first-class environment for writing OTP-style supervision trees. The four pieces that land together are:
-
The Melquiades Operator
<-|(unicode alias✉) for sending a message to a pid. -
actorcontainers that compile to loadedGenServermodules with exhaustiveness-checked message handlers. -
supcontainers that compile to verifiedSupervisorbehaviour modules with compile-time structural checks. -
Stdlib modules
Std.Actor,Std.Process,Std.Supervisorthat expose the runtime to Cure source.
Together they let you declare a production-grade supervision tree without ever dropping into Elixir or Erlang.
The Melquiades Operator
pid <-| message sends message to pid and evaluates to the message, matching Erlang’s ! semantics. The ASCII spelling and its unicode alias are interchangeable:
pid <-| :hello
pid ✉ :hello
Both forms lower to Erlang’s bang operator ({:op, Line, :!, PidForm, MsgForm} in abstract form), so the runtime cost is exactly the same as a bare erlang:send/2: non-blocking, returns the message it sent, never raises for a dead receiver.
The operator is non-associative and binds one notch below |> so pipelines feed into it naturally:
request
|> encode()
|> worker_pid <-| _
The last line is equivalent to worker_pid <-| encode(request).
Why “Melquiades”?
Named after the ghost-mailman of One Hundred Years of Solitude, who keeps delivering letters even after his own death. The arrow points into the inbox on the left: pid <-| message reads “the pid gets this message”.
Keyword form
send target, msg is a synonymous statement form retained for backward compatibility and for Std.Fsm clients. It desugars to the same {:send, …} MetaAST node as <-|, so round-trip printing preserves the author’s chosen spelling through a :melquiades_form meta key (:ascii, :unicode, or :keyword).
Actors
An actor container declares a typed process. The minimal grammar:
actor Counter with 0
on_start
(state) -> state
on_message
(:inc, n) -> n + 1
(:dec, n) -> n - 1
(:get, n) ->
notify(%[:value, n])
n
on_stop
(reason, _state) -> notify(%[:stopped, reason])
-
with <expr>seeds the actor’s initial payload. Omit it and the payload starts asnil. -
on_start,on_message,on_stopeach accept one or more clauses; the clause syntax mirrorson_transitionin FSMs (pattern tuple + optionalwhenguard + body). -
The first argument of an
on_messageclause is the incoming message; the second is the current payload. -
Inside any clause,
notify(message)sends to the process that spawned the actor. The helper is wired throughCure.Actor.State.notify_self/1, which reads the actor’s registered:callerfrom the process dictionary. -
The return value of an
on_messageclause becomes the actor’s new payload. Returning a%Cure.Actor.State{}struct replaces the entire runtime state instead, exactly like callback-mode FSMs.
Each actor Counter container compiles to a loaded BEAM module named Cure.Actor.Counter. The module is a regular GenServer:
iex> :"Cure.Actor.Counter".start_link(0)
{:ok, #PID<...>}
iex> send(pid, :inc)
iex> :"Cure.Actor.Counter".get_state(pid)
1
For user code, prefer Std.Actor.spawn/1 together with the <-| operator:
let pid = Std.Actor.spawn(:"Cure.Actor.Counter")
pid <-| :inc
pid <-| :inc
let current = Std.Actor.get_state(pid) # => 2
let _ = Std.Actor.stop(pid)
Inbox types (preview)
The type system already reserves Pid(Inbox) and Ref primitives. Pid alone elaborates to {:pid, :any}, the top of the covariant Pid family: everything accepted by any inbox is accepted by Pid, so existing FFI code keeps type-checking. The send-site checker clause unifies the message type against the receiver’s declared inbox and emits E046 Inbox Mismatch on conflict.
Supervisors
A sup container declares a supervisor module:
sup App.Root
strategy = :one_for_one
intensity = 3
period = 5
children
Counter as counter
Counter as counter_b (restart: :transient)
App.External as external (restart: :permanent, shutdown: 10000)
sup Workers as workers
Settings (strategy, intensity, period) default to :one_for_one, 3, and 5 respectively. The children block introduces one child spec per line.
Child specs take the form Module as child_id with an optional trailing parenthesised keyword list. The module path resolves with two conventions and one escape hatch:
-
A dotted path (
App.Gateway) is used verbatim and becomes the atom:"App.Gateway". -
A bare name (
Counter) resolves to:"Cure.Actor.Counter"by default (or:"Cure.Sup.Counter"for supervisor children). -
Prefix with the soft keyword
supto flip the default from worker to supervisor lookup:sup Workers as workers.
The supervisor compiler runs Cure.Sup.Verifier automatically. Verification enforces:
-
strategyis one of:one_for_one,:one_for_all,:rest_for_one,:simple_one_for_one. -
intensityis a non-negative integer;periodis a positive integer. - All child ids are unique within the supervisor.
-
Each
restart(if specified) is one of:permanent,:transient,:temporary. -
Each
shutdownis:brutal_kill,:infinity, or a positive integer. - The supervisor does not list itself as a direct child.
A verification failure stops codegen with a {:codegen_error, {:sup_verification_failed, errors}} result and surfaces under error codes E047, E048, and E050.
Soft keyword
sup is intentionally not reserved at the lexer level, so existing programs that use sup as a field name or local variable (for instance the superdiagonal row of a tridiagonal system in examples/cure_spline) keep compiling. The parser dispatches sup <Name> to the supervisor parser only at statement-prefix position when the next token is an identifier. In every other context, sup is a plain variable.
Runtime
Cure.Sup.Runtime wraps the compiled supervisor modules with a lazy ETS-backed registry so a tree can be reached by module atom. The table is created on first use:
{:ok, _pid} = Cure.Sup.Runtime.start(:"Cure.Sup.App.Root")
Cure.Sup.Runtime.which_children(:"Cure.Sup.App.Root")
:ok = Cure.Sup.Runtime.stop(:"Cure.Sup.App.Root")
From Cure, the equivalent is Std.Supervisor:
let tree = :"Cure.Sup.App.Root"
let _pid = Std.Supervisor.start(tree)
let kids = Std.Supervisor.which_children(tree)
let _ = Std.Supervisor.stop(tree)
Actor instances live in Cure.Actor.Runtime, a GenServer supervised by Cure.Supervisor and started automatically by the application. It tracks spawned actors in an ETS registry, monitors every pid, and cleans up on DOWN. Cure.Actor.Runtime.list_actors/0 returns every live actor for introspection.
Links, monitors, trap_exit
Std.Process exposes the raw BEAM process primitives directly:
mod MyApp.Pool
use Std.Process
fn watch(child: Pid) -> Ref =
let _ = link(child)
monitor(child)
The full surface:
-
link/1,unlink/1 -
monitor/1-> returns aRef -
demonitor/1 -
trap_exit/1(returns the previous value) -
exit/2 -
self/0,is_alive/1
monitor and trap_exit go through small wrappers in Cure.Process.Builtins so the Cure signatures can stay idiomatic ((Pid) -> Ref rather than the two-argument erlang BIFs). Everything else is a direct @extern(:erlang, ...) call.
Error codes
The new codes are catalogued in Cure.Compiler.Errors. Run cure explain <code> for the full text:
-
E045 Untyped Send –
<-|on a barePidin strict mode. - E046 Inbox Mismatch – message not a subtype of the receiver’s inbox ADT.
- E047 Supervisor Unknown Child – child resolves to no compiled module.
- E048 Supervisor Cycle – supervisor references itself transitively.
-
E049 Actor Handler Non-Exhaustive –
on_messagemisses an inbox variant. - E050 Invalid Supervisor Strategy – unknown strategy, restart, or shutdown value.
Full example
examples/cure_colony/cure_src/colony.cure ships with the release and exercises the whole surface: a worker actor, an echo actor, and a supervisor tree managing them under a :one_for_one strategy.
actor Worker with 0
on_start
(state) -> state
on_message
(:inc, n) -> n + 1
(:reset, _n) -> 0
(:get, n) ->
notify(%[:value, n])
n
actor Echo with nil
on_message
(msg, _payload) ->
notify(%[:echo, msg])
msg
sup Colony
strategy = :one_for_one
intensity = 3
period = 5
children
Worker as worker_a
Worker as worker_b (restart: :transient)
Echo as echo (restart: :permanent, shutdown: 2000)
See the on-disk reference docs/SUPERVISION.md for the full prose companion to this page.
See also
With Cure 0.26.0 the app container wraps an entire supervision tree into a first-class OTP application, and cure release packages it as a bootable BEAM release. Read the Applications page for the tour, or go straight to the on-disk reference docs/APP.md. The canonical end-to-end example is examples/cure_forge/: an app CureForge container, a sup Forge.Root tree, four cooperating actors, and a start-phase-driven cache warm-up.