Writing charts
A chart can be written as an SCXML document, a YAML document or Rust tokens in the statechart! macro. All three are read into the same chart IR, checked by the same validator and hashed to the same version. This page shows each surface, the same chart in all three, and the errors you get when a chart is wrong.
Several surfaces, one IR
The chart IR in vertep-core is the canonical chart: a tree of states with transitions, executable content, invokes and data, kept in document order. SCXML gives transition priority by document order, so the IR keeps that order as data. Every surface lowers to this IR, and every tool after that (the validator, the type checker, step, the RDF projection, the hash) works on the IR alone. So a chart behaves the same however it was written.
| Surface | Reader and writer | Default datamodel | Round trip |
|---|---|---|---|
SCXML 1.0 with vt: extensions | vertep-scxml (read, write) | null, as SCXML says | read(write(c)) == c for every chart the writer can represent |
| YAML 1.2 | vertep-yaml (read, write) | cel | read(write(c)) == c for every chart with unique state ids |
statechart! | vertep-macros, re-exported by vertep-dm-rust | rust (the only one it accepts) | lowers to the IR at compile time |
include_chart! | the SCXML and YAML readers, run inside the macro | must be rust | same typed API as statechart! |
| RDF (Turtle, TriG, N-Quads, N-Triples) | vertep-rdf | from the chart | see RDF and versions |
The vt command reads any of these files and picks the surface from the extension: .scxml or .xml, .yaml or .yml, .ttl, .trig, .nq, .nt. The commands are listed on The vt command.
SCXML
Vertep reads standard SCXML 1.0 documents and runs them with the W3C SCXML interpretation algorithm, checked against the W3C conformance suite (see Verification). Here is a small chart on the null datamodel, which has no data and accepts only In() conditions. Save it as door.scxml:
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" name="door" initial="closed">
<state id="closed">
<transition event="open" target="opened"/>
<transition event="lock" target="locked"/>
</state>
<state id="opened">
<transition event="close" target="closed"/>
</state>
<state id="locked">
<transition event="unlock" target="closed"/>
</state>
</scxml>
Check it, then convert it to YAML:
$ vt validate door.scxml
door.scxml: ok: urn:vertep:chart:sha256:1dc511b5d015deca8f320c136c262267c95b7e2cba6f76173318ded0951f63cd
$ vt convert --to yaml door.scxml
vertep: 1
name: door
datamodel:
value: "null"
implicit: true
initial: closed
states:
closed:
on:
open: opened
lock: locked
opened:
on:
close: closed
locked:
on:
unlock: closed
The YAML spells the datamodel as { value: "null", implicit: true }. The SCXML document did not name a datamodel, and YAML's default is cel, not null. The implicit flag records that the source said nothing, so converting back gives the same document and the same IR. Both files hash to the same version:
$ vt convert --to yaml door.scxml -o door.yaml
$ vt hash door.scxml
urn:vertep:chart:sha256:1dc511b5d015deca8f320c136c262267c95b7e2cba6f76173318ded0951f63cd
$ vt hash door.yaml
urn:vertep:chart:sha256:1dc511b5d015deca8f320c136c262267c95b7e2cba6f76173318ded0951f63cd
Reading SCXML from Rust
The vertep facade re-exports the reader as vertep::scxml. CompiledChart::new runs the well-formedness validator and builds the tables step needs. From examples/traffic-light/main.rs:
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Read the SCXML document into the chart IR, then validate and compile it.
let parsed = vertep::scxml::read_str(CHART)?;
let chart = CompiledChart::new(parsed.chart).map_err(|v| format!("invalid chart: {v:?}"))?;
// 2. Start a session. The chart names the `null` datamodel (no data, only `In()` guards).
let mut dm = NullDatamodel::new();
let mut timers: Vec<Timer> = Vec::new();
let mut now_ms = 0;
let start = StartInput {
info: SessionInfo {
sessionid: "light-1".into(),
..SessionInfo::default()
},
..StartInput::default()
};
let outcome = session::start(&chart, start, &mut dm).map_err(|e| format!("{e:?}"))?;
read_str uses default options, which accept only the null datamodel. For other datamodels pass a DatamodelSet, for example vertep_scxml::ReadOptions::default().with_datamodels(&vertep_scxml::AnyDatamodel), as crates/vertep-dm-rust/tests/rust_dsl.rs does. A src attribute on <script>, <data>, <content> or <invoke> is read only through a SourceResolver you supply; with none, every src is an error. The reader does no I/O of its own.
What SCXML loses, and the loss report
Reading returns the chart and a LossReport: everything the IR does not keep. Some losses are formatting (comments, processing instructions, the XML declaration, whitespace). Others are content: an unknown unprefixed attribute, an extension on an element that has no slot for one, a resolved src that was inlined. ReadOptions::strict(true) turns a non-empty report into an error. vt convert refuses a conversion that would lose content, and writes nothing:
$ vt convert --to yaml lossy.scxml
lossy.scxml: refusing to convert scxml to yaml: it would lose:
- lossy.scxml:2:3: attribute `color` is not defined on <state>
A chart with only formatting losses converts. examples/traffic-light/traffic-light.scxml has comments and converts to YAML without complaint.
The vt: extension namespace
Vertep features ride in the namespace https://w3id.org/vertep/vt#, usually bound to the prefix vt. A vt: attribute or element becomes an extension node on its owner in the IR; it is not dropped. Elements in any other foreign namespace are kept the same way and listed in the loss report. A document with vt: extensions is still SCXML, so another SCXML processor can run it and ignore them. The extensions in use today, and where each is explained:
| Extension | Where | What it does | Read by |
|---|---|---|---|
<vt:types> | child of <scxml> | declares named types and event payload types | types |
vt:type | <data>, <send>, <param> | the type of a data item, a send payload or a parameter | types |
vt:defer="order.*" | state | matching external events wait in the mailbox while the state is active | runtime |
vt:unhandled | <scxml> | discard, dead-letter or error for events no transition takes | runtime |
vt:restart, vt:restart-limit, vt:restart-window, vt:restart-backoff | <invoke> | restart policy (on-failure, always) for an invoked actor | runtime |
<vt:request> | state | request and reply with a timeout, lowered to a send, a waiting substate and two transitions | runtime |
<vt:reminder>, <vt:migrate> | executable content | a durable periodic reminder; a request to migrate to another chart version | runtime |
vt:decide and vt:candidates, vt:fallback, vt:deadline, vt:maxDecisions | state | a choice point where a decider picks one enabled event | agents |
<invoke type="vt:llm">, <vt:grant>, <vt:call> | invoke, state, executable content | LLM actors, capability grants, plan-chart calls | agents |
The runtime lowers <vt:request>, <vt:reminder> and <vt:migrate> into plain SCXML before compiling (see crates/vertep-runtime/src/chart.rs). The chart version is computed on the chart as written, before that lowering.
YAML
The YAML surface is a thin mirror of the IR. States are keyed by id under states, and key order is document order. A document starts with vertep: 1 (the format version, required) and name. The reader is built on a YAML 1.2 parser that keeps key order and reports a line and column for every node. YAML 1.1 parsers were rejected because they read on as the boolean true, and on is the key of every state's transitions.
This is examples/typed-order/order.yaml, a typed chart on the CEL datamodel:
# A typed order chart in YAML on the CEL datamodel.
#
# `types` names record and union types, `events` gives every event its payload type and
# `context` gives every data item a type and an initial value. Guards and assignments are CEL
# expressions: `ctx` is the context, `ev.data` the event payload. `vt typecheck` checks every
# expression against these declarations before the chart ever runs.
#
# Illustrates the type-system capability (named types, typed events and context, Command Line
# Type Tools), yaml-format and cel-datamodel.
vertep: 1
name: order
datamodel: cel
types:
Item: { sku: string, qty: int, price: decimal }
Pay: { amount: decimal, method: { oneOf: { Card: { last4: string }, Invoice: null } } }
Ship: { carrier: string, tracking: "optional<string>" }
events:
add: Item
checkout: null
pay: Pay
approve: null
ship: Ship
cancel: null
context:
items: { type: "list<Item>", init: "[]" }
total: { type: decimal, init: "0" }
limit: { type: decimal, init: "500" }
initial: shopping
states:
shopping:
on: { cancel: cancelled }
initial: cart
states:
cart:
on:
add:
- cond: "ev.data.qty > 0"
actions:
- assign: { location: ctx.items, expr: "ctx.items + [ev.data]" }
- assign: { location: ctx.total, expr: "ctx.total + ev.data.price * decimal(ev.data.qty)" }
checkout:
- { cond: "size(ctx.items) > 0", target: payment }
payment:
on:
pay:
- { cond: "ev.data.amount < ctx.total", target: payment }
- { cond: "ctx.total > ctx.limit", target: review }
- { target: fulfilment }
review:
on: { approve: fulfilment }
fulfilment:
on: { ship: shipped }
shipped: { type: final }
cancelled: { type: final }
The keys a state takes, from crates/vertep-yaml/src/lib.rs:
| Key | Meaning |
|---|---|
type | parallel, final or history (with history: shallow|deep); absent means a plain state |
initial | a child id, or a list of ids |
on | event name to transition. A transition is a target string, a list of targets, or a mapping with target, cond, type and actions. A list of mappings is several transitions, in order. |
always | eventless transitions, in the same forms |
entry, exit | executable content |
invoke, context, donedata, states | invokes, state-local data, done data, children |
Executable content is a list of mappings, each with exactly one key: assign, raise, send, log, if, foreach, cancel, script, call (a vt:call element) or extension (any other extension element). At the top level, types, events and context carry the types; Datamodels and types covers them.
Quote "null" when you mean the null datamodel. Unquoted, YAML reads it as a null value:
$ vt validate dn.yaml
dn.yaml:3:12: datamodel: expected a string, found nullThe writer is canonical and byte-stable. Spellings such as implicit: true, generated: true on generated state ids, a transitions list that keeps an order on grouping would change, and extensions lists on every node exist so that no IR distinction is lost.
The statechart! macro
statechart! writes a chart in Rust tokens on the rust datamodel. Guards and actions are ordinary Rust functions, bound by name. The macro runs inside the compiler: it lowers the tokens to the IR, runs the same well-formedness validator and type checker the runtime uses, reports unreachable states and unhandled events, and generates a typed API. Every finding is a compile error or warning at the offending tokens. The macros are re-exported by vertep-dm-rust, so a crate needs only that dependency.
This is examples/rust-macro/main.rs, a turnstile:
use vertep_dm_rust::{MachineError, statechart};
statechart! {
name: turnstile,
types { Coin = { cents: int } }
context { credit: int = 0, price: int = 50, passes: int = 0 }
events { coin: Coin, push, shutdown }
initial service;
state service {
initial locked;
on shutdown => off;
state locked {
// Targetless: stay locked and count the coin.
on coin / add_credit;
// Eventless: unlock as soon as there is enough credit.
always if paid => unlocked;
}
state unlocked {
on coin / add_credit;
on push => locked / let_through;
}
}
final off;
}
/// Guard: guards get the context and the payload read-only, and return a bool.
fn paid(ctx: &Ctx, _: &()) -> bool {
ctx.credit >= ctx.price
}
/// Action: actions may change the context; `Emit` would let them raise or send events.
fn add_credit(ctx: &mut Ctx, coin: &Coin, _: &mut Emit) {
ctx.credit += coin.cents;
}
/// Action without an emitter: the shorter signature is accepted too.
fn let_through(ctx: &mut Ctx, _: &()) {
ctx.credit -= ctx.price;
ctx.passes += 1;
}
fn main() -> Result<(), MachineError> {
println!("chart version {}", *VERSION);
// `Machine::start` runs the start macrostep; the effects (none here) are returned.
let (mut m, _effects) = Machine::start("turnstile-1")?;
show("(start)", &m);
let script = [
Event::Coin { cents: 20 },
Event::Push, // nothing happens: the turnstile is locked
Event::Coin { cents: 50 },
Event::Push,
Event::Push,
Event::Shutdown,
];
for event in script {
let label = format!("{event:?}");
m.send(event)?;
show(&label, &m);
}
assert!(m.is_terminated(), "`off` is a top-level final state");
Ok(())
}
$ cargo run -p vertep-examples --bin rust-macro
chart version urn:vertep:chart:sha256:0b1614ef361262f9f6fec5189fa5834756f37808a3a466f81a9f83d6ed20e868
(start) -> [Service, Locked] credit=0 passes=0
Coin { cents: 20 } -> [Service, Locked] credit=20 passes=0
Push -> [Service, Locked] credit=20 passes=0
Coin { cents: 50 } -> [Service, Unlocked] credit=70 passes=0
Push -> [Service, Locked] credit=20 passes=1
Push -> [Service, Locked] credit=20 passes=1
Shutdown -> [] credit=20 passes=1
The grammar
The full grammar is at the top of crates/vertep-macros/src/parse.rs. In short:
| Item | Form |
|---|---|
| chart header | name: order, (required) · datamodel: rust, · binding: early|late, |
| lint attributes | #[deny(unreachable_states)], #[allow(unhandled_events)] (both warn by default) |
| declarations | types { Pay = { amount: decimal } } · context { total: decimal = 0 } · events { start, pay: Pay } |
| states | state s; · state s { … } · parallel p { … } · final f; · history h deep => t; |
| state body | initial a; · data { … } · entry a, b; · exit a; · invoke "type" src "…" id "…" autoforward; |
| transitions | on pay if positive => payment / record_total; · always if ready => running; · targetless on coin / add_credit; · internal or external before / |
| guards | if name, if !name, if In(state) |
| actions | a function name · raise e · send e to "…" after "…" type "…" id "…" · log "…" · cancel "…" |
A guard has the signature fn(&Ctx, &P) -> bool, where P is the event's payload type (() for none). It gets no mutable context and no emitter, so it cannot change anything. An action is fn(&mut Ctx, &P, &mut Emit), fn(&mut Ctx, &P) or fn(&mut Ctx, &P) -> Vec<E>; its only effects are context changes and emitted events. A function with the wrong signature is a type error at the guard's or action's name.
The macro generates, next to the chart: Ctx (the context struct), Event (one variant per event, carrying its payload fields), State (one variant per state, with parent(), children() and contains()), a struct per named type, CHART (the IR), VERSION (the chart version IRI), NAME, bind() (a registry of the chart's functions) and Machine, a typed session with start, send, active, is_in, context and is_terminated.
include_chart!
include_chart!("tests/charts/order.scxml") reads an SCXML (.scxml, .xml) or YAML (.yaml, .yml) file at compile time, relative to the crate's CARGO_MANIFEST_DIR, and generates the same API as statechart!. The crate is rebuilt when the file changes. The file must use the rust datamodel. Charts on other datamodels are loaded at run time with their readers instead.
One chart in three surfaces
The test order_chart_is_the_same_from_every_surface in crates/vertep-dm-rust/tests/rust_dsl.rs writes one order chart three times, and asserts that all three give the same IR, the same API and the same version IRI, and run the same. In the macro:
statechart! {
name: order,
datamodel: rust,
types {
Pay = { amount: decimal },
Approval = { by: iri },
}
context { total: decimal = 0, limit: decimal = 100 }
events { start, pay: Pay, authorized, approve: Approval, cancel }
initial idle;
state idle { on start => checkout; }
state checkout {
initial cart;
on cancel => cancelled;
state cart {
on pay if positive => payment / record_total;
}
state payment {
on authorized if over_limit => review;
on authorized => paid;
}
state review { on approve => paid; }
}
final paid;
final cancelled;
}
With these functions, shared by all three:
pub fn positive(_: &Ctx, ev: &Pay) -> bool {
ev.amount > Decimal::ZERO
}
pub fn over_limit(ctx: &Ctx, _: &()) -> bool {
ctx.total > ctx.limit
}
pub fn record_total(ctx: &mut Ctx, ev: &Pay, _: &mut Emit) {
ctx.total = ev.amount;
}
In YAML, crates/vertep-dm-rust/tests/charts/order.yaml:
vertep: 1
name: order
datamodel: rust
types:
Pay:
amount: decimal
Approval:
by: iri
events:
start: null
pay: Pay
authorized: null
approve: Approval
cancel: null
context:
total:
type: decimal
init: "0"
limit:
type: decimal
init: "100"
initial: idle
states:
idle:
on:
start: checkout
checkout:
initial: cart
on:
cancel: cancelled
states:
cart:
on:
pay:
cond: "vt:guard/order/positive"
target: payment
actions:
- foreach:
array: "vt:action/order/record_total"
item: _vt_emit
do:
- send:
eventexpr: _vt_emit.event
targetexpr: _vt_emit.target
typeexpr: _vt_emit.type
delayexpr: _vt_emit.delay
content:
expr: _vt_emit.data
payment:
on:
authorized:
- cond: "vt:guard/order/over_limit"
target: review
- target: paid
review:
on:
approve: paid
paid:
type: final
cancelled:
type: final
In SCXML, crates/vertep-dm-rust/tests/charts/order.scxml:
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" xmlns:vt="https://w3id.org/vertep/vt#" version="1.0" name="order" datamodel="rust" initial="idle">
<vt:types xmlns:vt="https://w3id.org/vertep/vt#">type Pay = { amount: decimal };
type Approval = { by: iri };
event start;
event pay: Pay;
event authorized;
event approve: Approval;
event cancel;
</vt:types>
<datamodel>
<data id="total" expr="0" vt:type="decimal"/>
<data id="limit" expr="100" vt:type="decimal"/>
</datamodel>
<state id="idle">
<transition event="start" target="checkout"/>
</state>
<state id="checkout" initial="cart">
<transition event="cancel" target="cancelled"/>
<state id="cart">
<transition event="pay" cond="vt:guard/order/positive" target="payment">
<foreach array="vt:action/order/record_total" item="_vt_emit">
<send eventexpr="_vt_emit.event" targetexpr="_vt_emit.target" typeexpr="_vt_emit.type" delayexpr="_vt_emit.delay">
<content expr="_vt_emit.data"/>
</send>
</foreach>
</transition>
</state>
<state id="payment">
<transition event="authorized" cond="vt:guard/order/over_limit" target="review"/>
<transition event="authorized" target="paid"/>
</state>
<state id="review">
<transition event="approve" target="paid"/>
</state>
</state>
<final id="paid"/>
<final id="cancelled"/>
</scxml>
The SCXML and YAML show how the rust datamodel appears in the IR. A guard is a cond holding an IRI, vt:guard/<chart>/<function>. An action is a <foreach> whose array is the action's IRI, vt:action/<chart>/<function>. Evaluating the array runs the action, which may change the context, and yields the events it emitted; the body sends each one. This keeps every effect of an action visible in the IR and in the step output, without any change to the interpreter. The rules are in crates/vertep-dm-rust/src/lowering.rs.
$ vt hash order.scxml
urn:vertep:chart:sha256:05bdae901f2ef328f81ad649674bac2beab559eac46bae2dbc34db1bdcd2e38f
$ vt hash order.yaml
urn:vertep:chart:sha256:05bdae901f2ef328f81ad649674bac2beab559eac46bae2dbc34db1bdcd2e38f
The macro's VERSION constant is the same IRI; the test checks it.
The datamodel is part of the chart, so it is part of the version. The same states written once with CEL expressions and once with Rust functions are two charts with two versions.
Validation and well-formedness
A chart passes several checks before it can run. All of them report every error they find, with a location, instead of stopping at the first.
| Check | Where | Finds |
|---|---|---|
| Reading | vertep-scxml, vertep-yaml, the macro parser | syntax, unknown elements and keys, missing or exclusive attributes, duplicate state ids, unknown targets |
| Well-formedness | vertep_core::validate, run by CompiledChart::new | the structural rules of SCXML (table below) |
| SHACL shapes | vertep-rdf, run by vt validate | the same rules, stated over the RDF projection |
| Types | vertep_core::types::check_chart, run by vt typecheck, the loaders and the macro | payload, context and expression types (see types) |
A typo and a duplicate id, in SCXML:
$ vt validate door-typo.scxml; echo "exit $?"
door-typo.scxml:3:5: transition target names unknown state `opend`
door-typo.scxml:5:3: state id `closed` is already declared at 2:3
exit 1
The same typo in YAML. The YAML reader gives the dotted path of the node and suggests the closest state id:
$ vt validate door-typo.yaml
door-typo.yaml:8:13: states.closed.on.open.target: unknown target `opend`; did you mean `opened`?
Structural errors come from the core validator, and vt validate also checks the chart's RDF projection against the SHACL shapes. This chart puts only a history state in closed, a transition in a final state, and <donedata> in a state that is not final:
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" name="door" initial="closed">
<state id="closed">
<history id="h"/>
<transition event="open" target="opened"/>
</state>
<final id="opened">
<transition event="close" target="closed"/>
</final>
<state id="x"><donedata><content expr="1"/></donedata><state id="y"/></state>
</scxml>
$ vt validate door-shapes.scxml
door-shapes.scxml:2:3: StateHistoryOnly at State { id: "closed" }
door-shapes.scxml:6:3: FinalContent { what: "transitions" } at State { id: "opened" }
door-shapes.scxml:9:3: DoneDataOutsideFinal at State { id: "x" }
door-shapes.scxml: shapes: <urn:vertep:chart:sha256:dcf27340493a3568d3813f8b980134e8e4475d58cbc59e85e9da0c3be3a3c807#s.opened> (vt:child|vt:transition|vt:invoke|vt:data|vt:initial|vt:initialElement): a final state has only onentry, onexit and donedata (MaxCountConstraintComponent)
door-shapes.scxml: shapes: <urn:vertep:chart:sha256:dcf27340493a3568d3813f8b980134e8e4475d58cbc59e85e9da0c3be3a3c807#s.closed> = <urn:vertep:chart:sha256:dcf27340493a3568d3813f8b980134e8e4475d58cbc59e85e9da0c3be3a3c807#s.closed>: a state's children cannot all be history states (OrConstraintComponent)
door-shapes.scxml: shapes: <urn:vertep:chart:sha256:dcf27340493a3568d3813f8b980134e8e4475d58cbc59e85e9da0c3be3a3c807#s.x> vt:donedata: donedata belongs to final states (MaxCountConstraintComponent)
vt validate prints core violations by their internal names (StateHistoryOnly, FinalContent), not as sentences. The table below says what each name means.
| Violation | Meaning |
|---|---|
EmptyChart | the chart has no states |
DuplicateStateId, DuplicateDataId | two states, or two data items, share an id |
UnknownTarget | a target names no state |
IllegalInitialTarget | an initial target is not a proper descendant of its state |
InitialAttributeAndElement, InitialOnNonCompound, IllegalInitialTransition | both an initial attribute and element; an initial on a state without children; an <initial> transition with an event, a condition or no target |
HistoryAtRoot, HistoryContent, HistoryTransitionCount, IllegalHistoryTransition, IllegalHistoryTarget, HistoryDefaultCycle | misplaced history states, history states with content or several transitions, a default transition that leaves the parent or loops back |
FinalContent, DoneDataOutsideFinal | a final state with anything but onentry, onexit and donedata; donedata outside a final state |
ParallelHistoryOnly, StateHistoryOnly | a state whose only children are history states |
IllegalTargetSet | the targets of one transition cannot be active together |
ConflictingAttributes | attributes or children that exclude each other, or a missing required one |
Errors from the macro
In the macro, the same checks become compiler errors at the right span. These are the recorded outputs of the compile-fail tests in crates/vertep-dm-rust/tests/ui/. A misspelled target:
statechart! {
name: order,
events { pay }
initial cart;
state cart { on pay => paymnt; }
state payment;
}
error: unknown target state `paymnt`; did you mean `payment`?
--> tests/ui/unknown_target.rs:7:28
|
7 | state cart { on pay => paymnt; }
| ^^^^^^
An event declared under #[deny(unhandled_events)] that no transition handles:
error: event `refund` is declared but no transition handles it
--> tests/ui/unhandled_event.rs:6:19
|
6 | events { pay, refund }
| ^^^^^^
A guard that asks for &mut Ctx, which guards may not have:
error[E0631]: type mismatch in function arguments
--> tests/ui/guard_mut_ctx.rs:8:28
|
8 | state open { on pay if counted => paid; }
| ^^^^^^^ expected due to this
...
12 | fn counted(ctx: &mut Ctx, _: &()) -> bool {
| ----------------------------------------- found signature defined here
|
= note: expected function signature `for<'a, 'b> fn(&'a Ctx, &'b ()) -> _`
found function signature `fn(&mut Ctx, &()) -> _`
= note: required for `for<'a, 'b> fn(&'a mut Ctx, &'b ()) -> bool {counted}` to implement `GuardFn<Ctx, ()>`
note: required by a bound in `Registry::<C>::guard`
--> src/registry.rs
|
| pub fn guard<P, F>(&mut self, iri: &str, f: F) -> Result<&mut Self, RegistryError>
| ----- required by a bound in this associated function
...
| F: GuardFn<C, P>,
| ^^^^^^^^^^^^^ required by this bound in `Registry::<C>::guard`
help: consider wrapping the function in a closure
|
8 | state open { on pay if |arg0: &Ctx, arg1: &()| counted(&mut *arg0, arg1) => paid; }
| +++++++++++++++++++++++ ++++++++++++++++++