enshwick/points.rs
1//! Extension points: rules one mod lets other mods observe, advise or
2//! override.
3//!
4//! The mod that owns a rule declares it once with
5//! [`#[point]`](macro@crate::point): a `repr(C)` input, the type of the
6//! answer, a name `<owner id>:<point>` and a [`Mode`]. The owner defines it
7//! with [`Load::define_point`] and asks it with [`OwnedPoint::decide`],
8//! passing its own answer; other mods attach links with
9//! [`Load::extend_point`] or [`Load::observe_point`], ordered by
10//! [`Order`]. The loader checks every link's declaration against the
11//! owner's, field by field, once every mod is initialised, orders the links
12//! the same way at every start, turns a panicking link off and lets
13//! `mods/enshwick/mods.toml` switch one off. With no live link, asking a
14//! point is one atomic load.
15//!
16//! The owner lists its points in [`manifest!`](crate::manifest), which puts
17//! their schemas into the manifest; another mod writes its definition of a
18//! point from there with `cargo enshwick point <owner id>:<point>`.
19
20#[cfg(feature = "unstable")]
21mod described;
22
23use core::ffi::c_void;
24use core::marker::PhantomData;
25use core::sync::atomic::{AtomicU32, Ordering};
26
27use crate::abi::{LayoutField, PointFlags, PointMode, PointVerdict, Status, Str};
28use crate::load::{hand_over, ok};
29use crate::{Host, Load, Off};
30
31#[cfg(feature = "unstable")]
32pub use described::{DescribedField, DescribedPoint, DescribedVerdict};
33
34/// How links act on a point's answer, as the owning mod chooses.
35///
36/// # Examples
37///
38/// ```
39/// use enshwick::points::Mode;
40///
41/// assert!(Mode::Override.changes());
42/// assert!(!Mode::Observe.changes());
43/// ```
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum Mode {
47 /// Links see the answer and change nothing.
48 Observe,
49 /// Links may change the answer; the owner may ignore the change.
50 Advise,
51 /// Links may change the answer, and the owner takes it.
52 Override,
53}
54
55impl Mode {
56 /// Whether links may change the answer.
57 ///
58 /// # Examples
59 ///
60 /// See [`Mode`].
61 #[must_use]
62 pub const fn changes(self) -> bool {
63 !matches!(self, Self::Observe)
64 }
65
66 /// The mode as the ABI carries it.
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// use enshwick::abi::PointMode;
72 /// use enshwick::points::Mode;
73 ///
74 /// assert_eq!(Mode::Advise.to_abi(), PointMode::ADVISE);
75 /// ```
76 #[must_use]
77 pub const fn to_abi(self) -> PointMode {
78 match self {
79 Self::Observe => PointMode::OBSERVE,
80 Self::Advise => PointMode::ADVISE,
81 Self::Override => PointMode::OVERRIDE,
82 }
83 }
84}
85
86/// The input of an extension point, with its name, mode and answer type.
87///
88/// # Safety
89///
90/// - Implemented by [`#[point]`](macro@crate::point) only.
91/// - The input type is `repr(C)`.
92/// - [`LAYOUT`] lists every field of the input in order, with its size and
93/// alignment in the hash.
94/// - [`OUTPUT`] is the answer type, with its size and alignment in the hash.
95///
96/// [`LAYOUT`]: Point::LAYOUT
97/// [`OUTPUT`]: Point::OUTPUT
98///
99/// # Examples
100///
101/// ```
102/// use enshwick::points::{Mode, Point};
103///
104/// #[enshwick::point(name = "you.trading-post:price", mode = Override, output = u32)]
105/// #[derive(Clone, Copy, Debug)]
106/// pub struct Price {
107/// pub item: u32,
108/// }
109///
110/// assert_eq!(Price::NAME, "you.trading-post:price");
111/// assert_eq!(Price::MODE, Mode::Override);
112/// assert!(!Price::REFUSE);
113/// ```
114pub unsafe trait Point: Copy + 'static {
115 /// The point's name, `<owner id>:<point>`.
116 const NAME: &'static str;
117 /// How links act on the answer.
118 const MODE: Mode;
119 /// Whether a link may refuse, which stops the chain.
120 const REFUSE: bool;
121 /// The input's fields, in order.
122 const LAYOUT: &'static [LayoutField];
123 /// The answer's type, as one field.
124 const OUTPUT: &'static [LayoutField];
125 /// The point's schema as `cargo enshwick point` reads it from the
126 /// owner's manifest: its `[[point]]` table with the layout hashes, then
127 /// the `[[type]]` tables of its input and answer.
128 const SCHEMA: &'static str;
129 /// The answer.
130 type Output: Copy + 'static;
131}
132
133/// What a link answers.
134///
135/// # Examples
136///
137/// ```
138/// use enshwick::points::Verdict;
139///
140/// let raise = |limit: &u32| Verdict::Set(limit * 2);
141/// assert_eq!(raise(&3), Verdict::Set(6));
142/// ```
143#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum Verdict<O> {
146 /// The answer stays as the links before left it.
147 Pass,
148 /// The answer becomes this; ignored on an observed point.
149 Set(O),
150 /// The chain stops and the answer is a refusal, on a point that allows
151 /// it; elsewhere the loader takes it as [`Verdict::Pass`].
152 Refuse,
153}
154
155/// What the links made of the owner's answer.
156///
157/// # Examples
158///
159/// ```
160/// use enshwick::points::Decision;
161///
162/// assert_eq!(Decision::Changed(4).value_or(0), 4);
163/// assert_eq!(Decision::<u32>::Refused.value_or(0), 0);
164/// assert!(Decision::<u32>::Refused.is_refused());
165/// ```
166#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
167#[non_exhaustive]
168pub enum Decision<O> {
169 /// No link changed the answer.
170 Kept(O),
171 /// Links changed the answer to this.
172 Changed(O),
173 /// A link refused.
174 Refused,
175}
176
177impl<O: Copy> Decision<O> {
178 /// The answer, or `refused` when a link refused.
179 ///
180 /// # Examples
181 ///
182 /// See [`Decision`].
183 #[must_use]
184 pub const fn value_or(self, refused: O) -> O {
185 match self {
186 Self::Kept(value) | Self::Changed(value) => value,
187 Self::Refused => refused,
188 }
189 }
190
191 /// Whether a link refused.
192 ///
193 /// # Examples
194 ///
195 /// See [`Decision`].
196 #[must_use]
197 pub const fn is_refused(self) -> bool {
198 matches!(self, Self::Refused)
199 }
200}
201
202/// Where a link stands in its point's chain: links run in turn, each seeing
203/// the answer the ones before left.
204///
205/// `after` a mod sees, and may change, that mod's answer; `before` a mod
206/// leaves it the last word. Among links the ids leave free, the lower
207/// priority runs first, then the lower mod id. Ids of mods that do not run
208/// are ignored.
209///
210/// # Examples
211///
212/// ```
213/// use enshwick::points::Order;
214///
215/// let order = Order::new().after("you.haggling").priority(10);
216/// assert_eq!(order.priority_value(), 10);
217/// ```
218#[derive(Clone, Debug, Default, PartialEq, Eq)]
219pub struct Order {
220 before: Vec<&'static str>,
221 after: Vec<&'static str>,
222 priority: i32,
223}
224
225impl Order {
226 /// No constraint, priority 0.
227 ///
228 /// # Examples
229 ///
230 /// See [`Order`].
231 #[must_use]
232 pub const fn new() -> Self {
233 Self {
234 before: Vec::new(),
235 after: Vec::new(),
236 priority: 0,
237 }
238 }
239
240 /// Runs before the link of mod `id`.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// let order = enshwick::points::Order::new().before("you.haggling");
246 /// # let _ = order;
247 /// ```
248 #[must_use]
249 pub fn before(mut self, id: &'static str) -> Self {
250 self.before.push(id);
251 self
252 }
253
254 /// Runs after the link of mod `id`.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// let order = enshwick::points::Order::new().after("you.haggling");
260 /// # let _ = order;
261 /// ```
262 #[must_use]
263 pub fn after(mut self, id: &'static str) -> Self {
264 self.after.push(id);
265 self
266 }
267
268 /// Sets the priority; lower runs earlier.
269 ///
270 /// # Examples
271 ///
272 /// See [`Order`].
273 #[must_use]
274 pub const fn priority(mut self, priority: i32) -> Self {
275 self.priority = priority;
276 self
277 }
278
279 /// The priority.
280 ///
281 /// # Examples
282 ///
283 /// See [`Order`].
284 #[must_use]
285 pub const fn priority_value(&self) -> i32 {
286 self.priority
287 }
288}
289
290/// A point this mod defined, from [`Load::define_point`]: asks the links
291/// from any thread.
292///
293/// # Examples
294///
295/// ```
296/// #[enshwick::point(name = "you.trading-post:price", mode = Override, output = u32)]
297/// #[derive(Clone, Copy, Debug)]
298/// pub struct Price {
299/// pub item: u32,
300/// }
301///
302/// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
303/// let price = load.define_point::<Price>()?;
304/// let coins = price.decide(&Price { item: 7 }, 10).value_or(10);
305/// enshwick::info!("item 7 costs {coins}");
306/// Ok(())
307/// }
308/// ```
309pub struct OwnedPoint<P> {
310 host: Host,
311 id: u32,
312 live: &'static AtomicU32,
313 _point: PhantomData<fn(&P)>,
314}
315
316impl<P> Clone for OwnedPoint<P> {
317 fn clone(&self) -> Self {
318 *self
319 }
320}
321
322impl<P> Copy for OwnedPoint<P> {}
323
324impl<P> core::fmt::Debug for OwnedPoint<P> {
325 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
326 f.debug_struct("OwnedPoint")
327 .field("id", &self.id)
328 .field("live", &self.live.load(Ordering::Relaxed))
329 .finish_non_exhaustive()
330 }
331}
332
333impl<P: Point> OwnedPoint<P> {
334 /// Runs the live links over `answer`, this mod's own, on this thread.
335 /// With no live link it returns [`Decision::Kept`] at once.
336 ///
337 /// # Examples
338 ///
339 /// See [`OwnedPoint`].
340 #[inline]
341 pub fn decide(&self, input: &P, answer: P::Output) -> Decision<P::Output> {
342 if self.live.load(Ordering::Relaxed) == 0 {
343 return Decision::Kept(answer);
344 }
345 self.run(input, answer)
346 }
347
348 /// Whether any link runs.
349 ///
350 /// # Examples
351 ///
352 /// ```
353 /// #[enshwick::point(name = "you.trading-post:price", mode = Override, output = u32)]
354 /// #[derive(Clone, Copy, Debug)]
355 /// pub struct Price {
356 /// pub item: u32,
357 /// }
358 ///
359 /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
360 /// let price = load.define_point::<Price>()?;
361 /// assert!(!price.is_extended(), "links run once every mod is initialised");
362 /// Ok(())
363 /// }
364 /// ```
365 #[must_use]
366 pub fn is_extended(&self) -> bool {
367 self.live.load(Ordering::Relaxed) != 0
368 }
369
370 #[inline(never)]
371 fn run(&self, input: &P, answer: P::Output) -> Decision<P::Output> {
372 let mut value = answer;
373 let mut verdict = PointVerdict::PASS;
374 let status = (self.host.api().run_point)(
375 self.host.handle(),
376 self.id,
377 core::ptr::from_ref(input).cast(),
378 (&raw mut value).cast(),
379 &raw mut verdict,
380 );
381 if !status.is_ok() {
382 return Decision::Kept(answer);
383 }
384 match verdict {
385 PointVerdict::SET => Decision::Changed(value),
386 PointVerdict::REFUSE => Decision::Refused,
387 _ => Decision::Kept(answer),
388 }
389 }
390}
391
392/// Always 0: the count of a point the loader refused to define.
393static NONE_LIVE: AtomicU32 = AtomicU32::new(0);
394
395/// The ABI description of the point `P`.
396pub(crate) fn spec<P: Point>() -> crate::abi::PointSpec {
397 crate::abi::PointSpec {
398 header: crate::abi::Header::of::<crate::abi::PointSpec>(),
399 name: Str::new(P::NAME),
400 mode: P::MODE.to_abi(),
401 flags: if P::REFUSE {
402 PointFlags::REFUSE
403 } else {
404 PointFlags::NONE
405 },
406 input: crate::services::layout(P::LAYOUT, size_of::<P>()),
407 output: crate::services::layout(P::OUTPUT, size_of::<P::Output>()),
408 }
409}
410
411impl Load<'_> {
412 /// Defines the extension point `P` for other mods to extend, and returns
413 /// the handle that asks it.
414 ///
415 /// `P` comes from [`#[point]`](macro@crate::point), named after this
416 /// mod: `<id>:<point>`. Links run once every mod is initialised.
417 ///
418 /// # Errors
419 ///
420 /// [`Off`] when the name is outside this mod's id or defined twice, or
421 /// when called after `init`.
422 ///
423 /// # Examples
424 ///
425 /// See [`OwnedPoint`].
426 pub fn define_point<P: Point>(&mut self) -> Result<OwnedPoint<P>, Off> {
427 let spec = spec::<P>();
428 let mut handle = crate::abi::PointHandle::EMPTY;
429 let status = (self.host().api().define_point)(
430 self.host().handle(),
431 &raw const spec,
432 &raw mut handle,
433 );
434 // SAFETY: the loader's count lives for the process.
435 let live = unsafe { handle.live.as_ref() }.unwrap_or(&NONE_LIVE);
436 ok(
437 status,
438 OwnedPoint {
439 host: self.host(),
440 id: handle.id,
441 live,
442 _point: PhantomData,
443 },
444 || {
445 if status == Status::REFUSED {
446 format!(
447 "point {}: named after another mod or defined twice",
448 P::NAME
449 )
450 } else {
451 format!("point {}", P::NAME)
452 }
453 },
454 )
455 }
456
457 /// Attaches `link` to another mod's extension point `P`, at `order` in
458 /// its chain. A panic in `link` turns this link off, not the mod.
459 ///
460 /// The loader matches the link once every mod is initialised: with no
461 /// running mod defining `P`, the link stays inert, which is no error;
462 /// with a definition whose input lacks or changed a field of `P`, or
463 /// whose answer or mode differs, it logs why and the link stays inert.
464 /// Declare the owner in the manifest with `recommends`, or `requires`
465 /// when this mod is pointless without it.
466 ///
467 /// # Errors
468 ///
469 /// [`Off`] when called after `init`, or when this mod links `P` already.
470 ///
471 /// # Examples
472 ///
473 /// ```
474 /// use enshwick::points::{Order, Verdict};
475 ///
476 /// #[enshwick::point(name = "you.trading-post:price", mode = Override, output = u32)]
477 /// #[derive(Clone, Copy, Debug)]
478 /// pub struct Price {
479 /// pub item: u32,
480 /// }
481 ///
482 /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
483 /// load.extend_point(&Order::new(), |price: &Price, coins: &u32| {
484 /// if price.item == 7 { Verdict::Set(coins / 2) } else { Verdict::Pass }
485 /// })
486 /// }
487 /// ```
488 pub fn extend_point<P: Point>(
489 &mut self,
490 order: &Order,
491 link: impl Fn(&P, &P::Output) -> Verdict<P::Output> + Send + Sync + 'static,
492 ) -> Result<(), Off> {
493 attach(self.host(), order, link)
494 }
495
496 /// Attaches `link` to another mod's extension point `P` to see each
497 /// answer as the links before left it, changing nothing; otherwise as
498 /// [`Load::extend_point`].
499 ///
500 /// # Errors
501 ///
502 /// [`Off`] when called after `init`, or when this mod links `P` already.
503 ///
504 /// # Examples
505 ///
506 /// ```
507 /// use enshwick::points::Order;
508 ///
509 /// #[enshwick::point(name = "you.trading-post:price", mode = Observe, output = u32)]
510 /// #[derive(Clone, Copy, Debug)]
511 /// pub struct Price {
512 /// pub item: u32,
513 /// }
514 ///
515 /// fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
516 /// load.observe_point(&Order::new(), |price: &Price, coins: &u32| {
517 /// enshwick::debug!("item {} costs {coins}", price.item);
518 /// })
519 /// }
520 /// ```
521 pub fn observe_point<P: Point>(
522 &mut self,
523 order: &Order,
524 link: impl Fn(&P, &P::Output) + Send + Sync + 'static,
525 ) -> Result<(), Off> {
526 attach(self.host(), order, move |input: &P, value: &P::Output| {
527 link(input, value);
528 Verdict::Pass
529 })
530 }
531}
532
533/// Hands `link` to the loader for the point `P`.
534fn attach<P: Point, F: Fn(&P, &P::Output) -> Verdict<P::Output> + Send + Sync + 'static>(
535 host: Host,
536 order: &Order,
537 link: F,
538) -> Result<(), Off> {
539 let spec = spec::<P>();
540 let before: Vec<Str> = order.before.iter().map(|id| Str::new(id)).collect();
541 let after: Vec<Str> = order.after.iter().map(|id| Str::new(id)).collect();
542 let count = |ids: &[Str]| u32::try_from(ids.len()).unwrap_or(0);
543 let raw = crate::abi::PointLink {
544 header: crate::abi::Header::of::<crate::abi::PointLink>(),
545 point: &raw const spec,
546 before: before.as_ptr(),
547 after: after.as_ptr(),
548 before_count: count(&before),
549 after_count: count(&after),
550 priority: order.priority,
551 };
552 hand_over(
553 link,
554 |user| (host.api().attach_point)(host.handle(), &raw const raw, run_link::<P, F>, user),
555 &format!("point {}", P::NAME),
556 )
557}
558
559/// Runs a link; `user` is the link boxed by [`attach`], alive for the
560/// process. A panic answers [`PointVerdict::PANICKED`] and leaves the mod on.
561pub(crate) unsafe extern "system" fn run_link<
562 P: Point,
563 F: Fn(&P, &P::Output) -> Verdict<P::Output>,
564>(
565 input: *const c_void,
566 value: *mut c_void,
567 user: *mut c_void,
568) -> PointVerdict {
569 // SAFETY: `user` is the leaked `Box<F>` this function was attached with.
570 let link = unsafe { &*user.cast::<F>() };
571 // SAFETY: the loader checked the owner's input starts with every field
572 // of `P`, and the input lives for the call.
573 let Some(input) = (unsafe { input.cast::<P>().as_ref() }) else {
574 return PointVerdict::PASS;
575 };
576 // SAFETY: the loader checked the owner's answer is `P::Output`, and the
577 // value lives for the call, written by nothing else meanwhile.
578 let Some(value) = (unsafe { value.cast::<P::Output>().as_mut() }) else {
579 return PointVerdict::PASS;
580 };
581 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| link(input, value))) {
582 Ok(Verdict::Set(new)) if P::MODE.changes() => {
583 *value = new;
584 PointVerdict::SET
585 }
586 Ok(Verdict::Refuse) if P::REFUSE && P::MODE.changes() => PointVerdict::REFUSE,
587 Ok(_) => PointVerdict::PASS,
588 Err(_) => PointVerdict::PANICKED,
589 }
590}
591
592/// Orders the links of one point: `before` and `after` name other links'
593/// mods, then the lower priority and the lower id run first. A constraint
594/// that would close a cycle is dropped; its link is named in the second
595/// list.
596///
597/// The loader and the fake call it, so both order alike; mods have no use
598/// for it.
599///
600/// # Examples
601///
602/// ```
603/// use enshwick::points::{LinkPlace, order};
604///
605/// let links = [
606/// LinkPlace { id: "b", priority: 0, before: &[], after: &[] },
607/// LinkPlace { id: "a", priority: 0, before: &[], after: &["b"] },
608/// LinkPlace { id: "c", priority: -1, before: &[], after: &[] },
609/// ];
610/// let (running, dropped) = order(&links);
611/// assert_eq!(running, [2, 0, 1]);
612/// assert!(dropped.is_empty());
613/// ```
614#[doc(hidden)]
615#[must_use]
616pub fn order(links: &[LinkPlace<'_>]) -> (Vec<usize>, Vec<usize>) {
617 let n = links.len();
618 let index_of = |id: &str| links.iter().position(|l| l.id == id);
619 // edges[a] holds every b that must run after a.
620 let mut edges: Vec<Vec<usize>> = vec![Vec::new(); n];
621 for (i, link) in links.iter().enumerate() {
622 for &later in link.before {
623 if let Some(j) = index_of(later).filter(|&j| j != i) {
624 edges[i].push(j);
625 }
626 }
627 for &earlier in link.after {
628 if let Some(j) = index_of(earlier).filter(|&j| j != i) {
629 edges[j].push(i);
630 }
631 }
632 }
633 let key = |i: usize| (links[i].priority, links[i].id, i);
634 let mut dropped = Vec::new();
635 let mut placed = vec![false; n];
636 let mut running = Vec::with_capacity(n);
637 while running.len() < n {
638 let mut waiting = vec![0usize; n];
639 for (a, after) in edges.iter().enumerate() {
640 if placed[a] {
641 continue;
642 }
643 for &b in after {
644 waiting[b] += 1;
645 }
646 }
647 let free = (0..n)
648 .filter(|&i| !placed[i] && waiting[i] == 0)
649 .min_by_key(|&i| key(i));
650 if let Some(next) = free {
651 placed[next] = true;
652 running.push(next);
653 continue;
654 }
655 // A cycle: among the links of a cycle that nothing outside it holds
656 // back, the first by key runs next, and only the constraints of its
657 // own cycle are dropped. A link merely waiting behind a cycle keeps
658 // its constraints.
659 let reach = reachable(&edges, &placed);
660 let Some(next) = (0..n)
661 .filter(|&i| {
662 !placed[i]
663 && reach[i][i]
664 && (0..n).all(|q| placed[q] || !reach[q][i] || reach[i][q])
665 })
666 .min_by_key(|&i| key(i))
667 else {
668 break;
669 };
670 for (a, after) in edges.iter_mut().enumerate() {
671 if !placed[a] {
672 after.retain(|&b| b != next);
673 }
674 }
675 dropped.push(next);
676 placed[next] = true;
677 running.push(next);
678 }
679 (running, dropped)
680}
681
682/// `reach[a][b]`: whether `b` must run after `a`, through one constraint or
683/// more between links not placed yet.
684fn reachable(edges: &[Vec<usize>], placed: &[bool]) -> Vec<Vec<bool>> {
685 let n = edges.len();
686 let mut reach = vec![vec![false; n]; n];
687 for (start, row) in reach.iter_mut().enumerate() {
688 if placed[start] {
689 continue;
690 }
691 let mut stack = vec![start];
692 while let Some(a) = stack.pop() {
693 for &b in &edges[a] {
694 if !placed[b] && !row[b] {
695 row[b] = true;
696 stack.push(b);
697 }
698 }
699 }
700 }
701 reach
702}
703
704/// A link's place, as [`order`] reads it.
705///
706/// # Examples
707///
708/// See [`order`].
709#[doc(hidden)]
710#[derive(Clone, Copy, Debug, PartialEq, Eq)]
711pub struct LinkPlace<'a> {
712 /// The linking mod's id.
713 pub id: &'a str,
714 /// Lower runs earlier among free links.
715 pub priority: i32,
716 /// Ids of mods whose links run after this one.
717 pub before: &'a [&'a str],
718 /// Ids of mods whose links run before this one.
719 pub after: &'a [&'a str],
720}
721
722#[cfg(test)]
723mod tests {
724 use super::{LinkPlace, order};
725
726 fn place<'a>(id: &'a str, priority: i32, after: &'a [&'a str]) -> LinkPlace<'a> {
727 LinkPlace {
728 id,
729 priority,
730 before: &[],
731 after,
732 }
733 }
734
735 #[test]
736 fn a_cycle_drops_only_its_own_constraints() {
737 // `c` and `d` wait for each other; `x` waits for `c` and has the
738 // lowest priority, but its own constraint closes no cycle.
739 let links = [
740 place("c", 0, &["d"]),
741 place("d", 0, &["c"]),
742 place("x", -5, &["c"]),
743 ];
744 let (running, dropped) = order(&links);
745 assert_eq!(running, [0, 2, 1]);
746 assert_eq!(dropped, [0]);
747 }
748
749 #[test]
750 fn a_cycle_behind_another_waits_for_it() {
751 // `a` and `b` close one cycle, `c` and `d` another that also waits
752 // for `b`; the first cycle breaks first, whatever the priorities.
753 let links = [
754 place("a", 5, &["b"]),
755 place("b", 5, &["a"]),
756 place("c", -5, &["d", "b"]),
757 place("d", -5, &["c"]),
758 ];
759 let (running, dropped) = order(&links);
760 assert_eq!(running, [0, 1, 2, 3]);
761 assert_eq!(dropped, [0, 2]);
762 }
763}