Skip to main content

asupersync_macros/
lib.rs

1//! Proc macros for asupersync structured concurrency runtime.
2//!
3//! This crate provides procedural macros that simplify working with the asupersync
4//! async runtime's structured concurrency primitives. The macros handle the boilerplate
5//! for creating scopes, spawning tasks, joining results, and racing computations.
6//!
7//! # Available Macros
8//!
9//! - [`scope!`] - Create a structured concurrency scope
10//! - [`spawn!`] - Spawn a task within the current scope
11//! - [`join!`] - Join multiple futures, waiting for all to complete
12//! - [`join_all!`] - Join multiple futures into an array
13//! - [`race!`] - Race multiple futures, returning the first to complete
14//! - [`select!`] - Select over heterogeneous branches with an optional `else` arm
15//! - [`#[main]`](macro@main) / [`#[test]`](macro@test) - Production runtime entry attributes
16//! - [`session_protocol!`] - Generate typestate session protocols
17//! - [`conformance`] - Annotate conformance tests
18//! - [`lab_test`] / [`explore_seeds`] - Run deterministic lab-runtime tests
19//! - [`ProtoMessage`] / [`ProtoOneof`] - Derive the owned protobuf authoring contract
20//!
21//! # Contract With `asupersync`
22//!
23//! The root `asupersync` crate re-exports only the supported runtime DSL:
24//! `scope!`, `spawn!`, `join!`, `join_all!`, `race!`, and `select!`, and only
25//! when the `proc-macros` feature is enabled.
26//!
27//! This crate also defines `session_protocol!` and `#[conformance]`, but those
28//! remain explicit-path macros on `asupersync_macros`; they are not part of the
29//! default root macro contract.
30//!
31//! # Example
32//!
33//! ```ignore
34//! use asupersync_macros::{scope, spawn, join, race};
35//!
36//! async fn example(cx: &Cx, state: &mut RuntimeState) {
37//!     scope!(cx, state: state, {
38//!         let handle1 = spawn!(async { compute_a().await });
39//!         let handle2 = spawn!(async { compute_b().await });
40//!
41//!         // Wait for both
42//!         let (result_a, result_b) = join!(handle1, handle2);
43//!     });
44//! }
45//! ```
46
47mod entry;
48mod instrument;
49mod join;
50mod lab_test;
51mod proto;
52mod race;
53mod scope;
54mod select;
55mod session;
56mod spawn;
57mod util;
58
59use proc_macro::TokenStream;
60use syn::parse_macro_input;
61
62/// Derives the owned `asupersync::grpc::protobuf::ProtoMessage` contract.
63///
64/// Fields use `#[proto(kind, tag = N)]`, with explicit `optional`,
65/// `repeated`, and `packed` modifiers. Maps use
66/// `#[proto(map, key = "string", value = "uint64", tag = N)]`; oneofs use an
67/// `Option<T>` field annotated with `#[proto(oneof, tags = "N, M")]`.
68///
69/// Expansion is deterministic and Cargo-only: it invokes no schema compiler
70/// and reads no ambient files.
71#[proc_macro_derive(ProtoMessage, attributes(proto))]
72pub fn derive_proto_message(input: TokenStream) -> TokenStream {
73    let input = parse_macro_input!(input as syn::DeriveInput);
74    proto::derive_proto_message(&input)
75        .unwrap_or_else(syn::Error::into_compile_error)
76        .into()
77}
78
79/// Derives the owned `asupersync::grpc::protobuf::ProtoOneof` contract.
80///
81/// Every enum variant must be a one-value tuple variant with its own
82/// `#[proto(kind, tag = N)]` attribute.
83#[proc_macro_derive(ProtoOneof, attributes(proto))]
84pub fn derive_proto_oneof(input: TokenStream) -> TokenStream {
85    let input = parse_macro_input!(input as syn::DeriveInput);
86    proto::derive_proto_oneof(&input)
87        .unwrap_or_else(syn::Error::into_compile_error)
88        .into()
89}
90
91/// Runs an async `main` function on an asupersync production runtime.
92///
93/// Supported signatures:
94///
95/// ```ignore
96/// #[asupersync::main]
97/// async fn main() {}
98///
99/// #[asupersync::main(flavor = "current_thread", workers = 1, budget = 128)]
100/// async fn main(cx: &asupersync::Cx) -> Result<(), asupersync::Error> {
101///     Ok(())
102/// }
103/// ```
104#[proc_macro_attribute]
105pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
106    entry::main_impl(attr, item)
107}
108
109/// Runs an async test function on an asupersync production runtime.
110///
111/// This is distinct from [`#[lab_test]`](macro@lab_test): `#[asupersync::test]`
112/// uses the production runtime, while `#[lab_test]` uses deterministic lab
113/// runtime seed matrices.
114#[proc_macro_attribute]
115pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
116    entry::test_impl(attr, item)
117}
118
119/// Creates a structured concurrency scope.
120///
121/// The `scope!` macro creates an `asupersync::Scope` binding for the
122/// current `Cx` region and makes it available as `scope` inside the body.
123///
124/// Today this is an ergonomic binding helper, not a fresh child-region
125/// boundary. For actual child-region ownership and quiescence, call
126/// `asupersync::Scope::region` explicitly.
127///
128/// # Syntax
129///
130/// ```ignore
131/// scope!(cx, {
132///     // body with spawned tasks
133/// })
134/// scope!(cx, state: &mut state, {
135///     let _child = spawn!(async { work().await });
136/// })
137/// ```
138///
139/// # Arguments
140///
141/// - `cx` - The capability context (`&Cx`)
142/// - `body` - A block containing the scope's work
143/// - `state` - Optional runtime state binding used by nested `spawn!` calls
144///
145/// # Returns
146///
147/// The result of the scope body.
148///
149/// # Example
150///
151/// ```ignore
152/// scope!(cx, state: &mut state, {
153///     spawn!(async { work_a().await });
154///     spawn!(async { work_b().await });
155///     // Both tasks are awaited before scope exits
156/// })
157/// ```
158#[proc_macro]
159pub fn scope(input: TokenStream) -> TokenStream {
160    scope::scope_impl(input)
161}
162
163/// Spawns a task within the current scope.
164///
165/// The `spawn!` macro expands to `asupersync::Scope::spawn_registered`, so it requires
166/// ambient `__state` and `__cx` bindings in addition to the target `Scope`.
167///
168/// The easiest supported path is to use it inside `scope!(..., state: ..., { ... })`.
169///
170/// # Syntax
171///
172/// ```ignore
173/// spawn!(async { /* work */ })
174/// spawn!(async move { /* work with captured values */ })
175/// ```
176///
177/// # Returns
178///
179/// A `TaskHandle` that can be awaited to get the task's result.
180///
181/// # Example
182///
183/// ```ignore
184/// let handle = spawn!(async {
185///     expensive_computation().await
186/// });
187/// let result = handle.await;
188/// ```
189#[proc_macro]
190pub fn spawn(input: TokenStream) -> TokenStream {
191    spawn::spawn_impl(input)
192}
193
194/// Joins multiple futures, waiting for all to complete.
195///
196/// The `join!` macro polls all branches concurrently inside the enclosing task
197/// and returns their outputs as a tuple in input order. A pending branch never
198/// blocks ready branches from making progress, so same-duration sleeps complete
199/// in one duration rather than the sum of all durations.
200///
201/// # Syntax
202///
203/// ```ignore
204/// join!(future1, future2, ...)
205/// ```
206///
207/// # Returns
208///
209/// A tuple of all the futures' results in the order they were specified.
210///
211/// # Outcome Semantics
212///
213/// The combined outcome follows the severity lattice:
214/// - If all succeed: `Outcome::Ok((r1, r2, ...))`
215/// - If any fails: the most severe outcome is propagated
216///
217/// # Example
218///
219/// ```ignore
220/// let (a, b, c) = join!(
221///     fetch_user().await,
222///     fetch_profile().await,
223///     fetch_settings().await
224/// );
225/// ```
226#[proc_macro]
227pub fn join(input: TokenStream) -> TokenStream {
228    join::join_impl(input)
229}
230
231/// Joins multiple futures into an array, waiting for all to complete.
232///
233/// The `join_all!` macro is like `join!` but returns an array instead of a
234/// tuple. It uses the same concurrent polling expansion, so all branches are
235/// driven together within the enclosing task while preserving input order in
236/// the returned array.
237///
238/// # Syntax
239///
240/// ```ignore
241/// join_all!(future1, future2, ...)
242/// ```
243///
244/// # Returns
245///
246/// An array of all the futures' results in the order they were specified.
247/// Since all results must be the same type, this enables easier iteration.
248///
249/// # Example
250///
251/// ```ignore
252/// let results: [i32; 3] = join_all!(
253///     fetch_value(1).await,
254///     fetch_value(2).await,
255///     fetch_value(3).await
256/// );
257/// for result in results {
258///     println!("{}", result);
259/// }
260/// ```
261#[proc_macro]
262pub fn join_all(input: TokenStream) -> TokenStream {
263    join::join_all_impl(input)
264}
265
266/// Races multiple futures, returning the first to complete — **losers are
267/// drained**.
268///
269/// The `race!` macro expands to the drain-correct
270/// `asupersync::Cx::race_drained*` family: each branch is
271/// spawned as a region task and resolved through
272/// `asupersync::Scope::race_all`, so every losing branch is
273/// protocol-cancelled **and drained** (awaited to termination) before the macro
274/// returns. This is the drain guarantee that differentiates `race!` from a
275/// plain drop-the-losers select.
276///
277/// Because branches run as spawned tasks, each branch and its output must be
278/// `Send + 'static`, and `cx` must be a runtime-wired context carrying spawn
279/// authority. For a lower-level drop-on-cancel select over non-`'static`
280/// inline futures, call `asupersync::Cx::race` directly.
281///
282/// # Syntax
283///
284/// ```ignore
285/// race!(cx, { future1, future2, ... })
286/// race!(cx, { "name" => future1, "other" => future2, ... })
287/// race!(cx, timeout: Duration::from_secs(5), { future1, future2, ... })
288/// ```
289///
290/// # Returns
291///
292/// The result of the winning future.
293///
294/// # Loser Cleanup
295///
296/// All non-winning branches are cancelled and drained: the macro does not
297/// return until each loser task has terminated, so obligations and finalizers
298/// held by a loser are resolved rather than abandoned. (On the `timeout:` path,
299/// an elapsed deadline abandons the whole race by drop, matching
300/// `asupersync::Cx::race_drained_timeout`.)
301///
302/// # Example
303///
304/// ```ignore
305/// let result = race!(cx, {
306///     primary_service.fetch().await,
307///     backup_service.fetch().await,
308/// });
309/// // One completed; the loser was cancelled AND drained before this returned.
310/// ```
311#[proc_macro]
312pub fn race(input: TokenStream) -> TokenStream {
313    race::race_impl(input)
314}
315
316/// Selects over heterogeneous branches — **losers are drained** — with an
317/// optional non-blocking `else` arm.
318///
319/// `select!` is the N-ary, heterogeneous member of the race family. Each branch
320/// awaits its own future (the branch types may differ) and runs a handler arm;
321/// every handler must yield the same result type `R`. It lifts the fixed
322/// `Race2`/`Race3`/`Race4` arity ceiling and is the drain-correct alternative
323/// to `tokio::select!`.
324///
325/// # Two forms
326///
327/// **Blocking, drain-correct** (no `else` arm): each branch is rewritten into
328/// `async move { let <pat> = <future>.await; <handler> }`, and the homogeneous
329/// per-branch list routes through
330/// `asupersync::Cx::race_drained`. The first branch to win
331/// resolves the `select!`; every loser is protocol-cancelled **and drained**
332/// (awaited to termination) before the macro returns. Resolves to
333/// `Result<R, JoinError>`. Branch futures and `R` must be `Send + 'static`, and
334/// `cx` must carry spawn authority (`Cx<cap::All>`).
335///
336/// **Non-blocking default** (trailing `else => <handler>` arm): each branch is
337/// polled **exactly once** in source order; the first ready branch wins,
338/// otherwise the `else` handler runs immediately. This is the Go-style
339/// `default` arm — it never waits, so it does not drain; not-ready branches are
340/// dropped. Resolves to `R`.
341///
342/// # Determinism / tie-break
343///
344/// Every `select!` is replay-deterministic: the same seed always produces the
345/// same winner. The blocking form resolves through the runtime drain engine
346/// (`asupersync::Scope::race_all`), which breaks ties among
347/// same-turn-ready branches with the lab's **seeded** scheduler RNG — fixed by
348/// the seed, not by source position. The `else` form polls in strict **source
349/// order** and takes the first ready branch. The `biased` keyword is accepted
350/// on the blocking form for `tokio::select!` familiarity and documents that
351/// selection is deterministic; it does not impose strict source order — use the
352/// `else` form for that.
353///
354/// # Syntax
355///
356/// ```ignore
357/// // blocking, drain-correct
358/// let r = select!(cx, {
359///     a = primary.fetch()  => use_primary(a),
360///     b = backup.fetch()   => use_backup(b),
361/// })?;
362///
363/// // explicit source-order tie-break
364/// let r = select!(cx, biased, {
365///     a = fast()  => a,
366///     b = slow()  => b,
367/// })?;
368///
369/// // non-blocking Go-style default
370/// let r = select!(cx, {
371///     a = try_recv() => a,
372///     else => default_value(),
373/// });
374/// ```
375#[proc_macro]
376pub fn select(input: TokenStream) -> TokenStream {
377    select::select_impl(input)
378}
379
380/// Instruments a function or impl method with a tracing span.
381///
382/// The generated wrapper uses `asupersync::tracing_compat`, so it creates real
383/// spans when `tracing-integration` is enabled and becomes a no-op when tracing
384/// is disabled.
385///
386/// Supported arguments:
387///
388/// - `name = "custom_name"` overrides the span name
389/// - `level = "trace" | "debug" | "info" | "warn" | "error"` sets span level
390/// - `skip(arg1, arg2, ...)` excludes arguments from captured fields
391///
392/// # Examples
393///
394/// ```ignore
395/// use asupersync::tracing_compat::instrument;
396///
397/// #[instrument]
398/// async fn load_user(user_id: u64) -> Result<(), Error> {
399///     Ok(())
400/// }
401///
402/// #[instrument(name = "cache_refresh", level = "debug", skip(secret))]
403/// fn refresh(secret: &Secret, key: &str) {}
404/// ```
405#[proc_macro_attribute]
406pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
407    instrument::instrument_impl(attr, item)
408}
409
410/// Marks a test with the specification section and requirement it validates.
411///
412/// # Syntax
413///
414/// ```ignore
415/// #[conformance(spec = "3.2.1", requirement = "Region close waits for all children")]
416/// #[test]
417/// fn test_region_close_waits() { /* ... */ }
418/// ```
419///
420/// The macro is validation-only: it checks that `spec` and `requirement` are
421/// present and string literals, then leaves the item unchanged.
422#[proc_macro_attribute]
423pub fn conformance(attr: TokenStream, item: TokenStream) -> TokenStream {
424    match parse_conformance_args(&attr) {
425        Ok(_) => item,
426        Err(message) => util::compile_error(&message).into(),
427    }
428}
429
430/// Runs a deterministic lab-runtime test with optional seed matrices.
431///
432/// Supported function shapes:
433///
434/// ```ignore
435/// #[lab_test]
436/// fn raw_lab(lab: &mut asupersync::lab::LabRuntime) {
437///     // create tasks, advance virtual time, inspect lab state
438/// }
439///
440/// #[lab_test(seeds = 0..16, chaos)]
441/// async fn async_body(cx: &asupersync::cx::Cx) {
442///     // run under a root lab task with automatic quiescence/oracle checks
443/// }
444/// ```
445#[proc_macro_attribute]
446pub fn lab_test(attr: TokenStream, item: TokenStream) -> TokenStream {
447    lab_test::lab_test_impl(attr, item)
448}
449
450/// Runs a deterministic lab-runtime body across a seed sweep.
451///
452/// The body is invoked once per seed with a fresh
453/// `asupersync::lab::LabRuntime`. The generated test drains each
454/// run to quiescence, aggregates trace equivalence-class coverage, and reports
455/// failing seeds with replay-friendly reproducer commands.
456///
457/// Supported arguments:
458///
459/// - `base = N` or `base_seed = N` sets the first seed
460/// - `count = N` sets the number of seeds
461/// - `seeds = START..END` uses an exclusive range
462/// - `workers = N` or `worker_count = N` sets the lab worker count
463/// - `max_steps = N` sets the per-seed step limit
464/// - `chaos` enables the light deterministic chaos profile
465///
466/// # Example
467///
468/// ```ignore
469/// #[explore_seeds(seeds = 0..32, workers = 2)]
470/// fn cancellation_matrix(lab: &mut asupersync::lab::LabRuntime) {
471///     // build the per-seed scenario; the macro drains and checks it
472/// }
473/// ```
474#[proc_macro_attribute]
475pub fn explore_seeds(attr: TokenStream, item: TokenStream) -> TokenStream {
476    lab_test::explore_seeds_impl(attr, item)
477}
478
479/// Generates typestate-encoded session types from a protocol DSL.
480///
481/// The macro takes a protocol specification and generates a module containing
482/// message structs, paired session type aliases (initiator + responder), and
483/// constructor functions. The responder type is the dual of the initiator:
484/// `Send`↔`Recv`, `Select`↔`Offer`.
485///
486/// # Syntax
487///
488/// ```ignore
489/// session_protocol! {
490///     module_name<T> for ObligationVariant {
491///         msg MessageName;
492///         msg MessageWithFields { field: Type };
493///
494///         send MessageName => select {
495///             send T => end,
496///             send OtherMsg => end,
497///         }
498///     }
499/// }
500/// ```
501///
502/// # Body Actions
503///
504/// - `send Type => body` — send a value, then continue
505/// - `recv Type => body` — receive a value, then continue
506/// - `select { a, b }` — local choice (becomes `Offer` for responder)
507/// - `offer { a, b }` — remote choice (becomes `Select` for responder)
508/// - `loop { body }` — recursion point (generates `renew_loop` constructor)
509/// - `continue` — jump back to enclosing `loop`
510/// - `end` — protocol termination
511///
512/// # Generated Items
513///
514/// - `pub mod <name>` containing:
515///   - Message structs with `Debug, Clone` (+ `Copy` for unit structs)
516///   - `InitiatorSession` type alias
517///   - `ResponderSession` type alias
518///   - `new_session(channel_id) -> (Chan<Initiator, ...>, Chan<Responder, ...>)`
519///   - (if `loop` used) `InitiatorLoop`, `ResponderLoop` type aliases
520///   - (if `loop` used) `renew_loop(channel_id)` constructor
521///
522/// # Example
523///
524/// ```ignore
525/// session_protocol! {
526///     lease for Lease {
527///         msg AcquireMsg;
528///         msg RenewMsg;
529///         msg ReleaseMsg;
530///
531///         send AcquireMsg => loop {
532///             select {
533///                 send RenewMsg => continue,
534///                 send ReleaseMsg => end,
535///             }
536///         }
537///     }
538/// }
539/// ```
540#[proc_macro]
541pub fn session_protocol(input: TokenStream) -> TokenStream {
542    session::session_protocol_impl(input)
543}
544
545#[derive(Debug, Clone, PartialEq, Eq)]
546struct ConformanceArgs {
547    spec: String,
548    requirement: String,
549}
550
551fn parse_conformance_args(attr: &TokenStream) -> Result<ConformanceArgs, String> {
552    parse_conformance_args_str(&attr.to_string())
553}
554
555fn parse_conformance_args_str(input: &str) -> Result<ConformanceArgs, String> {
556    let raw = input.trim();
557    if raw.is_empty() {
558        return Err("conformance attribute requires arguments".to_string());
559    }
560
561    let mut spec = None;
562    let mut requirement = None;
563
564    for part in split_args(raw) {
565        let part = part.trim();
566        if part.is_empty() {
567            continue;
568        }
569        let (key, value) = split_key_value(part)?;
570        let value = parse_string_literal(value)?;
571        match key {
572            "spec" => spec = Some(value),
573            "requirement" => requirement = Some(value),
574            other => {
575                return Err(format!(
576                    "conformance attribute has unknown key '{other}', expected 'spec' or 'requirement'"
577                ));
578            }
579        }
580    }
581
582    let spec = spec.ok_or_else(|| "conformance attribute missing 'spec'".to_string())?;
583    let requirement =
584        requirement.ok_or_else(|| "conformance attribute missing 'requirement'".to_string())?;
585
586    Ok(ConformanceArgs { spec, requirement })
587}
588
589fn split_args(input: &str) -> Vec<String> {
590    let mut parts = Vec::new();
591    let mut current = String::new();
592    let mut in_string = false;
593    let mut escape = false;
594
595    for ch in input.chars() {
596        if in_string {
597            current.push(ch);
598            if escape {
599                escape = false;
600                continue;
601            }
602            if ch == '\\' {
603                escape = true;
604            } else if ch == '"' {
605                in_string = false;
606            }
607            continue;
608        }
609
610        match ch {
611            '"' => {
612                in_string = true;
613                current.push(ch);
614            }
615            ',' => {
616                parts.push(current);
617                current = String::new();
618            }
619            _ => current.push(ch),
620        }
621    }
622
623    if !current.trim().is_empty() {
624        parts.push(current);
625    }
626
627    parts
628}
629
630fn split_key_value(input: &str) -> Result<(&str, &str), String> {
631    let mut iter = input.splitn(2, '=');
632    let key = iter
633        .next()
634        .map(str::trim)
635        .filter(|s| !s.is_empty())
636        .ok_or_else(|| "conformance attribute expects key = \"value\" pairs".to_string())?;
637    let value = iter
638        .next()
639        .map(str::trim)
640        .filter(|s| !s.is_empty())
641        .ok_or_else(|| format!("conformance attribute missing value for '{key}'"))?;
642    Ok((key, value))
643}
644
645fn parse_string_literal(input: &str) -> Result<String, String> {
646    let trimmed = input.trim();
647    if !trimmed.starts_with('"') || !trimmed.ends_with('"') {
648        return Err(format!(
649            "conformance attribute values must be string literals, got: {trimmed}"
650        ));
651    }
652    let inner = &trimmed[1..trimmed.len() - 1];
653    let mut out = String::new();
654    let mut chars = inner.chars();
655    while let Some(ch) = chars.next() {
656        if ch == '\\' {
657            let next = chars.next().ok_or_else(|| {
658                "conformance attribute contains dangling escape sequence".to_string()
659            })?;
660            match next {
661                '\\' => out.push('\\'),
662                '"' => out.push('"'),
663                'n' => out.push('\n'),
664                'r' => out.push('\r'),
665                't' => out.push('\t'),
666                other => {
667                    return Err(format!(
668                        "conformance attribute contains unsupported escape: \\{other}"
669                    ));
670                }
671            }
672        } else {
673            out.push(ch);
674        }
675    }
676    Ok(out)
677}
678
679#[cfg(test)]
680mod tests {
681    use super::parse_conformance_args_str;
682
683    #[test]
684    fn parse_conformance_args_ok() {
685        let args =
686            parse_conformance_args_str(r#"spec = "3.2.1", requirement = "Region close waits""#)
687                .unwrap();
688        assert_eq!(args.spec, "3.2.1");
689        assert_eq!(args.requirement, "Region close waits");
690    }
691
692    #[test]
693    fn parse_conformance_args_missing_spec() {
694        let err = parse_conformance_args_str(r#"requirement = "Region close waits""#).unwrap_err();
695        assert!(err.contains("missing 'spec'"));
696    }
697
698    #[test]
699    fn parse_conformance_args_missing_requirement() {
700        let err = parse_conformance_args_str(r#"spec = "3.2.1""#).unwrap_err();
701        assert!(err.contains("missing 'requirement'"));
702    }
703}