until

Macro until 

Source
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 a Publisher.
  • 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 (like some_offset) are not treated as control variables for the outer expression my_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> where T: Copy, Expr is implemented.
  • Inside the generated closure, the macro will automatically call the Expr::get() method to retrieve the inner value. This means users can write my_control_var > 0 instead of my_control_var.get() > 0 in the expression passed to the macro.

§Error Handling

  • Non-Publisher External 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 implementing Publisher, it is treated like a local variable. If it does neither implement Publisher nor Expr, a compile-time error will be issued. All external dependencies that are meant to drive the reactivity must be Publishers.

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:

  1. Significantly complicate the dispatch and notification mechanisms.
  2. Prevent optimizations related to a fixed set of dependencies.
  3. 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;
}