Skip to main content

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}