until!() { /* proc-macro */ }Expand description
Creates a ControlExpr from a boolean expression, allowing asynchronous
code to efficiently await a specific condition.
This macro is a cornerstone of the odem-rs reactive system. It parses the
provided expression, identifies parts of it that refer to reactive data
sources (termed “control variables”), and constructs a ControlExpr.
ControlExpr implements [IntoFuture], meaning its result can be directly
.awaited. The future will be ready when the boolean expression evaluates
to true. The underlying mechanism uses the Waker associated with the
current process to re-evaluate the expression only when one of its
identified control variables changes.
§What are Control Variables?
A “control variable” is a part of the expression that the until!
macro identifies as a reactive data source. Specifically, any expression
segment that implements the Publisher trait is treated as a control
variable. This means it can be subscribed to, and it will notify subscribers
(like the ControlExpr) when its underlying value or state changes.
The macro identifies control variables based on their structure when they
form a path to a Publisher:
- Paths: Simple identifiers (e.g.,
my_control_var) or qualified paths (e.g.,my_module::reactive_data) that resolve to aPublisher. - Field Accesses: Accessing fields of a struct that ultimately yield a
Publisher(e.g.,data_struct.control_field). - Index Operations: Indexing into collections where the element itself
is a
Publisher(e.g.,control_array[0]).
These can be combined. For example, config.settings[exp_no].duration
would be treated as a single control variable if this entire expression
resolves to a type that implements Publisher.
A common reactive primitive is Control<T>, which implements Publisher
and provides a Cell-like API for interior mutability.
§What is NOT a Control Variable (in terms of triggering updates)?
- Literals: Constants like
true,false,0,"hello". - Non-Reactive Variables: Variables in the lexical context of the
closure that do not implement
Publisher. Their values at the time of evaluation are used. - Local Variables: Variables declared and used inside the closure.
- Expressions within Index Calculations: If an index itself is an
expression (e.g.,
my_array[some_offset + 1]), variables used within that index calculation (likesome_offset) are not treated as control variables for the outer expressionmy_array[...]. This is to prevent dynamic sets of dependencies. - Function/Method Call Results: The results of function or method calls are not treated as control variables.
§How It Works (Transformation)
The macro transforms the input expression (e.g., my_control > 10 && another_control.is_full())
into roughly the following structure:
ControlExpr::new(
// Tuple of references to identified control variables (Publishers)
(&my_control, &another_control),
|args| { // Closure taking current values/states from control variables
// Original expression, rewritten:
// If `my_control` implements `Expr` (e.g., `Control<T: Copy>`),
// `my_control` is rewritten as `args.0.get() > 10` (conceptual).
// If `another_control` is just a `Publisher`, its state is used directly.
// The macro uses autoref specialization to handle these cases.
(args.0 /* .get() if Expr */) > 10 && (args.1 /* .get() if Expr */).is_full()
}
)§Expr Trait Convenience
If a control variable (which implements Publisher) also implements the
Expr trait, the macro provides a convenience:
- For types like
Control<T>whereT: Copy,Expris implemented. - Inside the generated closure, the macro will automatically call the
Expr::get()method to retrieve the inner value. This means users can writemy_control_var > 0instead ofmy_control_var.get() > 0in the expression passed to the macro.
§Error Handling
- Non-
PublisherExternal Variables: If a non-local identifier (a path that is not a simple local variable) used in the expression does not resolve to a type implementingPublisher, it is treated like a local variable. If it does neither implementPublishernorExpr, a compile-time error will be issued. All external dependencies that are meant to drive the reactivity must bePublishers.
The specialized handling of different expression types (e.g., a direct
Publisher versus a Publisher + Expr) leverages an autoref specialization
technique to provide a seamless experience.
§Why Control Variables in Index Calculations Are Not Supported
Allowing variables within index expressions (e.g., slice[index_variable])
to be control variables themselves would imply that the set of other
control variables the ControlExpr depends on could change dynamically if
index_variable changes. For example, if index_variable changed from 0
to 1, the expression would effectively switch its dependency from
slice[0] to slice[1].
Supporting such dynamic dependencies would:
- Significantly complicate the dispatch and notification mechanisms.
- Prevent optimizations related to a fixed set of dependencies.
- Make reasoning about the reactive flow more difficult.
Therefore, expressions within an index are evaluated when the ControlExpr
is created or re-evaluated, but variables used solely within them do not
register as separate control variables for the overall expression.
§Limitations
- Maximum Control Variables: The macro currently supports a maximum of
12 distinct control variables within a single
until!invocation. If more are detected, a compile-time error will occur. - Complexity: While powerful, overuse, or overly complex expressions
within
until!can make the reactive logic harder to follow.
§Examples
1. Awaiting a simple Control<T> variable change:
let i: Control<i32> = Control::new(0);
sim.fork(async {
// In another task or later in the code:
i.set(1);
}).and(async {
// `i.get()` is implicitly called due to `Expr` trait
until!(i > 0).await;
println!("i is now greater than 0!");
}).await;Actual expansion:
ControlExpr::new((&i,), |args| args.0.get() > 0).await;
2. Awaiting a condition involving multiple Control<bool> variables:
let is_ready = Control::new(false);
let has_permission = Control::new(false);
sim.fork(async {
sim.advance(1.0).await;
is_ready.set(true);
}).and(async {
sim.advance(2.0).await;
has_permission.set(true);
}).and(async {
until!(is_ready && has_permission).await;
println!("Both conditions met!");
}).await;Actual expansion:
ControlExpr::new((&is_ready, &has_permission), |args| args.0.get() && args.1.get()).await;
3. Using complex paths to Control<T> fields:
let app_config = AppConfig {
settings: Settings {
font_size: Control::new(12),
dark_mode: Control::new(false),
}
};
let min_font_size = 10; // Local non-control variable
// Later...
until!(app_config.settings.font_size > min_font_size && app_config.settings.dark_mode).await;
println!("Font size is adequate and dark mode is enabled!");4. Interaction with local, non-control variables:
let val: Control<i32> = Control::new(5);
let local_modifier = 2; // Not a Publisher
let enabled_flag = true; // Not a Publisher
until!(val * local_modifier > 10 && enabled_flag).await;
println!("Condition with local variables met!");Here, val is the control variable. The closure captures ‘local_modifier’
and ‘enabled_flag’. Changes to them after ControlExpr creation
won’t trigger re-evaluation unless val also changes.
5. Usage in an async function:
async fn wait_for_signal(signal_strength: &Control<u32>) {
// The current task will sleep until signal_strength.get() is 5 or greater.
until!(signal_strength >= 5).await;
}