Std.Otp.Raw
View source →SEALED unsafe raw base for the typed BEAM process algebra.
Honest, permissive, effect-typed externs over stock Erlang/OTP BIFs — the
only trust boundary for concurrency. Std.Otp narrows these into a typed
algebra (Pid(m), session channels) and is the sanctioned surface; nothing
here is meant for direct use. Every operation that performs a BEAM side
effect returns Effect(T) (design 2026-07-09-effect-type-former §3.3,
2026-07-10-checked-beam-concurrency §2), which the old pure @extern types
(the "purity lie") did not.
The effect discipline forbids duplicating, dropping, or reordering an OPERATION — that is a statement about the PROGRAM'S EFFECT SEQUENCE, not about the mailbox. The BEAM itself guarantees far less, and nothing here may assume otherwise:
- NO DELIVERY. A
sendto a dead process silently succeeds. Nothing here promises a message arrives, and no typed op sequences on arrival. - ORDERING IS PAIRWISE ONLY. Signals from the SAME sender to the SAME target arrive in order — messages and exit signals alike. Order between DIFFERENT senders is unspecified.
(Bereczky/Horpácsi/Thompson, A Formalisation of Core Erlang, Thm. 2 and Ex. 3; verified against AtomVM in https://github.com/cure-lang/cure-otp/tree/main/docs/research/process-types/raw-algebra-conformance-checklist.md.)
Messages and replies are POLYMORPHIC, not Any: the type variable is the
narrow point where Std.Otp supplies the message-set code m. Payloads must
be first-order serialisable data (the BEAM copies messages); the typed layer
enforces that.
Process creation receives a function, never a bare Effect value. The
function boundary is the honest raw representation of a fresh BEAM
process; typed wrappers choose the indexed handle exposed to callers.
Which BEAM protocol a process handle speaks — a PHANTOM TAG, carried as RawPid's
third type argument. A plain process answers only raw sends; a gen_server
additionally answers call/cast/stop. Neither tag has values and neither
reaches the BEAM: the distinction is erased entirely.
Types
-
type NoMessageAn uninhabited message type.
BarePidcarries it, which is what makes a pid recovered from the registry UNSENDABLE:tellwould demand aNoMessagevalue and no constructor produces one. -
type BarePidA process handle with NO message-type claim — what a registry lookup can honestly give you. Supervisable (
link/monitor/exit/is_alivenever cared about the message type) but not sendable. Recovering a TYPED handle from a name needs a name-to-code association nothing builds yet; that is F-1 (see the audit, §6).
Functions
-
# fn raw_actor_server(m: Type, q: Type, r: Type, index: Int, checked: Tuple(BarePid, Atom)) -> RawServerPid(m, q, r) extern
Attach erased actor protocol indices to an already PID-validated carrier. This is the construction trust point used by Std.Otp.start_actor; it does not accept an arbitrary BeamTerm.
-
# fn raw_call(q: Type, r: Type, server: RawPid(q, r, Server), req: q) -> Effect(r) extern
Synchronous
gen_servercall — the typedStd.Otpsurface narrows this raw, return-polymorphic boundary.Server-only: calling a plain process blocks the caller for 5s and then exits it.⚠ PARTIAL. On timeout (default 5000 ms) or server death the CALLER EXITS. No value is returned at the wrong type — there is simply no continuation — so
Effect(r)is sound but not total. Atry_callthat reifies the failure needs a try/catch shim and is deferred (audit §6). -
# fn raw_call_dep(q: Type, kr: Type, res: Type, server: RawPid(q, kr, Server), req: q) -> Effect(res) extern
Dependent-reply variant of
raw_call: the server's reply-index slotkrand the RESULTresare DECOUPLED, so the caller may chooseres = ReplyOf(request)— a function of the EXPLICIT request. Same BIF (gen_server:call/2), same partiality/trust asraw_call;Std.Otp.call_depthreads it. -
# fn raw_cancel_timer(ref: TimerRef) -> Effect(Int | Bool) extern
Cancel a timer.
erlang:cancel_timer/1returns the REMAINING MILLISECONDS, orfalseonce the timer has already fired or been cancelled. It is notUnitand never was — the sharpest of theEffect(Unit)lies. -
# fn raw_cast(q: Type, r: Type, server: RawPid(q, r, Server), msg: q) -> Effect(Atom) extern
Asynchronous
gen_servercast — no reply.Server-only: a plain process does not implement the gen_server protocol. Returns the atomok. -
# fn raw_demonitor(ref: Ref) -> Effect(Bool) extern
Remove a monitor. Returns
true. Honest over the bareRef—erlang:demonitor/1accepts any reference — and drives no typed wrapper: the typeddemonitorflushes. -
# fn raw_demonitor_flush(ref: MonitorRef, opts: List(Atom)) -> Effect(Bool) extern
erlang:demonitor/2with[flush]— removes the monitor AND discards aDOWNthat has already been delivered. The option list is a real parameter, not baked in. Withflushand noinfo, the BIF always returnstrue. -
# fn raw_dep_actor_base(m: Type, q: Type, rep: Function(q, Type), index: Int, checked: Tuple(RawDepServerPid(m, q, rep), Atom)) -> RawServerPid(m, q, Unit) extern
-
# fn raw_dep_actor_server(m: Type, q: Type, rep: Function(q, Type), index: Int, checked: Tuple(RawServerPid(m, q, Unit), Atom)) -> RawDepServerPid(m, q, rep) extern
-
# fn raw_exit(m: Type, r: Type, k: Type, x: Type, pid: RawPid(m, r, k), reason: x) -> Effect(Bool) extern
Send an exit signal and query process liveness.
erlang:exit/2returnstrue. -
# fn raw_fsm_pid(event: Type, state: Type, data: Type, index: Int, checked: Tuple(BarePid, Atom)) -> RawFsmPid(event, state, data) extern
-
# fn raw_is_alive(m: Type, r: Type, k: Type, pid: RawPid(m, r, k)) -> Effect(Bool) extern
-
# fn raw_link(m: Type, r: Type, k: Type, pid: RawPid(m, r, k)) -> Effect(Bool) extern
Link and unlink the current process to a target. Both return
true. -
# fn raw_monitor(m: Type, r: Type, k: Type, kind: Atom, pid: RawPid(m, r, k)) -> Effect(MonitorRef) extern
Establish a monitor on
pid; the returnedMonitorReftags theDOWNmessage. -
# fn raw_name_register(m: Type, name: Atom, pid: RawPid(m, m, Plain)) -> Effect(Bool) extern
── Name: typed process registration (the F-1 typed name → handle) ──
name_whereisreturns a TYPEDOption(RawPid(m,m,Plain))— a sendable handle — instead of the untyped bare pidraw_whereisgives, closing F-1: the message typemrides on the caller'sStd.Otp.Name(m)(the trust point, as in Gleam'sName(message)). Register/unregister are Bool-safe via the helper. AtomVM caveat: needserlang:register/whereis; confirm support before relying on it there (host OTP is fine). -
# fn raw_name_unregister(name: Atom) -> Effect(Bool) extern
-
# fn raw_name_whereis(m: Type, name: Atom) -> Effect(Option(RawPid(m, m, Plain))) extern
-
# fn raw_register(m: Type, r: Type, k: Type, name: Atom, pid: RawPid(m, r, k)) -> Effect(Bool) extern
The registry operations deliberately remain raw and effect-typed. A higher layer may expose a name-indexed handle without asserting that a dynamic registry lookup is always successful.
register/unregisterboth answer the constant atomtrue— there is no failure they report by value (they raise instead). -
# fn raw_reply(r: Type, from: Tuple(RawPid(NoMessage, NoMessage, Plain), Ref), value: r) -> Effect(Atom) extern
Reply explicitly to a
gen_servercaller. The public wrapper gives the callback-sidefromvalue a linear binder before consuming it. -
# fn raw_selector_new(p: Type) -> Selector(p) extern
-
# fn raw_selector_receive(p: Type, selector: Selector(p), timeout: Int) -> Effect(Option(p)) extern
-
# fn raw_selector_select_map(p: Type, m: Type, selector: Selector(p), subject: Tuple(RawPid(m, m, Plain), Ref), transform: Function(m, p)) -> Selector(p) extern
-
# fn raw_self(m: Type) -> Effect(RawPid(m, m, Plain)) extern
The calling process's own pid. A process the VM spawned for us speaks no gen_server protocol, so the handle is
Plain. -
# fn raw_send(m: Type, r: Type, k: Type, dest: RawPid(m, r, k), msg: m) -> Effect(m) extern
Send
msgtodest. Fire-and-forget; the effect is performing the send. Tag-polymorphic: a raw send to a gen_server is legitimate — it lands inhandle_info.erlang:send/2returns the MESSAGE, notok. -
# fn raw_send_after(m: Type, r: Type, k: Type, delay: Int, pid: RawPid(m, r, k), msg: m) -> Effect(TimerRef) extern
Schedule a message for a process and receive an opaque timer reference.
-
# fn raw_server_call(m: Type, q: Type, r: Type, server: RawServerPid(m, q, r), req: q) -> Effect(r) extern
-
# fn raw_server_call_dep(m: Type, q: Type, kr: Type, res: Type, server: RawServerPid(m, q, kr), req: q) -> Effect(res) extern
-
# fn raw_server_cast(m: Type, q: Type, r: Type, server: RawServerPid(m, q, r), msg: m) -> Effect(Atom) extern
-
# fn raw_server_stop(m: Type, q: Type, r: Type, pid: RawServerPid(m, q, r)) -> Effect(Atom) extern
-
# fn raw_spawn(m: Type, thunk: Function(Unit)) -> Effect(RawPid(m, m, Plain)) extern
-
# fn raw_spawn_link(m: Type, thunk: Function(Unit)) -> Effect(RawPid(m, m, Plain)) extern
-
# fn raw_start_link(a: Type, name: Tuple(Atom, Atom), module: Atom, args: a, opts: List(Atom)) -> Effect(Tuple) extern
Start a gen_server through its standard callback module. The raw return is deliberately the OTP
{ok, pid}/error tuple; a behavior-specific wrapper narrows that contract after the callback context is established.⚠ The return is typed
Effect(Tuple), which is honest on AtomVM (whose gen_server init result is{ok, State} | {stop, Reason}) but NOT on OTP, where aninit/1returningignoremakesstart_linkreturn the BARE ATOMignore. Typing that honestly means producing a TYPED handle from an untyped BEAM tuple — the same unfounded assertion as F-1, and deferred with it (audit §6). This applies to every op in the family below. -
# fn raw_start_link_term(a: Type, module: Atom, args: a, opts: List(Atom)) -> Effect(RawTerm) extern
Honest inbound form: unlike the compatibility Tuple signature, this admits every OTP outcome (
{:ok,pid},{:error,reason}, or:ignore). -
# fn raw_start_link_unnamed(a: Type, module: Atom, args: a, opts: List(Atom)) -> Effect(Tuple) extern
-
# fn raw_statem_cast(event: Type, state: Type, data: Type, server: RawFsmPid(event, state, data), value: event) -> Effect(Atom) extern
-
# fn raw_statem_start_link(a: Type, name: Tuple(Atom, Atom), module: Atom, args: a, opts: List(Atom)) -> Effect(Tuple) extern
Start a gen_statem through its standard callback module. The raw return is deliberately the OTP
{ok, pid}/error tuple; the transparent FSM macro supplies the behavior-specific callback contract. -
# fn raw_statem_start_link_term(a: Type, module: Atom, args: a, opts: List(Atom)) -> Effect(RawTerm) extern
-
# fn raw_statem_start_link_unnamed(a: Type, module: Atom, args: a, opts: List(Atom)) -> Effect(Tuple) extern
-
# fn raw_stop(q: Type, r: Type, pid: RawPid(q, r, Server)) -> Effect(Atom) extern
Stop a generic OTP process. The reason is intentionally polymorphic at this raw boundary; typed lifecycle wrappers choose the narrower reason type required by their behavior.
Server-only, like the rest of the gen_server protocol. Returns the atomok. -
# fn raw_subject_new(m: Type) -> Effect(Tuple(RawPid(m, m, Plain), Ref)) extern
── Subject: a typed message ADDRESS
{owner_pid, tag_ref}(Gleam-style, F-1 addressing) ──A subject is a fresh
(pid, unique reference)pair. A send delivers{tag, message}to the owner's mailbox and a receive matches THAT tag, so one process may own several subjects of different message types and receive each independently — the decouplingPid(m)(one message type per process) cannot give. The taggedreceive … after … endis not a BIF, so these route through theCure.Otp.Builtinsruntime helper;Std.Otpnarrows them into the typedSubject(m)surface. Ordinary effect discipline (each op isEffect). -
# fn raw_subject_receive(m: Type, subject: Tuple(RawPid(m, m, Plain), Ref), timeout: Int) -> Effect(Option(m)) extern
-
# fn raw_subject_send(m: Type, subject: Tuple(RawPid(m, m, Plain), Ref), message: m) -> Effect(Atom) extern
-
# fn raw_supervisor_pid(index: Int, checked: Tuple(BarePid, Atom)) -> RawSupervisorPid extern
-
# fn raw_supervisor_start_link(a: Type, name: Tuple(Atom, Atom), module: Atom, args: List(a)) -> Effect(Tuple) extern
-
# fn raw_supervisor_start_link_term(a: Type, name: Tuple(Atom, Atom), module: Atom, args: List(a)) -> Effect(RawTerm) extern
Honest supervisor startup result, including OTP's bare
ignoreoutcome. -
# fn raw_supervisor_stop(pid: RawSupervisorPid, reason: Atom, timeout: Int) -> Effect(Atom) extern
-
# fn raw_term(a: Type, index: Int, boxed: Tuple(a, Atom)) -> RawTerm extern
-
# fn raw_term_atom(index: Int, checked: Tuple(RawTerm, Atom)) -> Atom extern
-
# fn raw_term_binary(index: Int, checked: Tuple(RawTerm, Atom)) -> Binary extern
The guard that admits this extraction is
raw_term_is_binary, so what comes out is aBinary— not a CureString, which is nominal and erases to{String, code_points}.Std.Beam.decode_stringdecodes the binary. -
# fn raw_term_boolean(index: Int, checked: Tuple(RawTerm, Atom)) -> Bool extern
-
# fn raw_term_float(index: Int, checked: Tuple(RawTerm, Atom)) -> Float extern
-
# fn raw_term_integer(index: Int, checked: Tuple(RawTerm, Atom)) -> Int extern
-
# fn raw_term_is_atom(term: RawTerm) -> Bool extern
Safe inbound observation floor. Predicates never refine by themselves; the representation-preserving projections below are used only after the corresponding predicate succeeds in Std.Beam.
-
# fn raw_term_is_binary(term: RawTerm) -> Bool extern
-
# fn raw_term_is_boolean(term: RawTerm) -> Bool extern
-
# fn raw_term_is_float(term: RawTerm) -> Bool extern
-
# fn raw_term_is_integer(term: RawTerm) -> Bool extern
-
# fn raw_term_is_pid(term: RawTerm) -> Bool extern
-
# fn raw_term_is_tuple(term: RawTerm) -> Bool extern
-
# fn raw_term_pid(index: Int, checked: Tuple(RawTerm, Atom)) -> BarePid extern
-
# fn raw_term_tuple_element(index: Int, term: RawTerm) -> RawTerm extern
-
# fn raw_term_tuple_size(term: RawTerm) -> Int extern
-
# fn raw_unlink(m: Type, r: Type, k: Type, pid: RawPid(m, r, k)) -> Effect(Bool) extern
-
# fn raw_unregister(name: Atom) -> Effect(Bool) extern
-
# fn raw_whereis(name: Atom) -> Effect(BarePid | :undefined) extern
whereisreturns the bare atomundefinedwhen the name is not registered — that is the BIF's real contract, and typing it as a pid would let a well-typed handle BE the atomundefined. The FFI boundary re-tags the union::undefinedby exact value, the pid by theis_pidguard@erases(:pid)declares.Std.Otp.whereisturns that into anOption.