1#![deny(missing_docs)]
2
3use proc_macro2::TokenStream as TokenStream2;
16use quote::quote;
17use syn::parse::{Parse, ParseStream};
18use syn::punctuated::Punctuated;
19use syn::{Attribute, Expr, Ident, LitInt, Meta, Result as SynResult, Token, Type, bracketed};
20
21#[allow(missing_docs)]
26pub mod kw {
27 syn::custom_keyword!(node);
28 syn::custom_keyword!(pool);
29 syn::custom_keyword!(deps);
30 syn::custom_keyword!(discover);
31 syn::custom_keyword!(dataflow);
32 syn::custom_keyword!(spawn);
33 syn::custom_keyword!(task);
34 syn::custom_keyword!(pool_size);
35 syn::custom_keyword!(policy);
36 syn::custom_keyword!(min);
37 syn::custom_keyword!(max);
38 syn::custom_keyword!(disabled);
39 syn::custom_keyword!(executor);
40 syn::custom_keyword!(resources);
41 syn::custom_keyword!(provides);
42 syn::custom_keyword!(slot_timeout);
43 syn::custom_keyword!(ack_timeout);
44 syn::custom_keyword!(beat_timeout);
45 syn::custom_keyword!(beat_window);
46 syn::custom_keyword!(reads);
47 syn::custom_keyword!(writes);
48 syn::custom_keyword!(observe);
49 syn::custom_keyword!(ready_on_write);
50 syn::custom_keyword!(exit);
51 syn::custom_keyword!(name);
52 syn::custom_keyword!(state);
53 syn::custom_keyword!(zeroed);
54 syn::custom_keyword!(cancel);
55 syn::custom_keyword!(fragment);
56 syn::custom_keyword!(endfragment);
57}
58
59#[allow(clippy::large_enum_variant)]
61#[derive(Clone)]
62pub enum StateInit {
63 Expr(Expr),
65 Zeroed(kw::zeroed),
67}
68
69#[derive(Clone)]
71pub struct Dep {
72 pub cfg: Vec<Attribute>,
74 pub ident: Ident,
76 pub ready: Option<Ident>,
78 pub bound: Option<Ident>,
80}
81
82pub fn parse_dep_list(input: ParseStream) -> SynResult<Vec<Dep>> {
84 let content;
85 bracketed!(content in input);
86 let mut deps = Vec::new();
87 while !content.is_empty() {
88 let cfg = content.call(Attribute::parse_outer)?;
89 let ident: Ident = content.parse()?;
90 let mut ready: Option<Ident> = None;
91 let mut bound: Option<Ident> = None;
92 while content.peek(Ident) {
93 let marker: Ident = content.parse()?;
94 let slot = match () {
95 _ if marker == "ready" => &mut ready,
96 _ if marker == "bound" => &mut bound,
97 _ => {
98 return Err(syn::Error::new_spanned(
99 &marker,
100 format!(
101 "expected `,`, `]`, or a dep marker (`ready`, \
102 `bound`), found `{marker}`"
103 ),
104 ));
105 }
106 };
107 if slot.is_some() {
108 return Err(syn::Error::new_spanned(
109 &marker,
110 format!("duplicate `{marker}` marker on this dep"),
111 ));
112 }
113 *slot = Some(marker);
114 }
115 if let (Some(b), None) = (&bound, &ready) {
116 return Err(syn::Error::new_spanned(
117 b,
118 "`bound` implies `ready` — write `deps: [X ready bound]`",
119 ));
120 }
121 deps.push(Dep {
122 cfg,
123 ident,
124 ready,
125 bound,
126 });
127 if content.peek(Token![,]) {
128 content.parse::<Token![,]>()?;
129 }
130 }
131 Ok(deps)
132}
133
134#[derive(Clone)]
136pub struct SignalDecl {
137 pub cfg: Vec<Attribute>,
139 pub path: syn::Path,
141 pub index: Option<Expr>,
143 pub observed: Option<Ident>,
145 pub beat: Option<Ident>,
147 pub veto: Option<Ident>,
150 pub via: Option<Expr>,
152}
153
154impl SignalDecl {
155 pub fn target(&self) -> TokenStream2 {
157 let path = &self.path;
158 match &self.index {
159 Some(i) => quote!(#path[#i]),
160 None => quote!(#path),
161 }
162 }
163
164 pub fn display(&self) -> String {
166 let mut out = path_to_string(&self.path);
167 if let Some(i) = &self.index {
168 out.push('[');
169 out.push_str("e!(#i).to_string().replace(' ', ""));
170 out.push(']');
171 }
172 out
173 }
174}
175
176pub fn parse_signal_list(input: ParseStream) -> SynResult<Vec<SignalDecl>> {
178 let content;
179 bracketed!(content in input);
180 let mut decls = Vec::new();
181 while !content.is_empty() {
182 let cfg = content.call(Attribute::parse_outer)?;
183 let path: syn::Path = content.parse()?;
184 let index = if content.peek(syn::token::Bracket) {
185 let idx;
186 bracketed!(idx in content);
187 Some(idx.parse::<Expr>()?)
188 } else {
189 None
190 };
191 let mut observed = None;
192 let mut beat = None;
193 let mut veto = None;
194 let mut via = None;
195 while content.peek(Ident) {
198 let marker: Ident = content.parse()?;
199 let slot = match marker.to_string().as_str() {
200 "observed" => &mut observed,
201 "beat" => &mut beat,
202 "veto" => &mut veto,
203 "via" => {
204 if observed.is_none() {
205 return Err(syn::Error::new_spanned(
206 &marker,
207 "`via` supplies the accessor an `observed` entry polls, \
208 which only an `observed` entry has: write `observed via \
209 <expr>`. `beat` only ever qualifies `observed`, and a \
210 heartbeat the body can state is stated by its verb",
211 ));
212 }
213 via = Some(content.parse::<Expr>()?);
214 break;
215 }
216 other => {
217 return Err(syn::Error::new_spanned(
218 &marker,
219 format!(
220 "expected `,`, `]`, or the `observed`/`beat`/`veto` markers, \
221 found `{other}`"
222 ),
223 ));
224 }
225 };
226 if slot.is_some() {
227 return Err(syn::Error::new_spanned(
228 &marker,
229 format!("duplicate `{marker}` marker"),
230 ));
231 }
232 *slot = Some(marker);
233 }
234 if let (Some(b), None) = (&beat, &observed) {
235 return Err(syn::Error::new_spanned(
236 b,
237 "a bare `beat` entry is not a declaration: write the heartbeat \
238 at the site that produces it, with `node.beat_put(&SIG, v)` / \
239 `node.beat_writer(&SIG)` or a `node.beat()` call in the body. \
240 `observed beat` is the form for a body the supervisor cannot \
241 see",
242 ));
243 }
244 decls.push(SignalDecl {
245 cfg,
246 path,
247 index,
248 observed,
249 beat,
250 veto,
251 via,
252 });
253 if !content.is_empty() {
254 content.parse::<Token![,]>()?;
255 }
256 }
257 Ok(decls)
258}
259
260pub fn check_signal_list<T: quote::ToTokens>(
264 tok: &T,
265 clause: &str,
266 decls: &[SignalDecl],
267) -> SynResult<()> {
268 if decls.is_empty() {
269 return Err(syn::Error::new_spanned(
270 tok,
271 format!(
272 "`{clause}:` must declare at least one signal path — omit the \
273 clause entirely to declare nothing"
274 ),
275 ));
276 }
277 for (i, decl) in decls.iter().enumerate() {
278 let name = decl.display();
279 if decls[..i].iter().any(|prev| prev.display() == name) {
280 return Err(syn::Error::new_spanned(
281 &decl.path,
282 format!("duplicate `{clause}:` entry `{name}`"),
283 ));
284 }
285 let bare = path_to_string(&decl.path);
286 if decls[..i].iter().any(|prev| {
287 path_to_string(&prev.path) == bare && prev.index.is_some() != decl.index.is_some()
288 }) {
289 return Err(syn::Error::new_spanned(
290 &decl.path,
291 format!(
292 "`{bare}` is declared both as a whole array and by element — \
293 pick one. An element-0 reference has the same address as the \
294 array, so nothing downstream can tell them apart"
295 ),
296 ));
297 }
298 }
299 Ok(())
300}
301
302pub fn path_to_string(p: &syn::Path) -> String {
304 let mut out = String::new();
305 if p.leading_colon.is_some() {
306 out.push_str("::");
307 }
308 for (i, seg) in p.segments.iter().enumerate() {
309 if i > 0 {
310 out.push_str("::");
311 }
312 out.push_str(&seg.ident.to_string());
313 }
314 out
315}
316
317pub fn parse_mode_list(input: ParseStream) -> SynResult<Vec<Ident>> {
319 let content;
320 bracketed!(content in input);
321 let punct = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
322 Ok(punct.into_iter().collect())
323}
324
325#[derive(Clone)]
327pub enum TaskSource {
328 Spawn(Expr),
330 Shell(Expr),
332}
333
334#[derive(Clone)]
336pub struct ResourceDecl {
337 pub cfg: Vec<Attribute>,
339 pub ident: Ident,
341 pub ty: Option<Type>,
344 pub local: Option<Ident>,
346 pub consume: Option<Ident>,
348 pub shared: Option<Ident>,
350 pub divisible: Option<Ident>,
352 pub serialized: Option<Ident>,
354}
355
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub enum ResourceKind {
360 Lend,
363 Consume,
365 Shared,
367 Divisible,
370}
371
372impl ResourceDecl {
373 pub fn kind(&self) -> ResourceKind {
375 if self.divisible.is_some() {
376 ResourceKind::Divisible
377 } else if self.shared.is_some() {
378 ResourceKind::Shared
379 } else if self.consume.is_some() {
380 ResourceKind::Consume
381 } else {
382 ResourceKind::Lend
383 }
384 }
385
386 pub fn shared_signature(&self) -> String {
388 let ty = &self.ty;
389 format!(
390 "{}{}shared {}",
391 if self.local.is_some() { "local " } else { "" },
392 if self.serialized.is_some() {
393 "serialized "
394 } else {
395 ""
396 },
397 quote!(#ty)
398 )
399 }
400}
401
402const KIND_MARKERS: [&str; 5] = ["local", "consume", "shared", "divisible", "serialized"];
403
404pub fn peek_kind_marker(content: ParseStream) -> Option<Ident> {
410 if !content.peek(syn::Ident) {
411 return None;
412 }
413 let fork = content.fork();
414 let ident: Ident = fork.parse().ok()?;
415 if !KIND_MARKERS.iter().any(|m| ident == m) {
416 return None;
417 }
418 if ident == "divisible" {
421 return (!fork.peek(Token![::]) && !fork.peek(Token![<])).then_some(ident);
422 }
423 if fork.is_empty() || fork.peek(Token![,]) || fork.peek(Token![::]) || fork.peek(Token![<]) {
424 return None;
425 }
426 Some(ident)
427}
428
429pub fn parse_resource_list(input: ParseStream) -> SynResult<Vec<ResourceDecl>> {
431 let content;
432 bracketed!(content in input);
433 let mut resources = Vec::new();
434 while !content.is_empty() {
435 let cfg = content.call(Attribute::parse_outer)?;
436 let ident: Ident = content.parse()?;
437 content.parse::<Token![:]>()?;
438 let mut markers: [Option<Ident>; 5] = Default::default();
439 while let Some(marker) = peek_kind_marker(&content) {
440 content.parse::<Ident>()?;
441 let i = KIND_MARKERS
442 .iter()
443 .position(|m| marker == m)
444 .expect("peeked a kind marker");
445 if markers[i].is_some() {
446 return Err(syn::Error::new_spanned(
447 &marker,
448 format!("duplicate `{marker}` marker"),
449 ));
450 }
451 markers[i] = Some(marker);
452 }
453 let [local, consume, shared, divisible, serialized] = markers;
454 if let (Some(_), Some(s)) = (&consume, &shared) {
455 return Err(syn::Error::new_spanned(
456 s,
457 "`consume` and `shared` are mutually exclusive — `consume` takes \
458 the single value out for one owner, `shared` copies it out to \
459 any number of consumers",
460 ));
461 }
462 if let Some(d) = &divisible
463 && let Some(other) = local.as_ref().or(consume.as_ref()).or(shared.as_ref())
464 {
465 return Err(syn::Error::new_spanned(
466 other,
467 format!(
468 "`divisible` is its own kind and takes no other marker: `{other}` \
469 describes how one value is handed over, `{d}` declares a \
470 budget of units the holder claims a share of"
471 ),
472 ));
473 }
474 if let (Some(s), None) = (&serialized, &shared) {
475 return Err(syn::Error::new_spanned(
476 s,
477 "`serialized` only qualifies `shared` — a slot with a single holder \
478 cannot be contended, so there is nothing to serialize",
479 ));
480 }
481 let ty = if let Some(d) = &divisible {
482 if !(content.is_empty() || content.peek(Token![,])) {
483 return Err(syn::Error::new_spanned(
484 d,
485 "`divisible` takes no type — the slot is a `Budget<K>` the graph \
486 sizes to its declaring nodes and pool members",
487 ));
488 }
489 None
490 } else {
491 Some(content.parse::<Type>()?)
492 };
493 resources.push(ResourceDecl {
494 cfg,
495 ident,
496 ty,
497 local,
498 consume,
499 shared,
500 divisible,
501 serialized,
502 });
503 if content.peek(Token![,]) {
504 content.parse::<Token![,]>()?;
505 }
506 }
507 Ok(resources)
508}
509
510#[derive(Clone)]
512pub struct AdoptedFn {
513 pub cfg: Vec<Attribute>,
515 pub path: syn::Path,
517}
518
519#[derive(Clone)]
521pub struct Gated<K, V = ()> {
522 pub cfg: Vec<Attribute>,
524 pub kw: K,
526 pub value: V,
528}
529
530#[derive(Clone)]
532pub struct ProvideDecl {
533 pub cfg: Vec<Attribute>,
535 pub ident: Ident,
537}
538
539#[derive(Clone)]
541pub struct NodeItem {
542 pub cfg: Vec<Attribute>,
544 pub ident: Ident,
546 pub mode: Ident,
548 pub deps: Vec<Dep>,
550 pub source: Option<TaskSource>,
552 pub pool_size: Option<LitInt>,
554 pub resources: Vec<ResourceDecl>,
556 pub disabled: Option<Gated<kw::disabled>>,
558 pub executor: Option<Ident>,
561 pub executor_defaulted: bool,
563 pub slot_timeout: Option<Gated<kw::slot_timeout, LitInt>>,
565 pub ack_timeout: Option<Gated<kw::ack_timeout, LitInt>>,
567 pub beat_timeout: Option<Gated<kw::beat_timeout, LitInt>>,
569 pub beat_window: Option<Gated<kw::beat_window, LitInt>>,
571 pub ready_on_write: Option<Gated<Ident>>,
573 pub reads: Vec<SignalDecl>,
575 pub writes: Vec<SignalDecl>,
577 pub exit: Option<syn::Type>,
579 pub state: Option<(kw::state, syn::Type, StateInit)>,
581 pub cancel: bool,
583 pub discover: Option<Gated<kw::discover>>,
585 pub dataflow: Vec<AdoptedFn>,
587 pub provides: Vec<ProvideDecl>,
589 pub provides_kw: Option<kw::provides>,
591 pub fragment: Option<String>,
593}
594
595#[derive(Clone)]
597pub struct ExecutorItem {
598 pub cfg: Vec<Attribute>,
600 pub ident: Ident,
602 pub default: bool,
605}
606
607#[derive(Clone)]
609pub struct PoolItem {
610 pub cfg: Vec<Attribute>,
612 pub ident: Ident,
614 pub modes: Vec<Ident>,
616 pub deps: Vec<Dep>,
618 pub source: TaskSource,
620 pub policy: Expr,
622 pub policy_ty: Option<Type>,
624 pub executor: Option<Ident>,
627 pub executor_defaulted: bool,
629 pub resources: Vec<ResourceDecl>,
631 pub slot_timeout: Option<Gated<kw::slot_timeout, LitInt>>,
633 pub ack_timeout: Option<Gated<kw::ack_timeout, LitInt>>,
635 pub reads: Vec<SignalDecl>,
637 pub writes: Vec<SignalDecl>,
639 pub min: Expr,
641 pub max: Expr,
643 pub state: Option<(kw::state, syn::Type, StateInit)>,
645 pub cancel: bool,
647 pub discover: Option<Gated<kw::discover>>,
649 pub dataflow: Vec<AdoptedFn>,
651 pub fragment: Option<String>,
653}
654
655#[allow(clippy::large_enum_variant)]
657#[derive(Clone)]
658pub enum Item {
659 Node(NodeItem),
661 Pool(PoolItem),
663 Executor(ExecutorItem),
665}
666
667#[derive(Clone)]
669pub struct GraphSpec {
670 pub name: Option<Ident>,
672 pub observe_writes: Option<(kw::observe, Expr)>,
674 pub observe_reads: Option<(kw::observe, Expr)>,
676 pub default_executor: Option<Ident>,
679 pub items: Vec<Item>,
681}
682
683pub fn apply_default_executor(items: &mut [Item], ex: &Ident) {
688 fn eligible(source: Option<&TaskSource>) -> bool {
689 match source {
690 Some(TaskSource::Shell(_)) => true,
691 Some(TaskSource::Spawn(e)) => matches!(e, Expr::Path(_) | Expr::Call(_)),
692 None => false,
693 }
694 }
695 for item in items {
696 match item {
697 Item::Node(n) if n.executor.is_none() && eligible(n.source.as_ref()) => {
698 n.executor = Some(ex.clone());
699 n.executor_defaulted = true;
700 }
701 Item::Pool(p) if p.executor.is_none() && eligible(Some(&p.source)) => {
702 p.executor = Some(ex.clone());
703 p.executor_defaulted = true;
704 }
705 _ => {}
706 }
707 }
708}
709
710pub fn item_resources(item: &Item) -> &[ResourceDecl] {
712 match item {
713 Item::Node(n) => &n.resources,
714 Item::Pool(p) => &p.resources,
715 Item::Executor(_) => &[],
716 }
717}
718
719pub fn item_executor(item: &Item) -> Option<&Ident> {
721 match item {
722 Item::Node(n) => n.executor.as_ref(),
723 Item::Pool(p) => p.executor.as_ref(),
724 Item::Executor(_) => None,
725 }
726}
727
728pub fn item_signal_entries(item: &Item) -> impl Iterator<Item = (bool, &SignalDecl)> {
733 let (reads, writes) = match item {
734 Item::Node(n) => (&n.reads[..], &n.writes[..]),
735 Item::Pool(p) => (&p.reads[..], &p.writes[..]),
736 Item::Executor(_) => (&[][..], &[][..]),
737 };
738 reads
739 .iter()
740 .map(|d| (false, d))
741 .chain(writes.iter().map(|d| (true, d)))
742}
743
744pub fn item_ident_cfg(item: &Item) -> Option<(&Ident, &[Attribute])> {
746 match item {
747 Item::Node(n) => Some((&n.ident, &n.cfg)),
748 Item::Pool(p) => Some((&p.ident, &p.cfg)),
749 Item::Executor(_) => None,
750 }
751}
752
753impl Parse for GraphSpec {
754 fn parse(input: ParseStream) -> SynResult<Self> {
755 let name = if input.peek(kw::name) && input.peek2(Token![:]) {
756 input.parse::<kw::name>()?;
757 input.parse::<Token![:]>()?;
758 let n: Ident = input.parse()?;
759 input.parse::<Token![;]>()?;
760 Some(n)
761 } else {
762 None
763 };
764 let mut observe_writes: Option<(kw::observe, Expr)> = None;
765 let mut observe_reads: Option<(kw::observe, Expr)> = None;
766 let mut items = Vec::new();
767 let mut default_executor: Option<Ident> = None;
768 let mut current_fragment: Option<String> = None;
769 while !input.is_empty() {
770 if input.peek(Token![@]) {
771 input.parse::<Token![@]>()?;
772 if input.peek(kw::fragment) {
773 input.parse::<kw::fragment>()?;
774 current_fragment = Some(input.parse::<Ident>()?.to_string());
775 } else if input.peek(kw::endfragment) {
776 input.parse::<kw::endfragment>()?;
777 current_fragment = None;
778 } else {
779 return Err(input.error("expected `@fragment NAME;` or `@endfragment;`"));
780 }
781 input.parse::<Token![;]>()?;
782 continue;
783 }
784 if input.peek(kw::observe) {
785 let k = input.parse::<kw::observe>()?;
786 let (slot, dir) = if input.peek(kw::writes) {
787 input.parse::<kw::writes>()?;
788 (&mut observe_writes, "writes")
789 } else if input.peek(kw::reads) {
790 input.parse::<kw::reads>()?;
791 (&mut observe_reads, "reads")
792 } else {
793 return Err(input.error("expected `observe writes:` or `observe reads:`"));
794 };
795 if slot.is_some() {
796 return Err(syn::Error::new_spanned(
797 k,
798 format!("duplicate `observe {dir}:` default"),
799 ));
800 }
801 input.parse::<Token![:]>()?;
802 *slot = Some((k, input.parse::<Expr>()?));
803 input.parse::<Token![;]>()?;
804 continue;
805 }
806 let cfg = input.call(Attribute::parse_outer)?;
807 if input.peek(kw::node) {
808 let mut n = parse_node(input, cfg)?;
809 n.fragment = current_fragment.clone();
810 items.push(Item::Node(n));
811 } else if input.peek(kw::pool) {
812 let mut p = parse_pool(input, cfg)?;
813 p.fragment = current_fragment.clone();
814 items.push(Item::Pool(p));
815 } else if input.peek(kw::executor) {
816 input.parse::<kw::executor>()?;
817 let ident: Ident = input.parse()?;
818 input.parse::<Token![;]>()?;
819 items.push(Item::Executor(ExecutorItem {
820 cfg,
821 ident,
822 default: false,
823 }));
824 } else if input.peek(Token![default]) && input.peek2(kw::executor) {
825 let k = input.parse::<Token![default]>()?;
826 input.parse::<kw::executor>()?;
827 let ident: Ident = input.parse()?;
828 input.parse::<Token![;]>()?;
829 if let Some(attr) = cfg.first() {
830 return Err(syn::Error::new_spanned(
831 attr,
832 "a `default executor` cannot be `#[cfg]`-gated — every \
833 inheriting node would reference its slot unconditionally; \
834 gate the nodes instead",
835 ));
836 }
837 if current_fragment.is_some() {
838 return Err(syn::Error::new_spanned(
839 k,
840 "a fragment cannot declare the graph's default executor; \
841 declare it at the compose site",
842 ));
843 }
844 if default_executor.is_some() {
845 return Err(syn::Error::new_spanned(k, "duplicate `default executor`"));
846 }
847 default_executor = Some(ident.clone());
848 items.push(Item::Executor(ExecutorItem {
849 cfg,
850 ident,
851 default: true,
852 }));
853 } else {
854 return Err(input.error(
855 "expected `node`, `pool`, `executor`, `default executor`, or \
856 `observe` (optionally `#[cfg(...)]`-prefixed)",
857 ));
858 }
859 }
860 if let Some(ex) = &default_executor {
861 apply_default_executor(&mut items, ex);
862 }
863 Ok(GraphSpec {
864 name,
865 observe_writes,
866 observe_reads,
867 default_executor,
868 items,
869 })
870 }
871}
872
873#[derive(Clone, Default)]
877pub struct CommonClauses {
878 pub executor: Option<Ident>,
880 pub spawn: Option<Expr>,
882 pub task: Option<(kw::task, Expr)>,
884 pub resources: Option<(kw::resources, Vec<ResourceDecl>)>,
886 pub reads: Vec<SignalDecl>,
888 pub writes: Vec<SignalDecl>,
890 pub state: Option<(kw::state, syn::Type, StateInit)>,
892 pub slot_timeout: Option<Gated<kw::slot_timeout, LitInt>>,
894 pub ack_timeout: Option<Gated<kw::ack_timeout, LitInt>>,
896 pub cancel: Option<kw::cancel>,
898 pub discover: Option<Gated<kw::discover>>,
900 pub dataflow: Option<(kw::dataflow, Vec<AdoptedFn>)>,
902}
903
904const COMMON_CLAUSE_NAMES: &str = "`task:`, `spawn:`, `executor:`, `resources:`, \
905 `reads:`, `writes:`, `discover`, `dataflow:`, `state:`, `slot_timeout:`, \
906 `ack_timeout:`, `cancel`";
907
908fn dup_clause<T: quote::ToTokens>(tok: &T, name: &str) -> syn::Error {
909 syn::Error::new_spanned(
910 tok,
911 format!("duplicate `{name}:` clause — one declaration is the contract"),
912 )
913}
914
915pub fn cfg_text(attrs: &[Attribute]) -> String {
917 attrs
918 .iter()
919 .map(|a| {
920 quote::ToTokens::to_token_stream(a)
921 .to_string()
922 .replace(' ', "")
923 })
924 .collect::<Vec<_>>()
925 .join(",")
926}
927
928#[derive(Clone, Copy)]
929enum ClauseHost {
930 Node,
931 Pool,
932}
933
934fn require_cfg_list(a: &Attribute, what: &str) -> SynResult<()> {
935 match &a.meta {
936 Meta::List(l) if l.path.is_ident("cfg") => Ok(()),
937 _ => Err(syn::Error::new_spanned(
938 a,
939 format!("only `#[cfg(...)]` attributes may gate a {what}"),
940 )),
941 }
942}
943
944fn parse_clause_cfg(input: ParseStream, host: ClauseHost) -> SynResult<Vec<Attribute>> {
946 if !input.peek(Token![#]) {
947 return Ok(Vec::new());
948 }
949 let attrs = input.call(Attribute::parse_outer)?;
950 for a in &attrs {
951 require_cfg_list(a, "clause")?;
952 }
953 let common =
954 input.peek(kw::slot_timeout) || input.peek(kw::ack_timeout) || input.peek(kw::discover);
955 let gateable = match host {
956 ClauseHost::Node => {
957 common
958 || input.peek(kw::beat_timeout)
959 || input.peek(kw::beat_window)
960 || input.peek(kw::ready_on_write)
961 || input.peek(kw::disabled)
962 }
963 ClauseHost::Pool => common,
964 };
965 if !gateable {
966 return Err(input.error(match host {
967 ClauseHost::Node => {
968 "`#[cfg(...)]` may only gate `slot_timeout:`, `ack_timeout:`, \
969 `beat_timeout:`, `beat_window:`, `ready_on_write`, `disabled`, \
970 or `discover` — gate the whole node, or a single entry inside \
971 `deps:`/`resources:`/`reads:`/`writes:`/`dataflow:`/`provides:`, \
972 for anything structural"
973 }
974 ClauseHost::Pool => {
975 "`#[cfg(...)]` may only gate `slot_timeout:`, `ack_timeout:`, or \
976 `discover` — gate the whole pool, or a single entry inside \
977 `deps:`/`resources:`/`reads:`/`writes:`/`dataflow:`, for \
978 anything structural"
979 }
980 }));
981 }
982 Ok(attrs)
983}
984
985impl CommonClauses {
986 pub fn parse_one(
988 &mut self,
989 input: ParseStream,
990 clause_cfg: &mut Vec<Attribute>,
991 ) -> SynResult<bool> {
992 if input.peek(kw::spawn) {
993 let k = input.parse::<kw::spawn>()?;
994 input.parse::<Token![:]>()?;
995 if self.spawn.is_some() {
996 return Err(dup_clause(&k, "spawn"));
997 }
998 self.spawn = Some(input.parse::<Expr>()?);
999 } else if input.peek(kw::task) {
1000 let k = input.parse::<kw::task>()?;
1001 input.parse::<Token![:]>()?;
1002 if self.task.is_some() {
1003 return Err(dup_clause(&k, "task"));
1004 }
1005 self.task = Some((k, input.parse::<Expr>()?));
1006 } else if input.peek(kw::executor) {
1007 let k = input.parse::<kw::executor>()?;
1008 input.parse::<Token![:]>()?;
1009 if self.executor.is_some() {
1010 return Err(dup_clause(&k, "executor"));
1011 }
1012 self.executor = Some(input.parse::<Ident>()?);
1013 } else if input.peek(kw::resources) {
1014 let k = input.parse::<kw::resources>()?;
1015 input.parse::<Token![:]>()?;
1016 if self.resources.is_some() {
1017 return Err(dup_clause(&k, "resources"));
1018 }
1019 self.resources = Some((k, parse_resource_list(input)?));
1020 } else if input.peek(kw::reads) {
1021 let k = input.parse::<kw::reads>()?;
1022 input.parse::<Token![:]>()?;
1023 if !self.reads.is_empty() {
1024 return Err(dup_clause(&k, "reads"));
1025 }
1026 self.reads = parse_signal_list(input)?;
1027 check_signal_list(&k, "reads", &self.reads)?;
1028 } else if input.peek(kw::writes) {
1029 let k = input.parse::<kw::writes>()?;
1030 input.parse::<Token![:]>()?;
1031 if !self.writes.is_empty() {
1032 return Err(dup_clause(&k, "writes"));
1033 }
1034 self.writes = parse_signal_list(input)?;
1035 check_signal_list(&k, "writes", &self.writes)?;
1036 } else if input.peek(kw::state) {
1037 let k = input.parse::<kw::state>()?;
1038 input.parse::<Token![:]>()?;
1039 if self.state.is_some() {
1040 return Err(dup_clause(&k, "state"));
1041 }
1042 if input.peek(kw::zeroed) && !input.peek2(Token![=]) {
1043 let z = input.parse::<kw::zeroed>()?;
1044 let ty: syn::Type = input.parse()?;
1045 self.state = Some((k, ty, StateInit::Zeroed(z)));
1046 } else {
1047 let ty: syn::Type = input.parse()?;
1048 input.parse::<Token![=]>()?;
1049 let init: Expr = input.parse()?;
1050 self.state = Some((k, ty, StateInit::Expr(init)));
1051 }
1052 } else if input.peek(kw::slot_timeout) {
1053 let k = input.parse::<kw::slot_timeout>()?;
1054 input.parse::<Token![:]>()?;
1055 if self.slot_timeout.is_some() {
1056 return Err(dup_clause(&k, "slot_timeout"));
1057 }
1058 let st: LitInt = input.parse()?;
1059 if st.base10_parse::<u64>()? == 0 {
1060 return Err(syn::Error::new_spanned(
1061 &st,
1062 "`slot_timeout:` must be at least 1 (milliseconds)",
1063 ));
1064 }
1065 self.slot_timeout = Some(Gated {
1066 cfg: core::mem::take(clause_cfg),
1067 kw: k,
1068 value: st,
1069 });
1070 } else if input.peek(kw::ack_timeout) {
1071 let k = input.parse::<kw::ack_timeout>()?;
1072 input.parse::<Token![:]>()?;
1073 if self.ack_timeout.is_some() {
1074 return Err(dup_clause(&k, "ack_timeout"));
1075 }
1076 let at: LitInt = input.parse()?;
1077 if at.base10_parse::<u64>()? == 0 {
1078 return Err(syn::Error::new_spanned(
1079 &at,
1080 "`ack_timeout:` must be at least 1 (milliseconds)",
1081 ));
1082 }
1083 self.ack_timeout = Some(Gated {
1084 cfg: core::mem::take(clause_cfg),
1085 kw: k,
1086 value: at,
1087 });
1088 } else if input.peek(kw::discover) {
1089 let k = input.parse::<kw::discover>()?;
1090 if self.discover.is_some() {
1091 return Err(syn::Error::new_spanned(k, "duplicate `discover` marker"));
1092 }
1093 if input.peek(Token![:]) {
1094 return Err(syn::Error::new_spanned(
1095 k,
1096 "`discover` takes no argument — the tables come from the \
1097 task fn's `#[dataflow]` attribute, sized by its scan",
1098 ));
1099 }
1100 self.discover = Some(Gated {
1101 cfg: core::mem::take(clause_cfg),
1102 kw: k,
1103 value: (),
1104 });
1105 } else if input.peek(kw::dataflow) {
1106 let k = input.parse::<kw::dataflow>()?;
1107 input.parse::<Token![:]>()?;
1108 if self.dataflow.is_some() {
1109 return Err(dup_clause(&k, "dataflow"));
1110 }
1111 let content;
1112 bracketed!(content in input);
1113 let mut fns: Vec<AdoptedFn> = Vec::new();
1114 while !content.is_empty() {
1115 let cfg = content.call(Attribute::parse_outer)?;
1116 let path: syn::Path = content.parse()?;
1117 if fns
1118 .iter()
1119 .any(|f| tokens_text(&f.path) == tokens_text(&path))
1120 {
1121 return Err(syn::Error::new_spanned(
1122 &path,
1123 "duplicate `dataflow:` fn — one adoption binds its tables",
1124 ));
1125 }
1126 fns.push(AdoptedFn { cfg, path });
1127 if content.peek(Token![,]) {
1128 content.parse::<Token![,]>()?;
1129 }
1130 }
1131 if fns.is_empty() {
1132 return Err(syn::Error::new_spanned(
1133 k,
1134 "`dataflow:` must name at least one `#[dataflow]` fn — omit \
1135 the clause entirely to adopt nothing",
1136 ));
1137 }
1138 self.dataflow = Some((k, fns));
1139 } else if input.peek(kw::cancel) {
1140 let k = input.parse::<kw::cancel>()?;
1141 if self.cancel.is_some() {
1142 return Err(syn::Error::new_spanned(k, "duplicate `cancel` marker"));
1143 }
1144 self.cancel = Some(k);
1145 } else {
1146 return Ok(false);
1147 }
1148 Ok(true)
1149 }
1150}
1151
1152pub fn task_source(
1156 spawn: Option<Expr>,
1157 task: Option<(kw::task, Expr)>,
1158) -> SynResult<Option<TaskSource>> {
1159 if let (Some(_), Some((k, _))) = (&spawn, &task) {
1160 return Err(syn::Error::new_spanned(
1161 k,
1162 "`task:` and `spawn:` are mutually exclusive — `spawn:` names a \
1163 hand-written `#[embassy_executor::task]` fn, `task:` generates one",
1164 ));
1165 }
1166 Ok(match (spawn, task) {
1167 (Some(e), _) => Some(TaskSource::Spawn(e)),
1168 (None, Some((_, e))) => Some(TaskSource::Shell(e)),
1169 (None, None) => None,
1170 })
1171}
1172
1173pub fn parse_node(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<NodeItem> {
1175 input.parse::<kw::node>()?;
1176 let ident: Ident = input.parse()?;
1177 input.parse::<Token![=]>()?;
1178 let mode: Ident = input.parse()?;
1179 let mut deps: Option<Vec<Dep>> = None;
1180
1181 let mut common = CommonClauses::default();
1182 let mut pool_size = None;
1183 let mut disabled: Option<Gated<kw::disabled>> = None;
1184 let mut beat_timeout = None;
1185 let mut beat_window = None;
1186 let mut ready_on_write: Option<Gated<Ident>> = None;
1187 let mut exit: Option<(kw::exit, syn::Type)> = None;
1188 let mut provides: Vec<ProvideDecl> = Vec::new();
1189 let mut provides_kw: Option<kw::provides> = None;
1190 while input.peek(Token![,]) {
1191 input.parse::<Token![,]>()?;
1192 let mut clause_cfg = parse_clause_cfg(input, ClauseHost::Node)?;
1193 if common.parse_one(input, &mut clause_cfg)? {
1194 continue;
1195 }
1196 if input.peek(kw::deps) {
1197 let k = input.parse::<kw::deps>()?;
1198 input.parse::<Token![:]>()?;
1199 if deps.is_some() {
1200 return Err(syn::Error::new_spanned(
1201 k,
1202 "duplicate `deps:` clause — one list declares them all",
1203 ));
1204 }
1205 deps = Some(parse_dep_list(input)?);
1206 } else if input.peek(kw::pool_size) {
1207 input.parse::<kw::pool_size>()?;
1208 input.parse::<Token![:]>()?;
1209 pool_size = Some(input.parse::<LitInt>()?);
1210 } else if input.peek(kw::disabled) {
1211 let k = input.parse::<kw::disabled>()?;
1212 if disabled.is_some() {
1213 return Err(syn::Error::new_spanned(k, "duplicate `disabled` marker"));
1214 }
1215 disabled = Some(Gated {
1216 cfg: clause_cfg,
1217 kw: k,
1218 value: (),
1219 });
1220 } else if input.peek(kw::ready_on_write) {
1221 let k = input.parse::<kw::ready_on_write>()?;
1222 if ready_on_write.is_some() {
1223 return Err(syn::Error::new_spanned(
1224 k,
1225 "duplicate `ready_on_write` marker",
1226 ));
1227 }
1228 ready_on_write = Some(Gated {
1229 cfg: clause_cfg,
1230 kw: Ident::new("ready_on_write", k.span),
1231 value: (),
1232 });
1233 } else if input.peek(kw::beat_timeout) {
1234 let k = input.parse::<kw::beat_timeout>()?;
1235 input.parse::<Token![:]>()?;
1236 if beat_timeout.is_some() {
1237 return Err(dup_clause(&k, "beat_timeout"));
1238 }
1239 beat_timeout = Some(Gated {
1240 cfg: clause_cfg,
1241 kw: k,
1242 value: input.parse::<LitInt>()?,
1243 });
1244 } else if input.peek(kw::beat_window) {
1245 let k = input.parse::<kw::beat_window>()?;
1246 input.parse::<Token![:]>()?;
1247 if beat_window.is_some() {
1248 return Err(dup_clause(&k, "beat_window"));
1249 }
1250 beat_window = Some(Gated {
1251 cfg: clause_cfg,
1252 kw: k,
1253 value: input.parse::<LitInt>()?,
1254 });
1255 } else if input.peek(kw::exit) {
1256 let k = input.parse::<kw::exit>()?;
1257 input.parse::<Token![:]>()?;
1258 exit = Some((k, input.parse::<syn::Type>()?));
1259 } else if input.peek(kw::provides) {
1260 let k = input.parse::<kw::provides>()?;
1261 input.parse::<Token![:]>()?;
1262 let content;
1263 bracketed!(content in input);
1264 while !content.is_empty() {
1265 let entry_cfg = content.call(Attribute::parse_outer)?;
1266 for a in &entry_cfg {
1267 require_cfg_list(a, "`provides:` entry")?;
1268 }
1269 let slot: Ident = content.parse()?;
1270 if provides.iter().any(|p| p.ident == slot) {
1271 return Err(syn::Error::new_spanned(
1272 &slot,
1273 "duplicate `provides:` slot — one entry clears it",
1274 ));
1275 }
1276 provides.push(ProvideDecl {
1277 cfg: entry_cfg,
1278 ident: slot,
1279 });
1280 if content.peek(Token![,]) {
1281 content.parse::<Token![,]>()?;
1282 }
1283 }
1284 if provides.is_empty() {
1285 return Err(syn::Error::new_spanned(
1286 k,
1287 "`provides:` must name at least one resource slot — omit \
1288 the clause entirely to provide nothing",
1289 ));
1290 }
1291 provides_kw = Some(k);
1292 } else {
1293 return Err(input.error(format!(
1294 "expected {COMMON_CLAUSE_NAMES}, `deps:`, `pool_size:`, \
1295 `beat_timeout:`, `beat_window:`, `ready_on_write`, `exit:`, \
1296 `provides:`, or `disabled`"
1297 )));
1298 }
1299 }
1300 input.parse::<Token![;]>()?;
1301 let CommonClauses {
1302 executor,
1303 spawn,
1304 task,
1305 resources,
1306 reads,
1307 writes,
1308 state,
1309 slot_timeout,
1310 ack_timeout,
1311 cancel,
1312 discover,
1313 dataflow,
1314 } = common;
1315
1316 if let Some(bt) = &beat_timeout
1317 && bt.value.base10_parse::<u64>()? == 0
1318 {
1319 return Err(syn::Error::new_spanned(
1320 &bt.value,
1321 "`beat_timeout:` must be at least 1 (milliseconds) — omit the clause \
1322 to leave the node unpoliced",
1323 ));
1324 }
1325
1326 if let (Some(bw), None) = (&beat_window, &beat_timeout) {
1327 return Err(syn::Error::new_spanned(
1328 &bw.value,
1329 "`beat_window:` requires `beat_timeout:` — the window counts \
1330 consecutive sweeps that found the node past its beat budget",
1331 ));
1332 }
1333
1334 if let Some(bw) = &beat_window
1335 && !(1..=255).contains(&bw.value.base10_parse::<u64>()?)
1336 {
1337 return Err(syn::Error::new_spanned(
1338 &bw.value,
1339 "`beat_window:` must be in 1..=255 — omit the clause for the \
1340 default of 1, which reports on the first stale sweep",
1341 ));
1342 }
1343
1344 if let Some(bt) = &beat_timeout
1345 && !bt.cfg.is_empty()
1346 {
1347 if let Some(bw) = &beat_window
1348 && cfg_text(&bw.cfg) != cfg_text(&bt.cfg)
1349 {
1350 return Err(syn::Error::new_spanned(
1351 &bw.value,
1352 "`beat_window:` must carry the same `#[cfg]` predicate as its \
1353 `beat_timeout:` — the window counts sweeps of a budget that \
1354 gate compiles out",
1355 ));
1356 }
1357 if let Some(row) = &ready_on_write
1358 && cfg_text(&row.cfg) != cfg_text(&bt.cfg)
1359 {
1360 return Err(syn::Error::new_spanned(
1361 &row.kw,
1362 "`ready_on_write` must carry the same `#[cfg]` predicate as its \
1363 `beat_timeout:` — readiness is asserted by the monitor sweep, \
1364 which that gate compiles out",
1365 ));
1366 }
1367 }
1368
1369 for d in &reads {
1370 if let Some(b) = &d.beat {
1371 return Err(syn::Error::new_spanned(
1372 b,
1373 "`beat` belongs on a `writes:` entry — a node's heartbeat is \
1374 something it produces, not something it consumes",
1375 ));
1376 }
1377 }
1378 for d in &reads {
1379 if let Some(v) = &d.veto {
1380 return Err(syn::Error::new_spanned(
1381 v,
1382 "`veto` belongs on a `writes:` entry — a veto is something a node \
1383 asserts, not something it consumes",
1384 ));
1385 }
1386 }
1387
1388 if let Some(k) = &discover {
1389 for d in reads.iter().chain(writes.iter()) {
1390 if d.observed.is_none() && d.beat.is_none() && d.veto.is_none() {
1391 return Err(syn::Error::new_spanned(
1392 &d.path,
1393 "beside `discover`, a `reads:`/`writes:` entry may only add \
1394 markers (`observed`, `beat`) to a signal the task fn \
1395 already accesses — this one carries none, so it would \
1396 declare a coupling the scan did not find. Drop the entry, \
1397 or drop `discover` and declare the whole relation",
1398 ));
1399 }
1400 }
1401 if spawn.is_none() && task.is_none() {
1402 return Err(syn::Error::new_spanned(
1403 k.kw,
1404 "`discover` needs a `task:`/`spawn:` fn to take its tables \
1405 from — a parked node has nothing to scan",
1406 ));
1407 }
1408 }
1409
1410 if let Some(row) = &ready_on_write {
1411 if !writes
1412 .iter()
1413 .any(|w| w.beat.is_some() && w.observed.is_some())
1414 {
1415 return Err(syn::Error::new_spanned(
1416 &row.kw,
1417 "`ready_on_write` requires an `observed beat` entry in \
1418 `writes:` — the sweep's own poll of that write is what asserts \
1419 the readiness. A body that beats through its verbs asserts \
1420 readiness itself, with `set_ready()` at the same write",
1421 ));
1422 }
1423 if beat_timeout.is_none() {
1424 return Err(syn::Error::new_spanned(
1425 &row.kw,
1426 "`ready_on_write` requires `beat_timeout:` — readiness is \
1427 asserted from the monitor sweep, which only visits nodes that \
1428 declare a beat budget",
1429 ));
1430 }
1431 }
1432
1433 if let (Some(ps), None) = (&pool_size, &task) {
1434 return Err(syn::Error::new_spanned(
1435 ps,
1436 "`pool_size:` requires `task:` — a `spawn:` task fn sets its own \
1437 `#[embassy_executor::task(pool_size = ...)]`",
1438 ));
1439 }
1440 if let Some((k, decls)) = &resources {
1441 if task.is_none() {
1442 return Err(syn::Error::new_spanned(
1443 k,
1444 "`resources:` requires `task:` — resources are handed to the \
1445 generated shell as owned arguments and restored by it; a \
1446 `spawn:` task fn manages its own arguments",
1447 ));
1448 }
1449 if decls.is_empty() {
1450 return Err(syn::Error::new_spanned(
1451 k,
1452 "`resources:` must declare at least one `NAME: Type` entry",
1453 ));
1454 }
1455 for (i, d) in decls.iter().enumerate() {
1456 if decls[..i].iter().any(|prev| prev.ident == d.ident) {
1457 return Err(syn::Error::new_spanned(
1458 &d.ident,
1459 format!("duplicate resource name `{}`", d.ident),
1460 ));
1461 }
1462 }
1463 }
1464 if let Some(ps) = &pool_size
1465 && ps.base10_parse::<usize>()? == 0
1466 {
1467 return Err(syn::Error::new_spanned(
1468 ps,
1469 "`pool_size:` must be at least 1",
1470 ));
1471 }
1472 if let Some((k, _, _)) = &state
1473 && task.is_none()
1474 {
1475 return Err(syn::Error::new_spanned(
1476 k,
1477 "`state:` requires `task:` — the generated shell owns the boxed \
1478 state across the worker call and drops it on exit; a `spawn:` \
1479 task fn can Box its own state",
1480 ));
1481 }
1482 if let Some((k, _)) = &exit
1483 && task.is_none()
1484 {
1485 return Err(syn::Error::new_spanned(
1486 k,
1487 "`exit:` requires `task:` — the generated shell is what captures \
1488 the worker's return value; a `spawn:` task fn can provide() into \
1489 a slot itself",
1490 ));
1491 }
1492 if let Some(k) = &cancel {
1493 if task.is_none() {
1494 return Err(syn::Error::new_spanned(
1495 k,
1496 "`cancel` requires `task:` — it wraps the generated shell's call \
1497 to the worker; a `spawn:` task fn can call \
1498 `node.run_cancellable(..)` itself",
1499 ));
1500 }
1501 if mode == "Pause" {
1502 return Err(syn::Error::new_spanned(
1503 k,
1504 "`cancel` cannot be combined with `Mode::Pause` — a Pause worker \
1505 must survive the stop and park on `wait_resume()`, but `cancel` \
1506 drops its future and records an exit; use `Mode::Terminate` (or \
1507 `OnDemand`), or drive the pause by hand in the worker",
1508 ));
1509 }
1510 }
1511 let source = task_source(spawn, task)?;
1512
1513 Ok(NodeItem {
1514 cfg,
1515 ident,
1516 mode,
1517 deps: deps.unwrap_or_default(),
1518 source,
1519 pool_size,
1520 disabled,
1521 executor,
1522 executor_defaulted: false,
1523 resources: resources.map(|(_, decls)| decls).unwrap_or_default(),
1524 slot_timeout,
1525 ack_timeout,
1526 beat_timeout,
1527 beat_window,
1528 ready_on_write,
1529 reads,
1530 writes,
1531 exit: exit.map(|(_, ty)| ty),
1532 state,
1533 cancel: cancel.is_some(),
1534 discover,
1535 dataflow: dataflow.map(|(_, f)| f).unwrap_or_default(),
1536 provides,
1537 provides_kw,
1538 fragment: None,
1539 })
1540}
1541
1542pub fn parse_pool(input: ParseStream, cfg: Vec<Attribute>) -> SynResult<PoolItem> {
1544 input.parse::<kw::pool>()?;
1545 let ident: Ident = input.parse()?;
1546 input.parse::<Token![=]>()?;
1547 let modes = parse_mode_list(input)?;
1548
1549 let mut common = CommonClauses::default();
1550 let mut deps: Option<Vec<Dep>> = None;
1551 let mut policy: Option<Expr> = None;
1552 let mut policy_ty: Option<Type> = None;
1553 let mut min: Option<Expr> = None;
1554 let mut max: Option<Expr> = None;
1555
1556 while input.peek(Token![,]) {
1557 input.parse::<Token![,]>()?;
1558 let mut clause_cfg = parse_clause_cfg(input, ClauseHost::Pool)?;
1559 if common.parse_one(input, &mut clause_cfg)? {
1560 continue;
1561 }
1562 if input.peek(kw::deps) {
1563 let k = input.parse::<kw::deps>()?;
1564 input.parse::<Token![:]>()?;
1565 if deps.is_some() {
1566 return Err(syn::Error::new_spanned(
1567 k,
1568 "duplicate `deps:` clause — one list declares them all",
1569 ));
1570 }
1571 deps = Some(parse_dep_list(input)?);
1572 } else if input.peek(kw::policy) {
1573 let k = input.parse::<kw::policy>()?;
1574 input.parse::<Token![:]>()?;
1575 if policy.is_some() {
1576 return Err(dup_clause(&k, "policy"));
1577 }
1578 policy_ty = {
1579 let fork = input.fork();
1580 if fork.parse::<Type>().is_ok() && fork.peek(Token![=]) {
1581 let ty: Type = input.parse()?;
1582 input.parse::<Token![=]>()?;
1583 Some(ty)
1584 } else {
1585 None
1586 }
1587 };
1588 policy = Some(input.parse::<Expr>()?);
1589 } else if input.peek(kw::min) {
1590 let k = input.parse::<kw::min>()?;
1591 input.parse::<Token![:]>()?;
1592 if min.is_some() {
1593 return Err(dup_clause(&k, "min"));
1594 }
1595 min = Some(input.parse::<Expr>()?);
1596 } else if input.peek(kw::max) {
1597 let k = input.parse::<kw::max>()?;
1598 input.parse::<Token![:]>()?;
1599 if max.is_some() {
1600 return Err(dup_clause(&k, "max"));
1601 }
1602 max = Some(input.parse::<Expr>()?);
1603 } else if input.peek(kw::exit) {
1604 let k = input.parse::<kw::exit>()?;
1605 return Err(syn::Error::new_spanned(
1606 k,
1607 "`exit:` is not supported on `pool` — the K members share one shell, \
1608 so per-member exit values need per-member storage; use per-node \
1609 `exit:` declarations, or have the worker provide() into an \
1610 app-declared slot itself",
1611 ));
1612 } else {
1613 return Err(input.error(format!(
1614 "expected {COMMON_CLAUSE_NAMES}, `deps:`, `policy:`, `min:`, or `max:`"
1615 )));
1616 }
1617 }
1618 input.parse::<Token![;]>()?;
1619 let CommonClauses {
1620 executor,
1621 spawn,
1622 task,
1623 resources,
1624 reads,
1625 writes,
1626 state,
1627 slot_timeout,
1628 ack_timeout,
1629 cancel,
1630 discover,
1631 dataflow,
1632 } = common;
1633 let source = task_source(spawn, task)?;
1634 let resources = resources.map(|(_, decls)| decls).unwrap_or_default();
1635
1636 if let Some(bad) = resources
1637 .iter()
1638 .find(|d| d.local.is_some() && d.shared.is_none())
1639 {
1640 return Err(syn::Error::new_spanned(
1641 &bad.ident,
1642 "`local` is not supported on take-kind `pool` resources (the one-executor \
1643 slot contract + per-member restore is deferred); a `shared local` entry \
1644 works (one pool-wide fan-out slot), or declare the take-kind `local` \
1645 resource on a node",
1646 ));
1647 }
1648
1649 let mut absent: Vec<&str> = Vec::new();
1650 if source.is_none() {
1651 absent.push("`task:` or `spawn:`");
1652 }
1653 if policy.is_none() {
1654 absent.push("`policy:`");
1655 }
1656 if min.is_none() {
1657 absent.push("`min:`");
1658 }
1659 if max.is_none() {
1660 absent.push("`max:`");
1661 }
1662 if !absent.is_empty() {
1663 return Err(syn::Error::new_spanned(
1664 &ident,
1665 format!(
1666 "`pool {ident}` is missing {} — an elastic pool needs its \
1667 dependencies, a member task, a scaling policy, and the \
1668 floor/ceiling the policy scales between",
1669 absent.join(", ")
1670 ),
1671 ));
1672 }
1673 let source = source.expect("absence checked above");
1674
1675 if let Some(k) = &cancel {
1676 if matches!(source, TaskSource::Spawn(_)) {
1677 return Err(syn::Error::new_spanned(
1678 k,
1679 "`cancel` requires `task:` — it wraps the generated shell's call to \
1680 the member worker; a `spawn:` member fn can call \
1681 `node.run_cancellable(..)` itself",
1682 ));
1683 }
1684 if let Some(m) = modes.iter().find(|m| *m == "Pause") {
1685 return Err(syn::Error::new_spanned(
1686 m,
1687 "`cancel` cannot be combined with a `Pause` member — a Pause worker \
1688 must survive the stop and park on `wait_resume()`, but `cancel` \
1689 drops its future and records an exit; use `Terminate` (or \
1690 `OnDemand`) members, or drive the pause by hand in the worker",
1691 ));
1692 }
1693 }
1694 Ok(PoolItem {
1695 cfg,
1696 ident,
1697 modes,
1698 deps: deps.unwrap_or_default(),
1699 source,
1700 policy: policy.expect("absence checked above"),
1701 policy_ty,
1702 executor,
1703 executor_defaulted: false,
1704 resources,
1705 slot_timeout,
1706 ack_timeout,
1707 reads,
1708 writes,
1709 min: min.expect("absence checked above"),
1710 max: max.expect("absence checked above"),
1711 state,
1712 cancel: cancel.is_some(),
1713 discover,
1714 dataflow: dataflow.map(|(_, f)| f).unwrap_or_default(),
1715 fragment: None,
1716 })
1717}
1718pub fn name_string(ident: &Ident) -> String {
1722 ident.to_string().to_lowercase().replace('_', "-")
1723}
1724
1725pub fn normalize_fragment_crate(stream: TokenStream2) -> TokenStream2 {
1731 use proc_macro2::{Punct, Spacing, TokenStream as TS, TokenTree};
1732 let mut out = TS::new();
1733 let mut iter = stream.into_iter().peekable();
1734 while let Some(tt) = iter.next() {
1735 match tt {
1736 TokenTree::Group(g) => {
1737 let inner = normalize_fragment_crate(g.stream());
1738 let mut ng = proc_macro2::Group::new(g.delimiter(), inner);
1739 ng.set_span(g.span());
1740 out.extend([TokenTree::Group(ng)]);
1741 }
1742 TokenTree::Punct(p) if p.as_char() == '$' => {
1743 out.extend([TokenTree::Punct(p)]);
1744 if let Some(TokenTree::Ident(i)) = iter.peek()
1745 && i == "crate"
1746 {
1747 let i = iter.next().expect("peeked");
1748 out.extend([i]);
1749 }
1750 }
1751 TokenTree::Ident(i) if i == "crate" => {
1752 let mut dollar = Punct::new('$', Spacing::Joint);
1753 dollar.set_span(i.span());
1754 out.extend([TokenTree::Punct(dollar), TokenTree::Ident(i)]);
1755 }
1756 other => out.extend([other]),
1757 }
1758 }
1759 out
1760}
1761
1762pub fn substitute_dollar_crate(stream: TokenStream2, replacement: &TokenStream2) -> TokenStream2 {
1767 use proc_macro2::{TokenStream as TS, TokenTree};
1768 let mut out = TS::new();
1769 let mut iter = stream.into_iter().peekable();
1770 while let Some(tt) = iter.next() {
1771 match tt {
1772 TokenTree::Group(g) => {
1773 let inner = substitute_dollar_crate(g.stream(), replacement);
1774 let mut ng = proc_macro2::Group::new(g.delimiter(), inner);
1775 ng.set_span(g.span());
1776 out.extend([TokenTree::Group(ng)]);
1777 }
1778 TokenTree::Punct(p) if p.as_char() == '$' => {
1779 if let Some(TokenTree::Ident(i)) = iter.peek()
1780 && i == "crate"
1781 {
1782 let span = iter.next().map(|t| t.span()).unwrap_or_else(|| p.span());
1783 out.extend(respan(replacement.clone(), span));
1784 } else {
1785 out.extend([TokenTree::Punct(p)]);
1786 }
1787 }
1788 other => out.extend([other]),
1789 }
1790 }
1791 out
1792}
1793
1794fn respan(stream: TokenStream2, span: proc_macro2::Span) -> TokenStream2 {
1795 stream
1796 .into_iter()
1797 .map(|mut tt| {
1798 tt.set_span(span);
1799 tt
1800 })
1801 .collect()
1802}
1803
1804#[derive(Clone)]
1806pub struct VerbCall {
1807 pub verb: String,
1809 pub cfgs: Vec<TokenStream2>,
1811 pub write: bool,
1813 pub target: Expr,
1815 pub path: String,
1817}
1818
1819pub const BUILTIN_READS: &[&str] = &["get", "reader", "open", "lease"];
1821pub const BUILTIN_WRITES: &[&str] = &["put", "writer", "beat_put", "beat_writer", "retire", "veto"];
1823
1824#[derive(Debug, Clone, Default)]
1826pub struct VerbTable {
1827 custom: Vec<(String, bool)>,
1828}
1829
1830impl VerbTable {
1831 pub fn builtin() -> Self {
1833 Self::default()
1834 }
1835
1836 pub fn direction(&self, ident: &str) -> Option<bool> {
1841 if BUILTIN_WRITES.contains(&ident) {
1842 return Some(true);
1843 }
1844 if BUILTIN_READS.contains(&ident) {
1845 return Some(false);
1846 }
1847 self.custom
1848 .iter()
1849 .find(|(n, _)| n == ident)
1850 .map(|(_, w)| *w)
1851 }
1852
1853 fn add(&mut self, name: &Ident, write: bool) -> SynResult<()> {
1854 let text = name.to_string();
1855 if BUILTIN_READS.contains(&text.as_str()) || BUILTIN_WRITES.contains(&text.as_str()) {
1856 return Err(syn::Error::new_spanned(
1857 name,
1858 format!(
1859 "`{text}` is a built-in verb and is always recognised: \
1860 registering it would either repeat what the crate already \
1861 says or contradict it. Give the new verb its own name"
1862 ),
1863 ));
1864 }
1865 if self.custom.iter().any(|(n, _)| *n == text) {
1866 return Err(syn::Error::new_spanned(
1867 name,
1868 format!("`{text}` is registered twice; a verb points one way"),
1869 ));
1870 }
1871 self.custom.push((text, write));
1872 Ok(())
1873 }
1874}
1875
1876impl Parse for VerbTable {
1877 fn parse(input: ParseStream) -> SynResult<Self> {
1878 let mut table = VerbTable::builtin();
1879 while !input.is_empty() {
1880 let kw: Ident = input.parse().map_err(|_| {
1881 syn::Error::new(
1882 input.span(),
1883 "`#[dataflow]` takes verb registrations: \
1884 `read(<verb>, ..)` / `write(<verb>, ..)`, naming methods \
1885 your own extension trait adds to `TaskNode`. A derived \
1886 table states this fn's couplings and nothing else",
1887 )
1888 })?;
1889 let write = match kw.to_string().as_str() {
1890 "read" => false,
1891 "write" => true,
1892 other => {
1893 return Err(syn::Error::new_spanned(
1894 &kw,
1895 format!(
1896 "expected `read(..)` or `write(..)`, found `{other}`. \
1897 The walker has no type information, so which way a \
1898 registered verb points is stated here"
1899 ),
1900 ));
1901 }
1902 };
1903 let names;
1904 syn::parenthesized!(names in input);
1905 let names = names.parse_terminated(Ident::parse, Token![,])?;
1906 if names.is_empty() {
1907 return Err(syn::Error::new_spanned(
1908 &kw,
1909 format!("`{kw}(..)` names no verb"),
1910 ));
1911 }
1912 for name in &names {
1913 table.add(name, write)?;
1914 }
1915 if !input.is_empty() {
1916 input.parse::<Token![,]>()?;
1917 }
1918 }
1919 Ok(table)
1920 }
1921}
1922
1923pub fn verb_table_of(attr: &Attribute) -> VerbTable {
1929 match &attr.meta {
1930 Meta::List(list) => syn::parse2(list.tokens.clone()).unwrap_or_default(),
1931 _ => VerbTable::builtin(),
1932 }
1933}
1934
1935pub fn rewrite_verb_calls(
1942 body: TokenStream2,
1943 node_param: &str,
1944 verbs: &VerbTable,
1945 on_call: &mut dyn FnMut(VerbCall) -> SynResult<Option<TokenStream2>>,
1946) -> SynResult<TokenStream2> {
1947 rewrite_verb_calls_in(body, node_param, verbs, &[], on_call)
1948}
1949
1950fn cfg_attr_predicate(g: &proc_macro2::Group) -> Option<TokenStream2> {
1951 use proc_macro2::{Delimiter, TokenTree};
1952 if g.delimiter() != Delimiter::Bracket {
1953 return None;
1954 }
1955 let mut it = g.stream().into_iter();
1956 match (it.next(), it.next(), it.next()) {
1957 (Some(TokenTree::Ident(i)), Some(TokenTree::Group(p)), None)
1958 if i == "cfg" && p.delimiter() == Delimiter::Parenthesis =>
1959 {
1960 Some(p.stream())
1961 }
1962 _ => None,
1963 }
1964}
1965
1966fn rewrite_verb_calls_in(
1967 body: TokenStream2,
1968 node_param: &str,
1969 verbs: &VerbTable,
1970 inherited: &[TokenStream2],
1971 on_call: &mut dyn FnMut(VerbCall) -> SynResult<Option<TokenStream2>>,
1972) -> SynResult<TokenStream2> {
1973 use proc_macro2::{Delimiter, Group, TokenTree};
1974
1975 let toks: Vec<TokenTree> = body.into_iter().collect();
1976 let mut out = TokenStream2::new();
1977 let mut i = 0;
1978 let mut pending: Vec<TokenStream2> = Vec::new();
1979 let mut current: Vec<TokenStream2> = Vec::new();
1980 while i < toks.len() {
1981 if let TokenTree::Punct(p) = &toks[i]
1982 && p.as_char() == '#'
1983 && let Some(TokenTree::Group(g)) = toks.get(i + 1)
1984 && g.delimiter() == Delimiter::Bracket
1985 {
1986 if let Some(pred) = cfg_attr_predicate(g) {
1987 pending.push(pred);
1988 }
1989 out.extend([toks[i].clone(), toks[i + 1].clone()]);
1990 i += 2;
1991 continue;
1992 }
1993 if !pending.is_empty() {
1994 current.append(&mut pending);
1995 }
1996 if matches!(&toks[i], TokenTree::Ident(id) if *id == "fn")
1997 && matches!(toks.get(i + 1), Some(TokenTree::Ident(_)))
1998 {
1999 while i < toks.len() {
2000 let done = matches!(&toks[i],
2001 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace)
2002 || matches!(&toks[i], TokenTree::Punct(p) if p.as_char() == ';');
2003 out.extend([toks[i].clone()]);
2004 i += 1;
2005 if done {
2006 break;
2007 }
2008 }
2009 current.clear();
2010 continue;
2011 }
2012 let own_receiver = i == 0
2013 || !matches!(&toks[i - 1], TokenTree::Punct(p)
2014 if p.as_char() == '.' || p.as_char() == ':');
2015 let matched = if own_receiver && i + 3 < toks.len() {
2016 match (&toks[i], &toks[i + 1], &toks[i + 2], &toks[i + 3]) {
2017 (
2018 TokenTree::Ident(n),
2019 TokenTree::Punct(dot),
2020 TokenTree::Ident(verb),
2021 TokenTree::Group(g),
2022 ) if *n == node_param
2023 && dot.as_char() == '.'
2024 && g.delimiter() == Delimiter::Parenthesis =>
2025 {
2026 let name = verb.to_string();
2027 verbs.direction(&name).map(|write| (name, write))
2028 }
2029 _ => None,
2030 }
2031 } else {
2032 None
2033 };
2034 if let Some((verb, write)) = matched {
2035 let TokenTree::Group(g) = &toks[i + 3] else {
2036 unreachable!()
2037 };
2038 let mut cfgs: Vec<TokenStream2> = inherited.to_vec();
2039 cfgs.extend(current.iter().cloned());
2040 let rebuilt = rewrite_first_arg(g, verb, write, node_param, verbs, &cfgs, on_call)?;
2041 out.extend(toks[i..i + 3].iter().cloned());
2042 let mut ng = Group::new(Delimiter::Parenthesis, rebuilt);
2043 ng.set_span(g.span());
2044 out.extend([TokenTree::Group(ng)]);
2045 i += 4;
2046 continue;
2047 }
2048 match &toks[i] {
2049 TokenTree::Group(g) => {
2050 let mut child = inherited.to_vec();
2051 child.extend(current.iter().cloned());
2052 let inner = rewrite_verb_calls_in(g.stream(), node_param, verbs, &child, on_call)?;
2053 let mut ng = Group::new(g.delimiter(), inner);
2054 ng.set_span(g.span());
2055 out.extend([TokenTree::Group(ng)]);
2056 if g.delimiter() == Delimiter::Brace {
2057 let continues = matches!(
2058 toks.get(i + 1),
2059 Some(TokenTree::Ident(id)) if *id == "else"
2060 ) || matches!(
2061 toks.get(i + 1),
2062 Some(TokenTree::Punct(p)) if matches!(p.as_char(), '.' | '?')
2063 );
2064 if !continues {
2065 current.clear();
2066 }
2067 }
2068 }
2069 TokenTree::Punct(p) if matches!(p.as_char(), ';' | ',') => {
2070 out.extend([toks[i].clone()]);
2071 current.clear();
2072 }
2073 t => out.extend([t.clone()]),
2074 }
2075 i += 1;
2076 }
2077 Ok(out)
2078}
2079
2080fn rewrite_first_arg(
2081 g: &proc_macro2::Group,
2082 verb: String,
2083 write: bool,
2084 node_param: &str,
2085 verbs: &VerbTable,
2086 cfgs: &[TokenStream2],
2087 on_call: &mut dyn FnMut(VerbCall) -> SynResult<Option<TokenStream2>>,
2088) -> SynResult<TokenStream2> {
2089 use proc_macro2::TokenTree;
2090
2091 let toks: Vec<TokenTree> = g.stream().into_iter().collect();
2092 let split = toks
2093 .iter()
2094 .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ','))
2095 .unwrap_or(toks.len());
2096 let (first, rest) = toks.split_at(split);
2097 let arg: TokenStream2 = first.iter().cloned().collect();
2098 let target = match syn::parse2::<Expr>(arg.clone()) {
2099 Ok(Expr::Reference(r))
2100 if matches!(*r.expr, Expr::Path(_)) || matches!(*r.expr, Expr::Index(_)) =>
2101 {
2102 *r.expr
2103 }
2104 _ => {
2105 return Err(syn::Error::new_spanned(
2106 &arg,
2107 "the supervisor derives dataflow from the literal path: name \
2108 the signal directly (`&path::TO_SIGNAL`, `&ARR[i]`)",
2109 ));
2110 }
2111 };
2112 let path = tokens_text(&target);
2113 let replacement = on_call(VerbCall {
2114 verb,
2115 write,
2116 target,
2117 path,
2118 cfgs: cfgs.to_vec(),
2119 })?;
2120 let mut out = replacement.unwrap_or(arg);
2121 out.extend(rewrite_verb_calls_in(
2122 rest.iter().cloned().collect(),
2123 node_param,
2124 verbs,
2125 cfgs,
2126 on_call,
2127 )?);
2128 Ok(out)
2129}
2130
2131fn tokens_text<T: quote::ToTokens>(t: &T) -> String {
2132 t.to_token_stream().to_string().replace(' ', "")
2133}
2134
2135pub fn node_param(sig: &syn::Signature) -> Option<Ident> {
2139 sig.inputs.iter().find_map(|arg| match arg {
2140 syn::FnArg::Typed(t) if tokens_text(&t.ty).contains("TaskNode") => match &*t.pat {
2141 syn::Pat::Ident(p) => Some(p.ident.clone()),
2142 _ => None,
2143 },
2144 _ => None,
2145 })
2146}
2147
2148pub fn is_dataflow_attr(attr: &Attribute) -> bool {
2151 dataflow_attr(attr).is_some()
2152}
2153
2154pub fn dataflow_attr(attr: &Attribute) -> Option<(Attribute, Option<String>)> {
2168 let (meta, preds) = unwrap_cfg_attr(&attr.meta)?;
2169 let attr = if preds.is_empty() {
2170 attr.clone()
2171 } else {
2172 syn::parse_quote!(#[#meta])
2173 };
2174 let cfg = match preds.as_slice() {
2175 [] => None,
2176 [one] => Some(one.clone()),
2177 many => Some(format!("all({})", many.join(","))),
2178 };
2179 Some((attr, cfg))
2180}
2181
2182fn unwrap_cfg_attr(meta: &Meta) -> Option<(Meta, Vec<String>)> {
2185 if meta
2186 .path()
2187 .segments
2188 .last()
2189 .is_some_and(|s| s.ident == "dataflow")
2190 {
2191 return Some((meta.clone(), Vec::new()));
2192 }
2193 let Meta::List(list) = meta else {
2194 return None;
2195 };
2196 if !list.path.is_ident("cfg_attr") {
2197 return None;
2198 }
2199 let mut args = list
2200 .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
2201 .ok()?
2202 .into_iter();
2203 let pred = tokens_text(&args.next()?);
2204 let (inner, mut preds) = args.find_map(|m| unwrap_cfg_attr(&m))?;
2205 preds.insert(0, pred);
2206 Some((inner, preds))
2207}
2208
2209#[derive(Debug, Clone, PartialEq, Eq)]
2211pub struct Access {
2212 pub func: String,
2214 pub verb: String,
2216 pub write: bool,
2218 pub path: String,
2220 pub cfgs: Vec<String>,
2226}
2227
2228pub fn scan_dataflow(src: &str, out: &mut Vec<Access>) {
2234 let Ok(file) = syn::parse_file(src) else {
2235 return;
2236 };
2237 for item in &file.items {
2238 scan_item(item, out);
2239 }
2240}
2241
2242fn scan_item(item: &syn::Item, out: &mut Vec<Access>) {
2243 match item {
2244 syn::Item::Fn(f) => scan_fn(&f.attrs, &f.sig, &f.block, out),
2245 syn::Item::Mod(m) => {
2246 if let Some((_, items)) = &m.content {
2247 for i in items {
2248 scan_item(i, out);
2249 }
2250 }
2251 }
2252 syn::Item::Impl(im) => {
2253 for ii in &im.items {
2254 if let syn::ImplItem::Fn(f) = ii {
2255 scan_fn(&f.attrs, &f.sig, &f.block, out);
2256 }
2257 }
2258 }
2259 _ => {}
2260 }
2261}
2262
2263fn scan_fn(attrs: &[Attribute], sig: &syn::Signature, block: &syn::Block, out: &mut Vec<Access>) {
2264 let Some((attr, wrap_cfg)) = attrs.iter().find_map(dataflow_attr) else {
2265 return;
2266 };
2267 let Some(param) = node_param(sig) else {
2268 return;
2269 };
2270 let verbs = verb_table_of(&attr);
2271 let func = sig.ident.to_string();
2272 let fn_cfgs: Vec<String> = attrs
2276 .iter()
2277 .filter_map(|a| match &a.meta {
2278 syn::Meta::List(l) if l.path.is_ident("cfg") => {
2279 Some(l.tokens.to_string().replace(' ', ""))
2280 }
2281 _ => None,
2282 })
2283 .chain(wrap_cfg)
2284 .collect();
2285 let mut seen: Vec<(bool, String)> = Vec::new();
2286 let _ = rewrite_verb_calls(quote!(#block), ¶m.to_string(), &verbs, &mut |call| {
2287 if !seen.contains(&(call.write, call.path.clone())) {
2288 seen.push((call.write, call.path.clone()));
2289 let mut cfgs: Vec<String> = Vec::new();
2293 for c in fn_cfgs
2294 .iter()
2295 .cloned()
2296 .chain(call.cfgs.iter().map(|c| c.to_string().replace(' ', "")))
2297 {
2298 if !cfgs.contains(&c) {
2299 cfgs.push(c);
2300 }
2301 }
2302 out.push(Access {
2303 func: func.clone(),
2304 verb: call.verb.clone(),
2305 write: call.write,
2306 path: call.path.clone(),
2307 cfgs,
2308 });
2309 }
2310 Ok(None)
2311 });
2312}
2313
2314#[cfg(test)]
2315mod tests {
2316 use super::*;
2317
2318 #[test]
2319 fn unknown_dep_marker_rejected() {
2320 match syn::parse_str::<GraphSpec>("node A = Terminate, deps: [B rdy];") {
2321 Ok(_) => panic!("unknown marker accepted"),
2322 Err(err) => {
2323 let msg = err.to_string();
2324 assert!(msg.contains("dep marker"), "got: {msg}");
2325 assert!(msg.contains("ready"), "got: {msg}");
2326 assert!(msg.contains("rdy"), "names the offending token: {msg}");
2327 }
2328 }
2329 }
2330
2331 #[test]
2332 fn beat_timeout_zero_rejected() {
2333 match syn::parse_str::<GraphSpec>("node A = Terminate, deps: [], beat_timeout: 0;") {
2334 Ok(_) => panic!("zero budget accepted"),
2335 Err(err) => assert!(
2336 err.to_string()
2337 .contains("`beat_timeout:` must be at least 1"),
2338 "got: {err}"
2339 ),
2340 }
2341 }
2342
2343 #[test]
2344 fn beat_window_without_timeout_rejected() {
2345 match syn::parse_str::<GraphSpec>("node A = Terminate, deps: [], beat_window: 3;") {
2346 Ok(_) => panic!("orphan window accepted"),
2347 Err(err) => assert!(
2348 err.to_string()
2349 .contains("`beat_window:` requires `beat_timeout:`"),
2350 "got: {err}"
2351 ),
2352 }
2353 }
2354
2355 #[test]
2356 fn beat_window_out_of_range_rejected() {
2357 for w in ["0", "256", "300"] {
2358 let src = format!("node A = Terminate, deps: [], beat_timeout: 100, beat_window: {w};");
2359 match syn::parse_str::<GraphSpec>(&src) {
2360 Ok(_) => panic!("`beat_window: {w}` accepted"),
2361 Err(err) => assert!(
2362 err.to_string()
2363 .contains("`beat_window:` must be in 1..=255"),
2364 "got: {err}"
2365 ),
2366 }
2367 }
2368 assert!(
2369 syn::parse_str::<GraphSpec>(
2370 "node A = Terminate, deps: [], beat_timeout: 100, beat_window: 255;"
2371 )
2372 .is_ok(),
2373 "255 is in range"
2374 );
2375 }
2376
2377 #[test]
2378 fn ready_on_write_needs_a_heartbeat_source() {
2379 let nothing = "node A = Terminate, deps: [], beat_timeout: 100, \
2380 ready_on_write, writes: [crate::X];";
2381 match syn::parse_str::<GraphSpec>(nothing) {
2382 Ok(_) => panic!("accepted with no `beat` write"),
2383 Err(err) => assert!(
2384 err.to_string()
2385 .contains("requires an `observed beat` entry"),
2386 "got: {err}"
2387 ),
2388 }
2389
2390 let no_budget = "observe writes: it.get();\n\
2391 node A = Terminate, deps: [], ready_on_write, \
2392 writes: [crate::X observed beat];";
2393 match syn::parse_str::<GraphSpec>(no_budget) {
2394 Ok(_) => panic!("accepted without `beat_timeout:`"),
2395 Err(err) => assert!(
2396 err.to_string().contains("requires `beat_timeout:`"),
2397 "got: {err}"
2398 ),
2399 }
2400
2401 let ok = "observe writes: it.get();\n\
2402 node A = Terminate, deps: [], beat_timeout: 100, ready_on_write, \
2403 writes: [crate::X observed beat];";
2404 assert!(
2405 syn::parse_str::<GraphSpec>(ok).is_ok(),
2406 "both halves present"
2407 );
2408
2409 let adopted = "node A = Terminate, deps: [], ready_on_write, \
2410 dataflow: [crate::hb::set_period];";
2411 match syn::parse_str::<GraphSpec>(adopted) {
2412 Ok(_) => panic!("adoption accepted as a heartbeat source"),
2413 Err(err) => assert!(
2414 err.to_string()
2415 .contains("requires an `observed beat` entry"),
2416 "got: {err}"
2417 ),
2418 }
2419 }
2420
2421 #[test]
2422 fn dataflow_scan_keys_on_the_node_param() {
2423 let src = r#"
2424 #[embassy_supervisor::dataflow]
2425 async fn worker(node: &'static TaskNode, map: Map) {
2426 let v = node.get(&PERIOD);
2427 map.get(&KEY);
2428 node.put(&OUT, v);
2429 if v > 0 {
2430 node.writer(&crate::stats::HITS[1]).fetch_add(1, O);
2431 }
2432 let mut rx = node.reader(&EST).receiver();
2433 node.put(&OUT, v + 1); // second site, one entry
2434 }
2435 async fn unannotated(node: &'static TaskNode) {
2436 node.put(&IGNORED, 1);
2437 }
2438 "#;
2439 let mut out = Vec::new();
2440 scan_dataflow(src, &mut out);
2441 let key: Vec<(&str, bool, &str)> = out
2442 .iter()
2443 .map(|a| (a.func.as_str(), a.write, a.path.as_str()))
2444 .collect();
2445 assert_eq!(
2446 key,
2447 [
2448 ("worker", false, "PERIOD"),
2449 ("worker", true, "OUT"),
2450 ("worker", true, "crate::stats::HITS[1]"),
2451 ("worker", false, "EST"),
2452 ],
2453 "{out:?}"
2454 );
2455 }
2456
2457 #[test]
2458 fn cfg_attr_wrapped_dataflow_is_scanned_with_its_predicate() {
2459 let src = r#"
2460 #[cfg(feature = "x")]
2461 #[cfg_attr(feature = "grown", embassy_supervisor::dataflow)]
2462 async fn worker(node: &'static TaskNode) {
2463 #[cfg(feature = "grown")] // same predicate again: recorded once
2464 let mut rx = node.reader(&LATEST).receiver();
2465 }
2466 #[cfg_attr(feature = "a", inline, dataflow)]
2467 async fn beside_others(node: &'static TaskNode) {
2468 node.put(&OUT, 1);
2469 }
2470 #[cfg_attr(feature = "a", cfg_attr(feature = "b", dataflow))]
2471 async fn nested(node: &'static TaskNode) {
2472 node.put(&DEEP, 1);
2473 }
2474 #[cfg_attr(feature = "a", inline)]
2475 async fn not_dataflow(node: &'static TaskNode) {
2476 node.put(&IGNORED, 1);
2477 }
2478 "#;
2479 let mut out = Vec::new();
2480 scan_dataflow(src, &mut out);
2481 let key: Vec<(&str, bool, &str, Vec<&str>)> = out
2482 .iter()
2483 .map(|a| {
2484 (
2485 a.func.as_str(),
2486 a.write,
2487 a.path.as_str(),
2488 a.cfgs.iter().map(String::as_str).collect(),
2489 )
2490 })
2491 .collect();
2492 assert_eq!(
2493 key,
2494 [
2495 (
2496 "worker",
2497 false,
2498 "LATEST",
2499 vec!["feature=\"x\"", "feature=\"grown\""]
2500 ),
2501 ("beside_others", true, "OUT", vec!["feature=\"a\""]),
2502 (
2503 "nested",
2504 true,
2505 "DEEP",
2506 vec!["all(feature=\"a\",feature=\"b\")"]
2507 ),
2508 ],
2509 "{out:?}"
2510 );
2511 }
2512
2513 #[test]
2514 fn cfg_attr_wrapped_verb_registrations_are_honoured() {
2515 let src = r#"
2516 #[cfg_attr(feature = "a", dataflow(read(subscribe), write(publish)))]
2517 async fn worker(node: &'static TaskNode) {
2518 let rx = node.subscribe(&EST);
2519 node.publish(&ARMED, true);
2520 }
2521 "#;
2522 let mut out = Vec::new();
2523 scan_dataflow(src, &mut out);
2524 let key: Vec<(&str, bool, &str)> = out
2525 .iter()
2526 .map(|a| (a.verb.as_str(), a.write, a.path.as_str()))
2527 .collect();
2528 assert_eq!(
2529 key,
2530 [("subscribe", false, "EST"), ("publish", true, "ARMED")],
2531 "{out:?}"
2532 );
2533 }
2534
2535 #[test]
2536 fn dataflow_attr_unwraps_cfg_attr() {
2537 let bare: Attribute = syn::parse_quote!(#[embassy_supervisor::dataflow]);
2538 let (attr, cfg) = dataflow_attr(&bare).unwrap();
2539 assert!(matches!(attr.meta, Meta::Path(_)));
2540 assert_eq!(cfg, None);
2541
2542 let wrapped: Attribute =
2543 syn::parse_quote!(#[cfg_attr(all(feature = "a", not(test)), dataflow(read(sub)))]);
2544 let (attr, cfg) = dataflow_attr(&wrapped).unwrap();
2545 assert!(is_dataflow_attr(&wrapped));
2546 assert_eq!(tokens_text(&attr), "#[dataflow(read(sub))]");
2547 assert_eq!(cfg.as_deref(), Some("all(feature=\"a\",not(test))"));
2548
2549 let other: Attribute = syn::parse_quote!(#[cfg_attr(feature = "a", inline)]);
2550 assert!(dataflow_attr(&other).is_none());
2551 assert!(!is_dataflow_attr(&other));
2552 let cfg_only: Attribute = syn::parse_quote!(#[cfg(feature = "a")]);
2553 assert!(!is_dataflow_attr(&cfg_only));
2554 }
2555
2556 #[test]
2557 fn nested_fns_are_not_walked() {
2558 let body = quote!({
2559 node.put(&OUT, 1);
2560 fn helper(node: &'static TaskNode) {
2561 node.put(&INNER, 2);
2562 }
2563 let f = || node.get(&IN);
2564 });
2565 let mut seen = Vec::new();
2566 let out = rewrite_verb_calls(body, "node", &VerbTable::builtin(), &mut |call| {
2567 seen.push(call.path.clone());
2568 Ok(Some(quote!(REPL)))
2569 })
2570 .unwrap()
2571 .to_string()
2572 .replace(' ', "");
2573 assert_eq!(seen, ["OUT", "IN"], "the nested fn's access is not ours");
2574 assert!(
2575 out.contains("put(&INNER,2)"),
2576 "and stays unrewritten: {out}"
2577 );
2578 assert!(out.contains("get(REPL)"), "the closure's is: {out}");
2579 }
2580
2581 #[test]
2583 fn walker_keys_on_the_actual_param_name() {
2584 let src = r#"
2585 #[dataflow]
2586 fn f(n: &'static TaskNode, node: Map) {
2587 n.put(&OUT, 1);
2588 node.get(&KEY);
2589 }
2590 "#;
2591 let mut out = Vec::new();
2592 scan_dataflow(src, &mut out);
2593 let key: Vec<(&str, bool)> = out.iter().map(|a| (a.path.as_str(), a.write)).collect();
2594 assert_eq!(key, [("OUT", true)], "{out:?}");
2595 }
2596
2597 #[test]
2599 fn dataflow_walker_rejects_a_computed_target() {
2600 let body = quote!({ node.get(some_binding) });
2601 let err =
2602 rewrite_verb_calls(body, "node", &VerbTable::builtin(), &mut |_| Ok(None)).unwrap_err();
2603 assert!(err.to_string().contains("literal path"), "{err}");
2604 }
2605
2606 #[test]
2609 fn dataflow_walker_replaces_the_first_argument() {
2610 let body = quote!({
2611 node.put(&OUT, v);
2612 node.get(&IN)
2613 });
2614 let out = rewrite_verb_calls(body, "node", &VerbTable::builtin(), &mut |call| {
2615 let k = if call.write { 1u32 } else { 0 };
2616 Ok(Some(quote!(REPL(#k))))
2617 })
2618 .unwrap()
2619 .to_string()
2620 .replace(' ', "");
2621 assert!(out.contains("put(REPL(1u32),v)"), "{out}");
2622 assert!(out.contains("get(REPL(0u32))"), "{out}");
2623 }
2624
2625 #[test]
2629 fn registered_verbs_are_walked_and_others_are_not() {
2630 let verbs: VerbTable = syn::parse_str("read(subscribe), write(publish, emit)").unwrap();
2631 let body = quote!({
2632 node.subscribe(&IN);
2633 node.publish(&OUT, v);
2634 node.emit(&LOG, e);
2635 node.set_ready();
2636 node.reader(&ALSO);
2637 });
2638 let mut seen = Vec::new();
2639 let out = rewrite_verb_calls(body, "node", &verbs, &mut |call| {
2640 seen.push((call.verb.clone(), call.write, call.path.clone()));
2641 Ok(Some(quote!(REPL)))
2642 })
2643 .unwrap()
2644 .to_string()
2645 .replace(' ', "");
2646 assert_eq!(
2647 seen,
2648 [
2649 ("subscribe".into(), false, "IN".to_string()),
2650 ("publish".into(), true, "OUT".to_string()),
2651 ("emit".into(), true, "LOG".to_string()),
2652 ("reader".into(), false, "ALSO".to_string()),
2653 ],
2654 "registered verbs join the built-ins, direction as declared"
2655 );
2656 assert!(out.contains("set_ready()"), "not a verb, untouched: {out}");
2657 }
2658
2659 #[test]
2663 fn verb_registration_rejects_its_mistakes() {
2664 assert!(
2665 syn::parse_str::<VerbTable>("").is_ok(),
2666 "bare `#[dataflow]`"
2667 );
2668
2669 let cases = [
2670 ("read(put)", "built-in"),
2671 ("write(reader)", "built-in"),
2672 ("read(a), write(a)", "twice"),
2673 ("read(a, a)", "twice"),
2674 ("beat(a)", "expected `read(..)` or `write(..)`"),
2675 ("subscribe", "expected `read(..)` or `write(..)`"),
2676 ("read()", "names no verb"),
2677 ("42", "verb registrations"),
2680 ];
2681 for (src, want) in cases {
2682 match syn::parse_str::<VerbTable>(src) {
2683 Ok(_) => panic!("`{src}` accepted"),
2684 Err(err) => assert!(
2685 err.to_string().contains(want),
2686 "`{src}`: wanted {want:?}, got: {err}"
2687 ),
2688 }
2689 }
2690 }
2691
2692 #[test]
2696 fn the_scanner_reads_the_registrations_too() {
2697 let src = r#"
2698 #[dataflow(read(subscribe), write(publish))]
2699 async fn entry(node: &'static TaskNode) {
2700 let rx = node.subscribe(&crate::EST);
2701 node.publish(&crate::ARMED, true);
2702 node.put(&crate::OTHER, 1);
2703 }
2704 "#;
2705 let mut out = Vec::new();
2706 scan_dataflow(src, &mut out);
2707 let key: Vec<(&str, &str, bool)> = out
2708 .iter()
2709 .map(|a| (a.verb.as_str(), a.path.as_str(), a.write))
2710 .collect();
2711 assert_eq!(
2712 key,
2713 [
2714 ("subscribe", "crate::EST", false),
2715 ("publish", "crate::ARMED", true),
2716 ("put", "crate::OTHER", true),
2717 ]
2718 );
2719 }
2720
2721 #[test]
2725 fn signal_entry_markers_compose() {
2726 match syn::parse_str::<GraphSpec>("node A = Terminate, deps: [], writes: [crate::X beat];")
2727 {
2728 Ok(_) => panic!("accepted a bare `beat` entry"),
2729 Err(err) => assert!(
2730 err.to_string().contains("beat_put"),
2731 "the rejection names the verb that carries it: {err}"
2732 ),
2733 }
2734 for src in [
2735 "node A = Terminate, deps: [], writes: [crate::X observed beat];",
2736 "node A = Terminate, deps: [], \
2737 writes: [crate::X observed beat via it.get()];",
2738 ] {
2739 let spec = syn::parse_str::<GraphSpec>(src).unwrap_or_else(|e| panic!("{src}: {e}"));
2740 let Item::Node(n) = &spec.items[0] else {
2741 unreachable!()
2742 };
2743 let entry = n.reads.first().or(n.writes.first()).unwrap();
2744 assert!(entry.beat.is_some(), "{src}");
2745 }
2746 }
2747
2748 #[test]
2751 fn discover_clause_shape() {
2752 let spec = syn::parse_str::<GraphSpec>(
2753 "node A = Terminate, deps: [], task: w, discover;\n\
2754 pool P = [Terminate, OnDemand], deps: [], task: w, discover, \
2755 policy: Pol::new(), min: 1, max: 2;",
2756 )
2757 .unwrap();
2758 let Item::Node(n) = &spec.items[0] else {
2759 unreachable!()
2760 };
2761 assert!(n.discover.is_some());
2762 let Item::Pool(p) = &spec.items[1] else {
2763 unreachable!()
2764 };
2765 assert!(p.discover.is_some());
2766
2767 assert!(
2770 syn::parse_str::<GraphSpec>(
2771 "node A = Terminate, deps: [], task: w, discover, \
2772 writes: [crate::X observed beat];"
2773 )
2774 .is_ok(),
2775 "a marked entry may sit beside `discover`"
2776 );
2777
2778 for (bad, want) in [
2779 (
2780 "node A = Terminate, deps: [], task: w, discover: 8;",
2781 "takes no argument",
2782 ),
2783 (
2784 "node A = Terminate, deps: [], task: w, discover, reads: [crate::X];",
2785 "may only add markers",
2786 ),
2787 ("node A = Terminate, deps: [], discover;", "nothing to scan"),
2788 ] {
2789 match syn::parse_str::<GraphSpec>(bad) {
2790 Ok(_) => panic!("accepted: {bad}"),
2791 Err(err) => assert!(err.to_string().contains(want), "{bad}: {err}"),
2792 }
2793 }
2794 }
2795
2796 #[test]
2799 fn bare_crate_normalizes_to_dollar_crate() {
2800 let ts: TokenStream2 = "task: crate::w, reads: [$crate::X, (crate::Y)]"
2801 .parse()
2802 .unwrap();
2803 let out = normalize_fragment_crate(ts.clone())
2804 .to_string()
2805 .replace(' ', "");
2806 assert_eq!(out.matches("$crate").count(), 3, "{out}");
2807
2808 let resolved =
2809 substitute_dollar_crate(normalize_fragment_crate(ts), &"::dep".parse().unwrap())
2810 .to_string()
2811 .replace(' ', "");
2812 assert_eq!(resolved, "task:::dep::w,reads:[::dep::X,(::dep::Y)]");
2813 }
2814
2815 #[test]
2816 fn dataflow_clause_shape() {
2817 let spec = syn::parse_str::<GraphSpec>(
2818 "node A = Terminate, deps: [], task: w, discover, \
2819 dataflow: [crate::hb::set_period];\n\
2820 node B = Terminate, deps: [], reads: [crate::X], \
2821 dataflow: [crate::hb::set_period, other::adjust];",
2822 )
2823 .unwrap();
2824 let Item::Node(a) = &spec.items[0] else {
2825 unreachable!()
2826 };
2827 assert_eq!(a.dataflow.len(), 1);
2828 let Item::Node(b) = &spec.items[1] else {
2829 unreachable!()
2830 };
2831 assert_eq!(b.dataflow.len(), 2);
2832
2833 for (bad, want) in [
2834 (
2835 "node A = Terminate, deps: [], dataflow: [];",
2836 "at least one",
2837 ),
2838 (
2839 "node A = Terminate, deps: [], dataflow: [f, f];",
2840 "duplicate",
2841 ),
2842 ] {
2843 match syn::parse_str::<GraphSpec>(bad) {
2844 Ok(_) => panic!("accepted: {bad}"),
2845 Err(err) => assert!(err.to_string().contains(want), "{bad}: {err}"),
2846 }
2847 }
2848 }
2849
2850 #[test]
2851 fn discover_cannot_carry_ready_on_write() {
2852 match syn::parse_str::<GraphSpec>(
2853 "node A = Terminate, deps: [], task: w, ready_on_write, discover;",
2854 ) {
2855 Ok(_) => panic!("accepted `ready_on_write` with nothing to fire from"),
2856 Err(err) => assert!(
2857 err.to_string()
2858 .contains("requires an `observed beat` entry"),
2859 "got: {err}"
2860 ),
2861 }
2862 assert!(
2863 syn::parse_str::<GraphSpec>(
2864 "node A = Terminate, deps: [], task: w, ready_on_write, \
2865 beat_timeout: 100, discover, \
2866 writes: [crate::X observed beat];"
2867 )
2868 .is_ok(),
2869 "a marked entry beside `discover` is a heartbeat source"
2870 );
2871 }
2872
2873 #[test]
2874 fn via_on_a_beat_only_entry_is_rejected() {
2875 match syn::parse_str::<GraphSpec>(
2876 "node A = Terminate, deps: [], writes: [crate::X beat via it.get()];",
2877 ) {
2878 Ok(_) => panic!("`beat via` accepted"),
2879 Err(err) => {
2880 let msg = err.to_string();
2881 assert!(msg.contains("only an `observed` entry has"), "got: {msg}");
2882 assert!(
2883 msg.contains("`beat` only ever qualifies `observed`"),
2884 "the `beat` half must be named too, got: {msg}"
2885 );
2886 }
2887 }
2888 }
2889
2890 #[test]
2891 fn a_bare_qualifier_names_the_form_it_belongs_to() {
2892 let src = "node A = Terminate, deps: [], writes: [crate::X via it.get()];";
2893 match syn::parse_str::<GraphSpec>(src) {
2894 Ok(_) => panic!("accepted a bare qualifier: {src}"),
2895 Err(err) => assert!(
2896 err.to_string().contains("`via` supplies the accessor"),
2897 "got: {err}"
2898 ),
2899 }
2900 }
2901
2902 #[test]
2903 fn beat_on_a_read_is_rejected() {
2904 let src = "observe reads: it.get();\n\
2905 node A = Terminate, deps: [], reads: [crate::X observed beat];";
2906 match syn::parse_str::<GraphSpec>(src) {
2907 Ok(_) => panic!("`beat` accepted on a read"),
2908 Err(err) => assert!(
2909 err.to_string().contains("belongs on a `writes:` entry"),
2910 "got: {err}"
2911 ),
2912 }
2913 }
2914
2915 #[test]
2916 fn bound_without_ready_rejected() {
2917 match syn::parse_str::<GraphSpec>(
2918 "node A = Terminate, deps: [];\nnode B = Terminate, deps: [A bound];",
2919 ) {
2920 Ok(_) => panic!("`bound` without `ready` accepted"),
2921 Err(err) => assert!(
2922 err.to_string().contains("`bound` implies `ready`"),
2923 "got: {err}"
2924 ),
2925 }
2926 }
2927
2928 #[test]
2929 fn dep_markers_compose() {
2930 assert!(
2931 syn::parse_str::<GraphSpec>(
2932 "node A = Terminate, deps: [];\n\
2933 node B = Terminate, deps: [A ready bound];\n\
2934 node C = Terminate, deps: [A bound ready];",
2935 )
2936 .is_ok()
2937 );
2938 match syn::parse_str::<GraphSpec>(
2939 "node A = Terminate, deps: [];\nnode B = Terminate, deps: [A ready ready];",
2940 ) {
2941 Ok(_) => panic!("duplicate marker accepted"),
2942 Err(err) => assert!(err.to_string().contains("duplicate `ready`"), "got: {err}"),
2943 }
2944 }
2945
2946 #[test]
2947 fn pool_accepts_coupling_clauses() {
2948 assert!(
2949 syn::parse_str::<GraphSpec>(
2950 "pool P = [Terminate, OnDemand], deps: [], task: w, \
2951 reads: [crate::IN], writes: [crate::OUT], \
2952 policy: Pol::new(), min: 1, max: 2;",
2953 )
2954 .is_ok()
2955 );
2956 assert!(
2957 syn::parse_str::<GraphSpec>(
2958 "pool P = [Terminate], deps: [], task: w, writes: [crate::OUT], \
2959 policy: Pol::new(), min: 1, max: 1;",
2960 )
2961 .is_ok()
2962 );
2963 }
2964
2965 #[test]
2966 fn empty_signal_list_rejected() {
2967 for clause in ["reads", "writes"] {
2968 let src = format!("node A = Terminate, deps: [], {clause}: [];");
2969 match syn::parse_str::<GraphSpec>(&src) {
2970 Ok(_) => panic!("empty `{clause}:` accepted"),
2971 Err(err) => assert!(
2972 err.to_string()
2973 .contains(&format!("`{clause}:` must declare at least one")),
2974 "got: {err}"
2975 ),
2976 }
2977 }
2978 assert!(
2979 syn::parse_str::<GraphSpec>(
2980 "pool P = [Terminate], deps: [], task: w, reads: [], \
2981 policy: Pol::new(), min: 1, max: 1;",
2982 )
2983 .is_err()
2984 );
2985 }
2986
2987 #[test]
2988 fn duplicate_signal_rejected() {
2989 match syn::parse_str::<GraphSpec>(
2990 "node A = Terminate, deps: [], reads: [crate::SIG, other::X, crate::SIG];",
2991 ) {
2992 Ok(_) => panic!("duplicate accepted"),
2993 Err(err) => assert!(
2994 err.to_string()
2995 .contains("duplicate `reads:` entry `crate::SIG`"),
2996 "got: {err}"
2997 ),
2998 }
2999 match syn::parse_str::<GraphSpec>(
3000 "pool P = [Terminate], deps: [], task: w, writes: [a::B, a::B], \
3001 policy: Pol::new(), min: 1, max: 1;",
3002 ) {
3003 Ok(_) => panic!("duplicate accepted on a pool"),
3004 Err(err) => assert!(
3005 err.to_string().contains("duplicate `writes:` entry `a::B`"),
3006 "got: {err}"
3007 ),
3008 }
3009 }
3010
3011 #[test]
3012 fn distinct_paths_sharing_a_segment_are_fine() {
3013 assert!(
3014 syn::parse_str::<GraphSpec>(
3015 "node A = Terminate, deps: [], reads: [a::SIG, b::SIG, SIG];",
3016 )
3017 .is_ok()
3018 );
3019 }
3020
3021 #[test]
3022 fn parked_node_may_declare_coupling() {
3023 assert!(
3024 syn::parse_str::<GraphSpec>(
3025 "node A = Terminate, deps: [], reads: [crate::IN], writes: [crate::OUT];",
3026 )
3027 .is_ok()
3028 );
3029 }
3030
3031 #[test]
3032 fn signal_list_takes_paths() {
3033 assert!(
3034 syn::parse_str::<GraphSpec>(
3035 "node A = Terminate, deps: [], reads: [SIG, ::root::SIG, a::b::C];",
3036 )
3037 .is_ok()
3038 );
3039 assert!(
3040 syn::parse_str::<GraphSpec>("node A = Terminate, deps: [], reads: [1 + 2];").is_err()
3041 );
3042 }
3043
3044 #[test]
3045 fn ack_timeout_accepted_and_validated() {
3046 assert!(
3047 syn::parse_str::<GraphSpec>(
3048 "node A = Terminate, deps: [], task: f, ack_timeout: 5000;\n\
3049 pool P = [Terminate], deps: [], task: w, policy: Pol::new(), \
3050 min: 1, max: 1, ack_timeout: 100;",
3051 )
3052 .is_ok()
3053 );
3054 for (src, needle) in [
3055 (
3056 "node A = Terminate, deps: [], task: f, ack_timeout: 0;",
3057 "must be at least 1",
3058 ),
3059 (
3060 "node A = Terminate, deps: [], task: f, ack_timeout: 10, ack_timeout: 20;",
3061 "duplicate `ack_timeout:` clause",
3062 ),
3063 ] {
3064 match syn::parse_str::<GraphSpec>(src) {
3065 Ok(_) => panic!("accepted: {src}"),
3066 Err(err) => assert!(err.to_string().contains(needle), "got: {err}"),
3067 }
3068 }
3069 }
3070
3071 #[test]
3072 fn beat_clauses_accepted() {
3073 assert!(
3074 syn::parse_str::<GraphSpec>(
3075 "node A = Terminate, deps: [], beat_timeout: 100, beat_window: 3;\n\
3076 node B = Terminate, deps: [], beat_window: 2, beat_timeout: 50, slot_timeout: 200;",
3077 )
3078 .is_ok()
3079 );
3080 }
3081
3082 #[test]
3083 fn cfg_gated_clauses() {
3084 const P: &str = "#[cfg(feature = \"x\")]";
3085 let spec = syn::parse_str::<GraphSpec>(&format!(
3086 "node A = Terminate, deps: [], task: w, discover, \
3087 {P} slot_timeout: 100, {P} ack_timeout: 200, \
3088 {P} beat_timeout: 100, {P} beat_window: 3, {P} disabled;\n\
3089 node B = Terminate, deps: [], task: w, {P} discover, \
3090 provides: [{P} R1, R2];",
3091 ))
3092 .expect("cfg-gated clauses parse");
3093 let Item::Node(a) = &spec.items[0] else {
3094 unreachable!()
3095 };
3096 assert_eq!(a.slot_timeout.as_ref().unwrap().cfg.len(), 1);
3097 assert_eq!(a.ack_timeout.as_ref().unwrap().cfg.len(), 1);
3098 assert_eq!(a.beat_timeout.as_ref().unwrap().cfg.len(), 1);
3099 assert_eq!(a.beat_window.as_ref().unwrap().cfg.len(), 1);
3100 assert_eq!(a.disabled.as_ref().unwrap().cfg.len(), 1);
3101 assert!(
3102 a.discover.as_ref().unwrap().cfg.is_empty(),
3103 "un-gated stays empty"
3104 );
3105 let Item::Node(b) = &spec.items[1] else {
3106 unreachable!()
3107 };
3108 assert_eq!(b.discover.as_ref().unwrap().cfg.len(), 1);
3109 assert_eq!(b.provides[0].cfg.len(), 1);
3110 assert!(b.provides[1].cfg.is_empty());
3111
3112 let spec = syn::parse_str::<GraphSpec>(&format!(
3114 "node A = Terminate, deps: [], task: w, \
3115 writes: [crate::S observed beat via it.get()], \
3116 {P} beat_timeout: 100, {P} ready_on_write;",
3117 ))
3118 .expect("gated ready_on_write parses beside its gated beat_timeout");
3119 let Item::Node(a) = &spec.items[0] else {
3120 unreachable!()
3121 };
3122 assert_eq!(a.ready_on_write.as_ref().unwrap().cfg.len(), 1);
3123
3124 for clause in [
3126 "task: w",
3127 "spawn: f()",
3128 "executor: HIGH",
3129 "exit: u32",
3130 "state: u32 = 0",
3131 "cancel",
3132 "pool_size: 2",
3133 "deps: [X]",
3134 "resources: [R: u32]",
3135 "reads: [crate::S]",
3136 "provides: [R]",
3137 "dataflow: [crate::f]",
3138 ] {
3139 match syn::parse_str::<GraphSpec>(&format!(
3140 "node A = Terminate, deps: [], {P} {clause};"
3141 )) {
3142 Ok(_) => panic!("`#[cfg]` on `{clause}` accepted"),
3143 Err(err) => assert!(
3144 err.to_string().contains("may only gate `slot_timeout:`"),
3145 "`{clause}`: wrong error: {err}"
3146 ),
3147 }
3148 }
3149 match syn::parse_str::<GraphSpec>(
3150 "node A = Terminate, deps: [], #[allow(dead_code)] beat_timeout: 100;",
3151 ) {
3152 Ok(_) => panic!("a non-cfg attribute accepted"),
3153 Err(err) => assert!(
3154 err.to_string()
3155 .contains("only `#[cfg(...)]` attributes may gate a clause"),
3156 "wrong error: {err}"
3157 ),
3158 }
3159 }
3160
3161 #[test]
3162 fn gated_beat_timeout_predicate_pairing() {
3163 const P: &str = "#[cfg(feature = \"x\")]";
3164 assert!(
3165 syn::parse_str::<GraphSpec>(&format!(
3166 "node A = Terminate, deps: [], beat_timeout: 100, {P} beat_window: 3;"
3167 ))
3168 .is_ok()
3169 );
3170 for (tail, needle) in [
3171 (
3172 "beat_window: 3",
3173 "`beat_window:` must carry the same `#[cfg]`",
3174 ),
3175 (
3176 "#[cfg(feature = \"y\")] beat_window: 3",
3177 "`beat_window:` must carry the same `#[cfg]`",
3178 ),
3179 ] {
3180 match syn::parse_str::<GraphSpec>(&format!(
3181 "node A = Terminate, deps: [], {P} beat_timeout: 100, {tail};"
3182 )) {
3183 Ok(_) => panic!("mismatched gate accepted: {tail}"),
3184 Err(err) => assert!(err.to_string().contains(needle), "{tail}: {err}"),
3185 }
3186 }
3187 match syn::parse_str::<GraphSpec>(&format!(
3188 "node A = Terminate, deps: [], task: w, \
3189 writes: [crate::S observed beat via it.get()], \
3190 {P} beat_timeout: 100, ready_on_write;"
3191 )) {
3192 Ok(_) => panic!("un-gated ready_on_write over a gated beat_timeout accepted"),
3193 Err(err) => assert!(
3194 err.to_string()
3195 .contains("`ready_on_write` must carry the same `#[cfg]`"),
3196 "{err}"
3197 ),
3198 }
3199 }
3200
3201 #[test]
3202 fn duplicate_clauses_rejected() {
3203 for (src, needle) in [
3204 (
3205 "node A = Terminate, deps: [], deps: [];",
3206 "duplicate `deps:` clause",
3207 ),
3208 (
3209 "node A = Terminate, deps: [], task: f, task: g;",
3210 "duplicate `task:` clause",
3211 ),
3212 (
3213 "node A = Terminate, deps: [], reads: [crate::X], reads: [crate::Y];",
3214 "duplicate `reads:` clause",
3215 ),
3216 (
3217 "pool P = [Terminate], deps: [A], deps: [], task: w, \
3218 policy: Pol::new(), min: 1, max: 1;",
3219 "duplicate `deps:` clause",
3220 ),
3221 (
3222 "pool P = [Terminate], deps: [], task: w, policy: Pol::new(), \
3223 policy: Other::new(), min: 1, max: 1;",
3224 "duplicate `policy:` clause",
3225 ),
3226 (
3227 "pool P = [Terminate], deps: [], task: w, policy: Pol::new(), \
3228 min: 1, max: 1, max: 2;",
3229 "duplicate `max:` clause",
3230 ),
3231 (
3232 "node A = Terminate, deps: [], beat_timeout: 100, beat_timeout: 200;",
3233 "duplicate `beat_timeout:` clause",
3234 ),
3235 (
3236 "node A = Terminate, deps: [], beat_timeout: 100, \
3237 beat_window: 3, beat_window: 4;",
3238 "duplicate `beat_window:` clause",
3239 ),
3240 (
3241 "node A = Terminate, deps: [], disabled, disabled;",
3242 "duplicate `disabled` marker",
3243 ),
3244 (
3245 "node A = Terminate, deps: [], task: w, \
3246 writes: [crate::S observed beat via it.get()], \
3247 beat_timeout: 100, ready_on_write, ready_on_write;",
3248 "duplicate `ready_on_write` marker",
3249 ),
3250 ] {
3251 match syn::parse_str::<GraphSpec>(src) {
3252 Ok(_) => panic!("duplicate accepted: {src}"),
3253 Err(err) => assert!(err.to_string().contains(needle), "got: {err}"),
3254 }
3255 }
3256 }
3257
3258 #[test]
3259 fn malformed_cfg_attribute_rejected() {
3260 for attr in ["#[cfg]", "#[cfg = \"x\"]", "#[cfg] #[cfg(feature = \"x\")]"] {
3261 for decl in [
3262 format!("node A = Terminate, deps: [], {attr} disabled;"),
3263 format!("node A = Terminate, deps: [], provides: [{attr} R];"),
3264 ] {
3265 match syn::parse_str::<GraphSpec>(&decl) {
3266 Ok(_) => panic!("malformed cfg accepted: {decl}"),
3267 Err(err) => assert!(
3268 err.to_string().contains("only `#[cfg(...)]` attributes"),
3269 "{decl}: {err}"
3270 ),
3271 }
3272 }
3273 }
3274 }
3275
3276 #[test]
3277 fn pool_clause_cfg_rejection_names_pool_alternatives() {
3278 const P: &str = "#[cfg(feature = \"x\")]";
3279 let spec = syn::parse_str::<GraphSpec>(&format!(
3280 "pool P = [Terminate], deps: [], task: w, \
3281 policy: Pol::new(), min: 1, max: 1, \
3282 {P} slot_timeout: 100, {P} ack_timeout: 200;"
3283 ))
3284 .expect("gated pool timeouts parse");
3285 let Item::Pool(p) = &spec.items[0] else {
3286 unreachable!()
3287 };
3288 assert_eq!(p.slot_timeout.as_ref().unwrap().cfg.len(), 1);
3289 assert_eq!(p.ack_timeout.as_ref().unwrap().cfg.len(), 1);
3290
3291 for clause in ["policy: Pol::new()", "min: 1", "beat_timeout: 100"] {
3292 match syn::parse_str::<GraphSpec>(&format!(
3293 "pool P = [Terminate], deps: [], task: w, {P} {clause};"
3294 )) {
3295 Ok(_) => panic!("`#[cfg]` on pool `{clause}` accepted"),
3296 Err(err) => {
3297 let msg = err.to_string();
3298 assert!(msg.contains("gate the whole pool"), "`{clause}`: {msg}");
3299 assert!(!msg.contains("provides"), "`{clause}`: {msg}");
3300 }
3301 }
3302 }
3303 }
3304
3305 #[test]
3306 fn missing_comma_after_marked_entry_is_an_error() {
3307 match syn::parse_str::<GraphSpec>(
3308 "node A = Terminate, deps: [], task: f, \
3309 writes: [crate::S observed via it.get() beat];",
3310 ) {
3311 Ok(_) => panic!("phantom entry accepted"),
3312 Err(err) => assert!(err.to_string().contains("expected `,`"), "got: {err}"),
3313 }
3314 assert!(
3315 syn::parse_str::<GraphSpec>(
3316 "node A = Terminate, deps: [], task: f, \
3317 writes: [crate::S observed beat via it.get(),];",
3318 )
3319 .is_ok(),
3320 "a trailing comma stays legal"
3321 );
3322 }
3323
3324 #[test]
3325 fn scan_records_fn_level_cfgs() {
3326 let mut out = Vec::new();
3327 scan_dataflow(
3328 "#[cfg(feature = \"x\")]\n#[dataflow]\nasync fn f(node: &'static TaskNode) \
3329 { node.put(&crate::S, 1); }",
3330 &mut out,
3331 );
3332 assert_eq!(out.len(), 1, "{out:?}");
3333 assert!(
3334 out[0].cfgs.iter().any(|c| c.contains("feature=")),
3335 "{:?}",
3336 out[0].cfgs
3337 );
3338 }
3339
3340 #[test]
3341 fn qualified_receiver_is_not_a_verb_call() {
3342 let body = quote!({
3343 self.node.put(&NOT_OURS, 1);
3344 foo::node.get(&ALSO_NOT);
3345 node.put(&OURS, 2);
3346 });
3347 let mut seen = Vec::new();
3348 rewrite_verb_calls(body, "node", &VerbTable::builtin(), &mut |call| {
3349 seen.push(call.path.clone());
3350 Ok(None)
3351 })
3352 .unwrap();
3353 assert_eq!(seen, ["OURS"]);
3354 }
3355
3356 fn parse_err(src: &str) -> String {
3357 syn::parse_str::<GraphSpec>(src)
3358 .err()
3359 .expect("rejected")
3360 .to_string()
3361 }
3362
3363 #[test]
3364 fn divisible_parses_bare_and_takes_no_type() {
3365 let spec = syn::parse_str::<GraphSpec>(
3366 "node A = Terminate, deps: [], task: f, resources: [P: divisible, Q: divisible];",
3367 )
3368 .unwrap();
3369 let Item::Node(n) = &spec.items[0] else {
3370 panic!("node")
3371 };
3372 assert_eq!(n.resources.len(), 2);
3373 assert_eq!(n.resources[0].kind(), ResourceKind::Divisible);
3374 assert!(n.resources[0].ty.is_none());
3375 assert_eq!(n.resources[1].kind(), ResourceKind::Divisible);
3376 let err =
3377 parse_err("node A = Terminate, deps: [], task: f, resources: [P: divisible u32];");
3378 assert!(err.contains("takes no type"), "{err}");
3379 }
3380
3381 #[test]
3382 fn divisible_is_exclusive_with_the_other_kinds() {
3383 for other in ["shared", "consume", "local"] {
3384 let err = parse_err(&format!(
3385 "node A = Terminate, deps: [], task: f, resources: [P: {other} divisible];"
3386 ));
3387 assert!(err.contains("its own kind"), "{other}: {err}");
3388 }
3389 let err = parse_err(
3390 "node A = Terminate, deps: [], task: f, resources: [P: divisible divisible];",
3391 );
3392 assert!(err.contains("duplicate `divisible`"), "{err}");
3393 }
3394
3395 #[test]
3396 fn a_type_named_divisible_needs_a_path() {
3397 let spec = syn::parse_str::<GraphSpec>(
3398 "node A = Terminate, deps: [], task: f, resources: [P: crate::divisible];",
3399 )
3400 .unwrap();
3401 let Item::Node(n) = &spec.items[0] else {
3402 panic!("node")
3403 };
3404 assert_eq!(n.resources[0].kind(), ResourceKind::Lend);
3405 assert!(n.resources[0].ty.is_some());
3406 }
3407
3408 #[test]
3409 fn serialized_only_qualifies_shared() {
3410 let err =
3411 parse_err("node A = Terminate, deps: [], task: f, resources: [B: serialized Bus];");
3412 assert!(err.contains("only qualifies `shared`"), "{err}");
3413 let spec = syn::parse_str::<GraphSpec>(
3414 "node A = Terminate, deps: [], task: f, resources: [B: shared serialized Bus];",
3415 )
3416 .unwrap();
3417 let Item::Node(n) = &spec.items[0] else {
3418 panic!("node")
3419 };
3420 assert_eq!(n.resources[0].kind(), ResourceKind::Shared);
3421 assert_eq!(n.resources[0].shared_signature(), "serialized shared Bus");
3422 assert_eq!(item_executor(&spec.items[0]).map(ToString::to_string), None);
3423 }
3424
3425 #[test]
3426 fn item_executor_names_the_routing_slot() {
3427 let spec = syn::parse_str::<GraphSpec>(
3428 "executor HIGH; node A = Terminate, deps: [], executor: HIGH, spawn: f;",
3429 )
3430 .unwrap();
3431 assert_eq!(item_executor(&spec.items[0]), None);
3432 assert_eq!(
3433 item_executor(&spec.items[1]).map(ToString::to_string),
3434 Some("HIGH".into())
3435 );
3436 }
3437
3438 #[test]
3439 fn default_executor_routes_every_eligible_item() {
3440 let spec = syn::parse_str::<GraphSpec>(
3441 "default executor THREAD; executor HIGH; \
3442 node A = Terminate, deps: [], task: f; \
3443 node B = Terminate, deps: [], executor: HIGH, task: f; \
3444 node C = Pause, deps: []; \
3445 node D = Terminate, deps: [], spawn: |s| { let _ = s; Ok(()) }; \
3446 node E = Terminate, deps: [], spawn: g; \
3447 pool P = [Terminate], deps: [], task: f, policy: p, min: 1, max: 1;",
3448 )
3449 .unwrap();
3450 assert_eq!(spec.default_executor.as_ref().unwrap(), "THREAD");
3451 let Item::Executor(x) = &spec.items[0] else {
3452 panic!("executor")
3453 };
3454 assert!(x.default);
3455 let Item::Executor(x) = &spec.items[1] else {
3456 panic!("executor")
3457 };
3458 assert!(!x.default);
3459 let ex = |i: usize| item_executor(&spec.items[i]).map(ToString::to_string);
3460 let defaulted = |i: usize| match &spec.items[i] {
3461 Item::Node(n) => n.executor_defaulted,
3462 Item::Pool(p) => p.executor_defaulted,
3463 Item::Executor(_) => false,
3464 };
3465 assert_eq!(ex(2), Some("THREAD".into()));
3467 assert!(defaulted(2));
3468 assert_eq!(ex(3), Some("HIGH".into()));
3469 assert!(!defaulted(3));
3470 assert_eq!(ex(4), None);
3473 assert_eq!(ex(5), None);
3474 assert_eq!(ex(6), Some("THREAD".into()));
3475 assert!(defaulted(6));
3476 assert_eq!(ex(7), Some("THREAD".into()));
3477 assert!(defaulted(7));
3478 }
3479
3480 #[test]
3481 fn default_executor_applies_across_fragment_markers() {
3482 let spec = syn::parse_str::<GraphSpec>(
3483 "default executor THREAD; \
3484 @fragment F; node A = Terminate, deps: [], task: f; @endfragment;",
3485 )
3486 .unwrap();
3487 assert_eq!(
3488 item_executor(&spec.items[1]).map(ToString::to_string),
3489 Some("THREAD".into())
3490 );
3491 }
3492
3493 #[test]
3494 fn default_executor_rejects_cfg_fragment_and_duplicate() {
3495 let cases = [
3496 (
3497 "#[cfg(feature = \"x\")] default executor THREAD;",
3498 "cannot be `#[cfg]`-gated",
3499 ),
3500 (
3501 "@fragment F; default executor THREAD; @endfragment;",
3502 "fragment cannot declare",
3503 ),
3504 (
3505 "default executor A; default executor B;",
3506 "duplicate `default executor`",
3507 ),
3508 ];
3509 for (src, needle) in cases {
3510 let err = syn::parse_str::<GraphSpec>(src)
3511 .err()
3512 .unwrap_or_else(|| panic!("`{src}` parsed"))
3513 .to_string();
3514 assert!(err.contains(needle), "{src}: {err}");
3515 }
3516 }
3517
3518 #[test]
3519 fn local_may_route_through_an_executor() {
3520 let spec = syn::parse_str::<GraphSpec>(
3521 "executor HIGH; node A = Terminate, deps: [], executor: HIGH, task: f, \
3522 resources: [R: local consume T];",
3523 )
3524 .expect("`local` + `executor:` is a per-slot tier check in the macro, not a parse error");
3525 assert_eq!(
3526 item_executor(&spec.items[1]).map(ToString::to_string),
3527 Some("HIGH".into())
3528 );
3529 }
3530
3531 #[test]
3532 fn veto_is_a_writes_marker_in_any_order() {
3533 let spec = syn::parse_str::<GraphSpec>(
3534 "observe writes: it.get();\n\
3535 node A = Terminate, deps: [], task: f, \
3536 writes: [crate::T veto, crate::U observed veto beat, crate::V beat veto observed via it.x()];",
3537 )
3538 .unwrap();
3539 let Item::Node(n) = &spec.items[0] else {
3540 panic!("node")
3541 };
3542 assert!(n.writes[0].veto.is_some() && n.writes[0].observed.is_none());
3543 assert!(
3544 n.writes[1].veto.is_some()
3545 && n.writes[1].beat.is_some()
3546 && n.writes[1].observed.is_some()
3547 );
3548 assert!(n.writes[2].via.is_some() && n.writes[2].veto.is_some());
3549 let err = parse_err("node A = Terminate, deps: [], task: f, writes: [crate::T veto veto];");
3550 assert!(err.contains("duplicate `veto`"), "{err}");
3551 let err = parse_err("node A = Terminate, deps: [], task: f, reads: [crate::T veto];");
3552 assert!(err.contains("belongs on a `writes:` entry"), "{err}");
3553 let err =
3554 parse_err("node A = Terminate, deps: [], task: f, writes: [crate::T via it.x()];");
3555 assert!(err.contains("only an `observed` entry has"), "{err}");
3556 let err = parse_err(
3557 "node A = Terminate, deps: [], task: f, writes: [crate::T observed via it.x() veto];",
3558 );
3559 assert!(
3560 err.contains("expected `,`"),
3561 "`via <expr>` ends the entry: {err}"
3562 );
3563 let err = parse_err("node A = Terminate, deps: [], task: f, writes: [crate::T bogus];");
3564 assert!(err.contains("`observed`/`beat`/`veto` markers"), "{err}");
3565 assert!(BUILTIN_WRITES.contains(&"veto") && BUILTIN_WRITES.contains(&"retire"));
3566 }
3567
3568 #[test]
3569 fn veto_beside_discover_is_a_marker() {
3570 assert!(
3571 syn::parse_str::<GraphSpec>(
3572 "node A = Terminate, deps: [], task: w, discover, writes: [crate::T veto];"
3573 )
3574 .is_ok()
3575 );
3576 }
3577}