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}