wasm4pm_compat/petri.rs
1//! Petri net, WF-net, and OC-Petri-net **shapes** — with soundness as a
2//! *typestate claim*, never a computed proof.
3//!
4//! This module models the place/transition canon: a [`crate::petri::PetriNet`] of [`crate::petri::Place`]s
5//! and [`crate::petri::Transition`]s joined by [`crate::petri::Arc`]s with a [`crate::petri::Marking`]; a [`crate::petri::WfNet`]
6//! (workflow net) specialization with a single source and sink and a soundness
7//! *claim* tracked at the type level; and an [`crate::petri::ObjectCentricPetriNet`] whose
8//! arcs are typed by object type and may be variable.
9//!
10//! ## Soundness is a *claim*, not a result
11//!
12//! WF-net **soundness** (option to complete, proper completion, no dead
13//! transitions) is a non-trivial property. This crate **does not compute it** —
14//! computing soundness is an engine and graduates to `wasm4pm`. Instead, a
15//! [`crate::petri::WfNet`] carries a *typestate token* recording at the type level whether
16//! soundness is [`crate::petri::SoundnessUnknown`] (default), merely [`crate::petri::SoundnessClaimed`]
17//! (asserted by a human/upstream, unproven here), or [`crate::petri::SoundnessWitnessed`]
18//! (carrying a witness obtained from `wasm4pm` and re-attached here).
19//!
20//! These three tokens are **empty enums** (uninhabited markers) used only as
21//! `PhantomData` type parameters — zero-cost, never constructed.
22//!
23//! ## Structure only
24//!
25//! [`crate::petri::PetriNet::validate`] and [`crate::petri::WfNet::validate`] check *structural* shape
26//! (arcs reference declared nodes; a WF-net has an initial and final marking).
27//! They never check reachability, boundedness, liveness, or soundness — those
28//! are `wasm4pm`.
29//!
30//! ## Graduation to `wasm4pm`
31//!
32//! Soundness witnessing, boundedness/safeness analysis, reachability, and
33//! token-game replay graduate to `wasm4pm`. A [`crate::petri::SoundnessWitnessed`] WF-net is
34//! the shape into which such a witness is re-attached for evidence-carrying
35//! interchange.
36//!
37//! [`SoundnessUnknown`]: crate::petri::SoundnessUnknown
38//! [`SoundnessClaimed`]: crate::petri::SoundnessClaimed
39//! [`SoundnessWitnessed`]: crate::petri::SoundnessWitnessed
40
41use core::marker::PhantomData;
42
43use crate::law::SoundnessState;
44
45// ── Place / Transition node-kind markers ─────────────────────────────────────
46
47/// A zero-sized type-level marker asserting that a type parameter represents a
48/// **place** node in a Petri net.
49///
50/// `PlaceNodeMarker` and [`TransitionNodeMarker`] are the runtime-facing
51/// counterparts of the [`crate::law::EndpointKind`] const-param enum. Use them
52/// as type parameters when you want the compiler to enforce that a slot is
53/// filled by a place kind rather than a transition kind.
54///
55/// Structure-only: carries no data, no token semantics.
56///
57/// ```
58/// use wasm4pm_compat::petri::PlaceNodeMarker;
59/// fn place_slot<P: wasm4pm_compat::petri::IsPlaceNode>() {}
60/// place_slot::<PlaceNodeMarker>();
61/// ```
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
63pub struct PlaceNodeMarker;
64
65/// A zero-sized type-level marker asserting that a type parameter represents a
66/// **transition** node in a Petri net.
67///
68/// Paired with [`PlaceNodeMarker`]; see its documentation.
69///
70/// ```
71/// use wasm4pm_compat::petri::TransitionNodeMarker;
72/// fn transition_slot<T: wasm4pm_compat::petri::IsTransitionNode>() {}
73/// transition_slot::<TransitionNodeMarker>();
74/// ```
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
76pub struct TransitionNodeMarker;
77
78mod node_marker_seal {
79 pub trait PlaceSeal {}
80 pub trait TransitionSeal {}
81 impl PlaceSeal for super::PlaceNodeMarker {}
82 impl TransitionSeal for super::TransitionNodeMarker {}
83}
84
85/// Sealed trait — only [`PlaceNodeMarker`] is a place node kind.
86///
87/// ```
88/// use wasm4pm_compat::petri::{PlaceNodeMarker, IsPlaceNode};
89/// fn needs_place<P: IsPlaceNode>(_: P) {}
90/// needs_place(PlaceNodeMarker);
91/// ```
92pub trait IsPlaceNode: node_marker_seal::PlaceSeal {}
93impl IsPlaceNode for PlaceNodeMarker {}
94
95/// Sealed trait — only [`TransitionNodeMarker`] is a transition node kind.
96///
97/// ```
98/// use wasm4pm_compat::petri::{TransitionNodeMarker, IsTransitionNode};
99/// fn needs_transition<T: IsTransitionNode>(_: T) {}
100/// needs_transition(TransitionNodeMarker);
101/// ```
102pub trait IsTransitionNode: node_marker_seal::TransitionSeal {}
103impl IsTransitionNode for TransitionNodeMarker {}
104
105// ── Bipartite arc type law ───────────────────────────────────────────────────
106
107/// A typed arc from a **place** to a **transition** — the only valid
108/// pre-incidence direction in a Petri net.
109///
110/// Paper: Murata (1989) §2 — F ⊆ (P×T) ∪ (T×P); arcs must be bipartite.
111/// There is **no** `PlaceToPlaceArc` or `TransitionToTransitionArc` type in
112/// this crate; they are unconstructible.
113///
114/// The generic kinds `P` and `T` are independent namespace markers; a place
115/// kind and a transition kind can be the same Rust type without breaking the
116/// law (the arc direction is carried by the struct type, not by the type params).
117///
118/// Structure-only: a typed directed edge. No token flow.
119///
120/// ```
121/// use wasm4pm_compat::petri::PlaceToTransitionArc;
122/// struct P1; struct T1;
123/// let arc = PlaceToTransitionArc::<P1, T1, u8>::new(0u8);
124/// assert_eq!(arc.weight(), 0);
125/// ```
126pub struct PlaceToTransitionArc<P, T, Weight> {
127 pub(crate) _from: PhantomData<P>,
128 pub(crate) _to: PhantomData<T>,
129 /// Arc weight (pre-incidence W⁻).
130 pub weight: Weight,
131}
132
133impl<P, T, Weight: Copy> PlaceToTransitionArc<P, T, Weight> {
134 /// Constructs a place→transition arc with the given weight.
135 ///
136 /// ```
137 /// use wasm4pm_compat::petri::PlaceToTransitionArc;
138 /// struct P1; struct T1;
139 /// let arc = PlaceToTransitionArc::<P1, T1, u32>::new(1u32);
140 /// assert_eq!(arc.weight(), 1u32);
141 /// ```
142 pub fn new(weight: Weight) -> Self {
143 PlaceToTransitionArc {
144 _from: PhantomData,
145 _to: PhantomData,
146 weight,
147 }
148 }
149
150 /// The arc weight.
151 ///
152 /// ```
153 /// use wasm4pm_compat::petri::PlaceToTransitionArc;
154 /// struct P1; struct T1;
155 /// assert_eq!(PlaceToTransitionArc::<P1, T1, u8>::new(3u8).weight(), 3);
156 /// ```
157 pub fn weight(&self) -> Weight {
158 self.weight
159 }
160}
161
162/// A typed arc from a **transition** to a **place** — the only valid
163/// post-incidence direction in a Petri net.
164///
165/// Paper: Murata (1989) §2 — bipartite arc law. Structure-only.
166///
167/// ```
168/// use wasm4pm_compat::petri::TransitionToPlaceArc;
169/// struct T1; struct P1;
170/// let arc = TransitionToPlaceArc::<T1, P1, u8>::new(1u8);
171/// assert_eq!(arc.weight(), 1);
172/// ```
173pub struct TransitionToPlaceArc<T, P, Weight> {
174 pub(crate) _from: PhantomData<T>,
175 pub(crate) _to: PhantomData<P>,
176 /// Arc weight (post-incidence W⁺).
177 pub weight: Weight,
178}
179
180impl<T, P, Weight: Copy> TransitionToPlaceArc<T, P, Weight> {
181 /// Constructs a transition→place arc with the given weight.
182 ///
183 /// ```
184 /// use wasm4pm_compat::petri::TransitionToPlaceArc;
185 /// struct T1; struct P1;
186 /// let arc = TransitionToPlaceArc::<T1, P1, u32>::new(1u32);
187 /// assert_eq!(arc.weight(), 1u32);
188 /// ```
189 pub fn new(weight: Weight) -> Self {
190 TransitionToPlaceArc {
191 _from: PhantomData,
192 _to: PhantomData,
193 weight,
194 }
195 }
196
197 /// The arc weight.
198 ///
199 /// ```
200 /// use wasm4pm_compat::petri::TransitionToPlaceArc;
201 /// struct T1; struct P1;
202 /// assert_eq!(TransitionToPlaceArc::<T1, P1, u8>::new(2u8).weight(), 2);
203 /// ```
204 pub fn weight(&self) -> Weight {
205 self.weight
206 }
207}
208
209// ── BipartiteArcConst: const-generic bipartite arc law ───────────────────────
210
211/// A bipartite arc with direction encoded as a const-generic [`crate::law::ArcDirectionConst`]
212/// parameter.
213///
214/// `BipartiteArcConst<{ArcDirectionConst::PlaceToTransition}, Weight>` and
215/// `BipartiteArcConst<{ArcDirectionConst::TransitionToPlace}, Weight>` are
216/// **distinct types** at compile time — a slot requiring a pre-incidence arc
217/// (`PlaceToTransition`) rejects a post-incidence arc (`TransitionToPlace`) with
218/// a type error.
219///
220/// Paper: Murata (1989) §2 — `F ⊆ (P×T) ∪ (T×P)`.
221///
222/// ## Difference from [`PlaceToTransitionArc`] / [`TransitionToPlaceArc`]
223///
224/// The named-type arcs use *struct shape* to enforce direction (no common base
225/// type). `BipartiteArcConst` provides a **single type** parameterised over
226/// direction — useful for generic containers that hold arcs of either direction
227/// while still encoding direction in the type.
228///
229/// Structure-only: a typed directed edge. No token flow.
230///
231/// ```
232/// use wasm4pm_compat::petri::BipartiteArcConst;
233/// use wasm4pm_compat::law::ArcDirectionConst;
234///
235/// let pre = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
236/// assert_eq!(pre.place_id(), "p0");
237/// assert_eq!(pre.weight(), 1u8);
238/// ```
239pub struct BipartiteArcConst<const DIR: crate::law::ArcDirectionConst, Weight> {
240 place_id: alloc::string::String,
241 transition_id: alloc::string::String,
242 /// Arc weight (multiplicity).
243 pub weight: Weight,
244}
245
246extern crate alloc;
247
248impl<const DIR: crate::law::ArcDirectionConst, Weight: Copy> BipartiteArcConst<DIR, Weight> {
249 /// Construct a bipartite arc with the given endpoints and weight.
250 ///
251 /// ```
252 /// use wasm4pm_compat::petri::BipartiteArcConst;
253 /// use wasm4pm_compat::law::ArcDirectionConst;
254 /// let post = BipartiteArcConst::<{ ArcDirectionConst::TransitionToPlace }, u32>::new("p1", "t0", 2);
255 /// assert_eq!(post.transition_id(), "t0");
256 /// assert_eq!(post.place_id(), "p1");
257 /// ```
258 pub fn new(
259 place_id: impl Into<alloc::string::String>,
260 transition_id: impl Into<alloc::string::String>,
261 weight: Weight,
262 ) -> Self {
263 BipartiteArcConst {
264 place_id: place_id.into(),
265 transition_id: transition_id.into(),
266 weight,
267 }
268 }
269
270 /// The place endpoint id.
271 ///
272 /// ```
273 /// use wasm4pm_compat::petri::BipartiteArcConst;
274 /// use wasm4pm_compat::law::ArcDirectionConst;
275 /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
276 /// assert_eq!(a.place_id(), "p0");
277 /// ```
278 pub fn place_id(&self) -> &str {
279 &self.place_id
280 }
281
282 /// The transition endpoint id.
283 ///
284 /// ```
285 /// use wasm4pm_compat::petri::BipartiteArcConst;
286 /// use wasm4pm_compat::law::ArcDirectionConst;
287 /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 1);
288 /// assert_eq!(a.transition_id(), "t0");
289 /// ```
290 pub fn transition_id(&self) -> &str {
291 &self.transition_id
292 }
293
294 /// The arc weight.
295 ///
296 /// ```
297 /// use wasm4pm_compat::petri::BipartiteArcConst;
298 /// use wasm4pm_compat::law::ArcDirectionConst;
299 /// let a = BipartiteArcConst::<{ ArcDirectionConst::PlaceToTransition }, u8>::new("p0", "t0", 3);
300 /// assert_eq!(a.weight(), 3u8);
301 /// ```
302 pub fn weight(&self) -> Weight {
303 self.weight
304 }
305
306 /// The arc direction encoded in the const parameter.
307 ///
308 /// ```
309 /// use wasm4pm_compat::petri::BipartiteArcConst;
310 /// use wasm4pm_compat::law::ArcDirectionConst;
311 /// let a = BipartiteArcConst::<{ ArcDirectionConst::TransitionToPlace }, u8>::new("t0", "p0", 1);
312 /// assert_eq!(a.direction(), ArcDirectionConst::TransitionToPlace);
313 /// ```
314 pub const fn direction(&self) -> crate::law::ArcDirectionConst {
315 DIR
316 }
317}
318
319// ── IsValidArc sealed trait ──────────────────────────────────────────────────
320
321mod arc_seal {
322 pub trait Sealed {}
323 impl<P, T, W> Sealed for super::PlaceToTransitionArc<P, T, W> {}
324 impl<T, P, W> Sealed for super::TransitionToPlaceArc<T, P, W> {}
325}
326
327/// Sealed marker: only [`PlaceToTransitionArc`] and [`TransitionToPlaceArc`]
328/// are valid arcs in a Petri net. No user type can implement this trait.
329///
330/// ```
331/// use wasm4pm_compat::petri::{PlaceToTransitionArc, IsValidArc};
332/// struct P1; struct T1;
333/// fn accept<A: IsValidArc>(_arc: A) {}
334/// accept(PlaceToTransitionArc::<P1, T1, u8>::new(1u8));
335/// ```
336pub trait IsValidArc: arc_seal::Sealed {}
337impl<P, T, W> IsValidArc for PlaceToTransitionArc<P, T, W> {}
338impl<T, P, W> IsValidArc for TransitionToPlaceArc<T, P, W> {}
339
340// ── Non-forgeable WF-net soundness (const-generic + private seal) ────────────
341
342mod wfnet_seal {
343 /// Private seal — only constructible inside `petri`. Prevents external
344 /// users from building `WfNetConst<{SoundnessState::Witnessed}>` directly.
345 pub(super) struct WfNetSeal;
346}
347
348/// A WF-net with soundness state encoded as a const generic parameter.
349///
350/// The `SOUNDNESS` parameter tracks whether soundness is unknown, claimed, or
351/// witnessed. Crucially, `WfNetConst<{SoundnessState::Witnessed}>` is **only**
352/// constructible via [`WfNetConst::witness_soundness`], which requires a
353/// [`SoundnessProof`] — a type that is itself only constructible inside this
354/// module or via the `wasm4pm` graduation bridge.
355///
356/// Direct struct-literal construction fails because `_seal` is a private field.
357///
358/// ```
359/// use wasm4pm_compat::petri::{WfNetConst, SoundnessProof};
360/// use wasm4pm_compat::law::SoundnessState;
361///
362/// // Build an unknown-soundness net, then claim it:
363/// let unknown = WfNetConst::<{ SoundnessState::Unknown }>::new();
364/// let claimed = unknown.claim_sound();
365/// // To reach Witnessed, you would call claimed.witness_soundness(proof)
366/// // where proof is only producible by the wasm4pm engine or this module.
367/// ```
368///
369/// ```compile_fail
370/// use wasm4pm_compat::petri::WfNetConst;
371/// use wasm4pm_compat::law::SoundnessState;
372/// // ERROR: _seal is a private field; direct forged construction is impossible.
373/// let forged: WfNetConst<{ SoundnessState::Witnessed }> = WfNetConst {
374/// _seal: todo!(),
375/// };
376/// ```
377#[doc(alias = "workflow net")]
378#[doc(alias = "WF-net")]
379pub struct WfNetConst<const SOUNDNESS: SoundnessState> {
380 // Private seal prevents direct struct-literal construction of any
381 // WfNetConst variant from outside this module.
382 _seal: wfnet_seal::WfNetSeal,
383}
384
385/// A proof token for `SoundnessState::Witnessed` — only constructible inside
386/// `petri` (or via the `wasm4pm` graduation bridge).
387///
388/// Callers outside this module cannot construct `SoundnessProof` because the
389/// inner `wfnet_seal::WfNetSeal` type is private.
390pub struct SoundnessProof(wfnet_seal::WfNetSeal);
391
392impl SoundnessProof {
393 /// Module-private constructor — only `petri` and the `wasm4pm` bridge can
394 /// produce a proof.
395 #[allow(dead_code)]
396 pub(crate) fn new() -> Self {
397 SoundnessProof(wfnet_seal::WfNetSeal)
398 }
399}
400
401/// A typed soundness-proof carrier that records *which* WF-net shape the
402/// proof was issued for, via a phantom type parameter `N`.
403///
404/// [`SoundnessProof`] is a bare token — it records that *some* net's soundness
405/// was witnessed but not *which* net. `WfNetSoundnessProofOf<N>` adds a phantom
406/// `N` so that a proof issued for one net type cannot be silently transplanted
407/// onto a different net type.
408///
409/// The phantom `N` is structural metadata only — `WfNetSoundnessProofOf` is
410/// still only constructible inside this module or via the `wasm4pm` bridge
411/// (the `_seal` field is private).
412///
413/// Structure-only: zero-cost phantom wrapper around the soundness-proof seal.
414///
415/// ```
416/// use core::marker::PhantomData;
417/// use wasm4pm_compat::petri::WfNetSoundnessProofOf;
418///
419/// // A user-defined net type marker:
420/// struct OrderFulfillmentNet;
421/// // The proof is typed to that specific net; cannot be used for other nets.
422/// let _: core::marker::PhantomData<WfNetSoundnessProofOf<OrderFulfillmentNet>>;
423/// ```
424pub struct WfNetSoundnessProofOf<N> {
425 _net: PhantomData<N>,
426 _seal: wfnet_seal::WfNetSeal,
427}
428
429impl<N> WfNetSoundnessProofOf<N> {
430 /// Module-private constructor. Only `petri` and the `wasm4pm` bridge can
431 /// produce a typed soundness proof.
432 #[allow(dead_code)]
433 pub(crate) fn new() -> Self {
434 WfNetSoundnessProofOf {
435 _net: PhantomData,
436 _seal: wfnet_seal::WfNetSeal,
437 }
438 }
439}
440
441impl Default for WfNetConst<{ SoundnessState::Unknown }> {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447impl WfNetConst<{ SoundnessState::Unknown }> {
448 /// Construct a `WfNetConst` in the initial `Unknown` soundness state.
449 ///
450 /// ```
451 /// use wasm4pm_compat::petri::WfNetConst;
452 /// use wasm4pm_compat::law::SoundnessState;
453 /// let _wf = WfNetConst::<{ SoundnessState::Unknown }>::new();
454 /// ```
455 pub fn new() -> Self {
456 WfNetConst {
457 _seal: wfnet_seal::WfNetSeal,
458 }
459 }
460
461 /// Advance to `Claimed` soundness — a type-level re-tagging only.
462 ///
463 /// ```
464 /// use wasm4pm_compat::petri::WfNetConst;
465 /// use wasm4pm_compat::law::SoundnessState;
466 /// let claimed: WfNetConst<{ SoundnessState::Claimed }> =
467 /// WfNetConst::<{ SoundnessState::Unknown }>::new().claim_sound();
468 /// ```
469 pub fn claim_sound(self) -> WfNetConst<{ SoundnessState::Claimed }> {
470 WfNetConst { _seal: self._seal }
471 }
472}
473
474impl WfNetConst<{ SoundnessState::Claimed }> {
475 /// Advance a *claimed* WF-net to `Witnessed` — requires a [`SoundnessProof`].
476 ///
477 /// The `SoundnessProof` is only constructible inside this module or by the
478 /// `wasm4pm` graduation bridge. This is the sanctioned, non-forgeable path
479 /// to `SoundnessState::Witnessed`.
480 ///
481 /// ```ignore
482 /// // Conceptual: the wasm4pm bridge supplies the proof after verifying soundness.
483 /// let witnessed = claimed.witness_soundness(proof_from_wasm4pm);
484 /// ```
485 pub fn witness_soundness(
486 self,
487 _proof: SoundnessProof,
488 ) -> WfNetConst<{ SoundnessState::Witnessed }> {
489 WfNetConst {
490 _seal: wfnet_seal::WfNetSeal,
491 }
492 }
493}
494
495/// A shared query surface for [`WfNetConst`] that is independent of soundness state.
496///
497/// `WfNetConst<S>` has three concrete impls (one per `SoundnessState` variant).
498/// This blanket impl adds a `soundness_state()` query so callers that receive a
499/// `WfNetConst<S>` in generic context can read the soundness state as a runtime
500/// value without knowing `S` statically.
501///
502/// The method is separately documented from the struct because the docs are on
503/// the trait, not on the three concrete impls.
504///
505/// ```
506/// use wasm4pm_compat::petri::{WfNetConst, WfNetQuery};
507/// use wasm4pm_compat::law::SoundnessState;
508/// let wf = WfNetConst::<{ SoundnessState::Unknown }>::new().claim_sound();
509/// assert_eq!(wf.soundness_state(), SoundnessState::Claimed);
510/// ```
511pub trait WfNetQuery {
512 /// The soundness state of this WF-net as a runtime value.
513 fn soundness_state(&self) -> SoundnessState;
514}
515
516impl WfNetQuery for WfNetConst<{ SoundnessState::Unknown }> {
517 fn soundness_state(&self) -> SoundnessState {
518 SoundnessState::Unknown
519 }
520}
521impl WfNetQuery for WfNetConst<{ SoundnessState::Claimed }> {
522 fn soundness_state(&self) -> SoundnessState {
523 SoundnessState::Claimed
524 }
525}
526impl WfNetQuery for WfNetConst<{ SoundnessState::Witnessed }> {
527 fn soundness_state(&self) -> SoundnessState {
528 SoundnessState::Witnessed
529 }
530}
531
532/// A place: a named token-holding location in a Petri net.
533///
534/// Structure-only: identity and name; no token dynamics.
535#[derive(Clone, Debug, PartialEq, Eq)]
536pub struct Place {
537 id: String,
538}
539
540impl Place {
541 /// Construct a place with an id.
542 ///
543 /// ```
544 /// use wasm4pm_compat::petri::Place;
545 /// assert_eq!(Place::new("p0").id(), "p0");
546 /// ```
547 pub fn new(id: impl Into<String>) -> Self {
548 Place { id: id.into() }
549 }
550
551 /// The place id.
552 ///
553 /// ```
554 /// use wasm4pm_compat::petri::Place;
555 /// assert_eq!(Place::new("p0").id(), "p0");
556 /// ```
557 pub fn id(&self) -> &str {
558 &self.id
559 }
560}
561
562/// A transition: a named firing element. An empty `label` denotes a *silent*
563/// (tau) transition.
564///
565/// Structure-only: identity and label; no firing semantics.
566#[derive(Clone, Debug, PartialEq, Eq)]
567pub struct Transition {
568 id: String,
569 label: String,
570}
571
572impl Transition {
573 /// Construct a labeled transition.
574 ///
575 /// ```
576 /// use wasm4pm_compat::petri::Transition;
577 /// let t = Transition::new("t0", "approve");
578 /// assert_eq!(t.id(), "t0");
579 /// assert!(!t.is_silent());
580 /// ```
581 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
582 Transition {
583 id: id.into(),
584 label: label.into(),
585 }
586 }
587
588 /// Construct a silent (tau) transition with an empty label.
589 ///
590 /// ```
591 /// use wasm4pm_compat::petri::Transition;
592 /// assert!(Transition::silent("t0").is_silent());
593 /// ```
594 pub fn silent(id: impl Into<String>) -> Self {
595 Transition::new(id, "")
596 }
597
598 /// The transition id.
599 ///
600 /// ```
601 /// use wasm4pm_compat::petri::Transition;
602 /// assert_eq!(Transition::new("t0", "a").id(), "t0");
603 /// ```
604 pub fn id(&self) -> &str {
605 &self.id
606 }
607
608 /// The transition label (empty means silent).
609 ///
610 /// ```
611 /// use wasm4pm_compat::petri::Transition;
612 /// assert_eq!(Transition::new("t0", "a").label(), "a");
613 /// ```
614 pub fn label(&self) -> &str {
615 &self.label
616 }
617
618 /// Whether the transition is silent (tau).
619 ///
620 /// ```
621 /// use wasm4pm_compat::petri::Transition;
622 /// assert!(Transition::silent("t0").is_silent());
623 /// ```
624 pub fn is_silent(&self) -> bool {
625 self.label.is_empty()
626 }
627}
628
629/// The direction of a Petri-net [`Arc`] relative to a transition.
630#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
631pub enum ArcDirection {
632 /// Place → Transition (an input/consume arc).
633 PlaceToTransition,
634 /// Transition → Place (an output/produce arc).
635 TransitionToPlace,
636}
637
638/// An arc: a directed, weighted connection between a place and a transition.
639///
640/// `weight` is the arc multiplicity (default 1). An arc whose endpoints are not
641/// declared is refused as [`PetriRefusal::InvalidVariableArc`] (the closest
642/// structural law in this shape's vocabulary) or, for WF-nets, a soundness
643/// concern at `wasm4pm`.
644///
645/// Structure-only: a typed graph edge, no token flow.
646#[derive(Clone, Debug, PartialEq, Eq)]
647pub struct Arc {
648 place_id: String,
649 transition_id: String,
650 direction: ArcDirection,
651 weight: u32,
652 /// For OC-Petri-nets: the object type this arc carries, if any.
653 object_type: Option<String>,
654 /// For OC-Petri-nets: whether the arc is a *variable* arc (multiple tokens
655 /// of the object type).
656 variable: bool,
657}
658
659impl Arc {
660 /// Construct a place→transition arc with weight 1.
661 ///
662 /// ```
663 /// use wasm4pm_compat::petri::{Arc, ArcDirection};
664 /// let a = Arc::place_to_transition("p", "t");
665 /// assert_eq!(a.direction(), ArcDirection::PlaceToTransition);
666 /// assert_eq!(a.weight(), 1);
667 /// ```
668 pub fn place_to_transition(
669 place_id: impl Into<String>,
670 transition_id: impl Into<String>,
671 ) -> Self {
672 Arc {
673 place_id: place_id.into(),
674 transition_id: transition_id.into(),
675 direction: ArcDirection::PlaceToTransition,
676 weight: 1,
677 object_type: None,
678 variable: false,
679 }
680 }
681
682 /// Construct a transition→place arc with weight 1.
683 ///
684 /// ```
685 /// use wasm4pm_compat::petri::{Arc, ArcDirection};
686 /// let a = Arc::transition_to_place("t", "p");
687 /// assert_eq!(a.direction(), ArcDirection::TransitionToPlace);
688 /// ```
689 pub fn transition_to_place(
690 transition_id: impl Into<String>,
691 place_id: impl Into<String>,
692 ) -> Self {
693 Arc {
694 place_id: place_id.into(),
695 transition_id: transition_id.into(),
696 direction: ArcDirection::TransitionToPlace,
697 weight: 1,
698 object_type: None,
699 variable: false,
700 }
701 }
702
703 /// Set the arc weight (multiplicity). Builder-style.
704 ///
705 /// ```
706 /// use wasm4pm_compat::petri::Arc;
707 /// assert_eq!(Arc::place_to_transition("p", "t").with_weight(3).weight(), 3);
708 /// ```
709 pub fn with_weight(mut self, weight: u32) -> Self {
710 self.weight = weight;
711 self
712 }
713
714 /// Type the arc by object type, marking it variable or not (OC-Petri-net).
715 /// Builder-style.
716 ///
717 /// ```
718 /// use wasm4pm_compat::petri::Arc;
719 /// let a = Arc::place_to_transition("p", "t").typed("order", true);
720 /// assert_eq!(a.object_type(), Some("order"));
721 /// assert!(a.is_variable());
722 /// ```
723 pub fn typed(mut self, object_type: impl Into<String>, variable: bool) -> Self {
724 self.object_type = Some(object_type.into());
725 self.variable = variable;
726 self
727 }
728
729 /// The place endpoint id.
730 pub fn place_id(&self) -> &str {
731 &self.place_id
732 }
733
734 /// The transition endpoint id.
735 pub fn transition_id(&self) -> &str {
736 &self.transition_id
737 }
738
739 /// The arc direction.
740 ///
741 /// ```
742 /// use wasm4pm_compat::petri::{Arc, ArcDirection};
743 /// assert_eq!(Arc::place_to_transition("p", "t").direction(), ArcDirection::PlaceToTransition);
744 /// ```
745 pub fn direction(&self) -> ArcDirection {
746 self.direction
747 }
748
749 /// The arc weight (multiplicity).
750 ///
751 /// ```
752 /// use wasm4pm_compat::petri::Arc;
753 /// assert_eq!(Arc::place_to_transition("p", "t").weight(), 1);
754 /// ```
755 pub fn weight(&self) -> u32 {
756 self.weight
757 }
758
759 /// The OC-Petri-net object type carried by this arc, if any.
760 ///
761 /// ```
762 /// use wasm4pm_compat::petri::Arc;
763 /// assert_eq!(Arc::place_to_transition("p", "t").object_type(), None);
764 /// ```
765 #[must_use]
766 pub fn object_type(&self) -> Option<&str> {
767 self.object_type.as_deref()
768 }
769
770 /// Whether this is a variable arc (OC-Petri-net).
771 ///
772 /// ```
773 /// use wasm4pm_compat::petri::Arc;
774 /// assert!(!Arc::place_to_transition("p", "t").is_variable());
775 /// ```
776 pub fn is_variable(&self) -> bool {
777 self.variable
778 }
779}
780
781/// A marking: how many tokens sit on each place.
782///
783/// Structure-only: a snapshot of token counts by place id. This crate never
784/// fires a transition to move from one marking to another.
785#[derive(Clone, Debug, Default, PartialEq, Eq)]
786pub struct Marking {
787 tokens: Vec<(String, u32)>,
788}
789
790impl Marking {
791 /// Construct an empty marking.
792 ///
793 /// ```
794 /// use wasm4pm_compat::petri::Marking;
795 /// assert!(Marking::empty().is_empty());
796 /// ```
797 pub fn empty() -> Self {
798 Marking::default()
799 }
800
801 /// Construct a marking from `(place_id, token_count)` pairs.
802 ///
803 /// ```
804 /// use wasm4pm_compat::petri::Marking;
805 /// let m = Marking::new([("p0".to_string(), 1)]);
806 /// assert_eq!(m.tokens_on("p0"), 1);
807 /// ```
808 pub fn new(tokens: impl IntoIterator<Item = (String, u32)>) -> Self {
809 Marking {
810 tokens: tokens.into_iter().collect(),
811 }
812 }
813
814 /// The token count on a place (0 if unmarked).
815 ///
816 /// ```
817 /// use wasm4pm_compat::petri::Marking;
818 /// assert_eq!(Marking::empty().tokens_on("p0"), 0);
819 /// ```
820 pub fn tokens_on(&self, place_id: &str) -> u32 {
821 self.tokens
822 .iter()
823 .find(|(p, _)| p == place_id)
824 .map(|(_, n)| *n)
825 .unwrap_or(0)
826 }
827
828 /// Whether the marking places no tokens anywhere.
829 ///
830 /// ```
831 /// use wasm4pm_compat::petri::Marking;
832 /// assert!(Marking::empty().is_empty());
833 /// ```
834 pub fn is_empty(&self) -> bool {
835 self.tokens.iter().all(|(_, n)| *n == 0)
836 }
837}
838
839/// A paired initial and final marking for a WF-net.
840///
841/// WF-net soundness (van der Aalst, 1998) requires both an *initial* marking
842/// (a single token on the source place) and a *final* marking (a single token
843/// on the sink place). `InitialFinalMarkingPair` bundles these two markings as
844/// a single named shape, so a WF-net construction site can pass them together
845/// without risk of swapping the two arguments.
846///
847/// ## Structural law
848///
849/// [`InitialFinalMarkingPair::validate`] checks that neither marking is empty
850/// and that the initial and final markings do not overlap (a place carrying
851/// tokens in both is a structural defect). It does **not** check that the
852/// markings reference declared places — that check is performed by
853/// [`WfNet::validate`].
854///
855/// Structure-only: a shape. No token dynamics.
856///
857/// ```
858/// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking, PetriRefusal};
859/// let pair = InitialFinalMarkingPair::new(
860/// Marking::new([("src".to_string(), 1)]),
861/// Marking::new([("snk".to_string(), 1)]),
862/// );
863/// assert!(pair.validate().is_ok());
864/// ```
865#[derive(Clone, Debug, PartialEq, Eq)]
866pub struct InitialFinalMarkingPair {
867 /// The initial marking (typically one token on the source place).
868 pub initial: Marking,
869 /// The final marking (typically one token on the sink place).
870 pub final_marking: Marking,
871}
872
873impl InitialFinalMarkingPair {
874 /// Construct an `InitialFinalMarkingPair` from two markings.
875 ///
876 /// ```
877 /// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking};
878 /// let pair = InitialFinalMarkingPair::new(
879 /// Marking::new([("src".to_string(), 1)]),
880 /// Marking::new([("snk".to_string(), 1)]),
881 /// );
882 /// assert_eq!(pair.initial.tokens_on("src"), 1);
883 /// assert_eq!(pair.final_marking.tokens_on("snk"), 1);
884 /// ```
885 pub fn new(initial: Marking, final_marking: Marking) -> Self {
886 InitialFinalMarkingPair {
887 initial,
888 final_marking,
889 }
890 }
891
892 /// Structurally validate the marking pair.
893 ///
894 /// Returns [`PetriRefusal::MissingInitialMarking`] if the initial marking is
895 /// empty, [`PetriRefusal::MissingFinalMarking`] if the final marking is empty,
896 /// or [`PetriRefusal::InvalidVariableArc`] if any place id appears in both
897 /// markings (overlapping initial/final places is a structural defect).
898 ///
899 /// ```
900 /// use wasm4pm_compat::petri::{InitialFinalMarkingPair, Marking, PetriRefusal};
901 /// // Empty initial marking:
902 /// let bad = InitialFinalMarkingPair::new(
903 /// Marking::empty(),
904 /// Marking::new([("snk".to_string(), 1)]),
905 /// );
906 /// assert_eq!(bad.validate(), Err(PetriRefusal::MissingInitialMarking));
907 /// ```
908 #[must_use = "check the shape-check result"]
909 pub fn validate(&self) -> Result<(), PetriRefusal> {
910 if self.initial.is_empty() {
911 return Err(PetriRefusal::MissingInitialMarking);
912 }
913 if self.final_marking.is_empty() {
914 return Err(PetriRefusal::MissingFinalMarking);
915 }
916 // Detect overlapping place ids between initial and final markings.
917 for (pid, cnt) in &self.initial.tokens {
918 if *cnt > 0 && self.final_marking.tokens_on(pid) > 0 {
919 return Err(PetriRefusal::InvalidVariableArc);
920 }
921 }
922 Ok(())
923 }
924}
925
926/// A plain Petri net: places, transitions, arcs, and an initial marking.
927///
928/// [`PetriNet::validate`] checks structural shape (arcs reference declared
929/// nodes); it does not analyze behavior. Reachability, boundedness, and
930/// soundness graduate to `wasm4pm`.
931///
932/// Structure-only.
933#[derive(Clone, Debug, Default, PartialEq, Eq)]
934pub struct PetriNet {
935 places: Vec<Place>,
936 transitions: Vec<Transition>,
937 arcs: Vec<Arc>,
938 initial: Marking,
939}
940
941impl PetriNet {
942 /// Construct a Petri net from its parts.
943 ///
944 /// ```
945 /// use wasm4pm_compat::petri::{PetriNet, Place, Transition, Arc, Marking};
946 /// let net = PetriNet::new(
947 /// [Place::new("p")],
948 /// [Transition::new("t", "a")],
949 /// [Arc::place_to_transition("p", "t")],
950 /// Marking::new([("p".to_string(), 1)]),
951 /// );
952 /// assert!(net.validate().is_ok());
953 /// ```
954 pub fn new(
955 places: impl IntoIterator<Item = Place>,
956 transitions: impl IntoIterator<Item = Transition>,
957 arcs: impl IntoIterator<Item = Arc>,
958 initial: Marking,
959 ) -> Self {
960 PetriNet {
961 places: places.into_iter().collect(),
962 transitions: transitions.into_iter().collect(),
963 arcs: arcs.into_iter().collect(),
964 initial,
965 }
966 }
967
968 /// The places.
969 pub fn places(&self) -> &[Place] {
970 &self.places
971 }
972
973 /// The transitions.
974 pub fn transitions(&self) -> &[Transition] {
975 &self.transitions
976 }
977
978 /// The arcs.
979 pub fn arcs(&self) -> &[Arc] {
980 &self.arcs
981 }
982
983 /// The initial marking.
984 pub fn initial_marking(&self) -> &Marking {
985 &self.initial
986 }
987
988 /// Structurally validate that every arc references declared nodes.
989 ///
990 /// This is a shape check, not behavior analysis. A dangling arc is reported
991 /// as [`PetriRefusal::InvalidVariableArc`] (this shape's structural
992 /// arc-defect law). Initial marking presence is required at the WF-net
993 /// level, not here.
994 ///
995 /// ```
996 /// use wasm4pm_compat::petri::{PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
997 /// // Arc references transition "ghost" that does not exist.
998 /// let net = PetriNet::new(
999 /// [Place::new("p")],
1000 /// [Transition::new("t", "a")],
1001 /// [Arc::place_to_transition("p", "ghost")],
1002 /// Marking::empty(),
1003 /// );
1004 /// assert_eq!(net.validate(), Err(PetriRefusal::InvalidVariableArc));
1005 /// ```
1006 #[must_use = "check the shape-check result"]
1007 pub fn validate(&self) -> Result<(), PetriRefusal> {
1008 use std::collections::HashSet;
1009 let pids: HashSet<&str> = self.places.iter().map(Place::id).collect();
1010 let tids: HashSet<&str> = self.transitions.iter().map(Transition::id).collect();
1011 for a in &self.arcs {
1012 if !pids.contains(a.place_id()) || !tids.contains(a.transition_id()) {
1013 return Err(PetriRefusal::InvalidVariableArc);
1014 }
1015 }
1016 Ok(())
1017 }
1018}
1019
1020/// Soundness state marker: soundness has **not** been asserted or proven.
1021///
1022/// This is an **uninhabited** type-level token (an empty enum) used only as a
1023/// `PhantomData` parameter on [`WfNet`]. It is never constructed; it carries no
1024/// data and costs nothing at runtime. It is the *default* state of a freshly
1025/// constructed WF-net.
1026#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1027pub enum SoundnessUnknown {}
1028
1029/// Soundness state marker: soundness is **claimed** but unproven here.
1030///
1031/// An uninhabited type-level token. A [`WfNet`] in this state asserts soundness
1032/// (e.g. by an upstream tool or a human) but this crate has **not** verified it —
1033/// verification is a `wasm4pm` engine. Treat this as a claim to be discharged,
1034/// not as evidence.
1035#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1036pub enum SoundnessClaimed {}
1037
1038/// Soundness state marker: soundness is **witnessed**.
1039///
1040/// An uninhabited type-level token. A [`WfNet`] reaches this state only by
1041/// re-attaching a soundness witness obtained from `wasm4pm`; this crate models
1042/// the *shape* of "soundness has been witnessed", not the witnessing itself.
1043#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1044pub enum SoundnessWitnessed {}
1045
1046/// A workflow net (WF-net): a Petri net with a single source and sink place and
1047/// a soundness state tracked at the type level by the `S` typestate parameter.
1048///
1049/// The `S` parameter is one of [`SoundnessUnknown`] (default),
1050/// [`SoundnessClaimed`], or [`SoundnessWitnessed`] — empty-enum markers used via
1051/// `PhantomData`. The field/parameter is a **claim about** soundness, never a
1052/// computed proof: [`WfNet::validate`] checks only *structural* WF-net shape
1053/// (initial and final markings present), and the soundness transition methods
1054/// merely *re-type* the claim; they do not compute soundness.
1055///
1056/// Structure-only: actual soundness witnessing graduates to `wasm4pm`, which
1057/// produces the witness that justifies moving to [`SoundnessWitnessed`].
1058#[derive(Clone, Debug, PartialEq, Eq)]
1059pub struct WfNet<S = SoundnessUnknown> {
1060 net: PetriNet,
1061 final_marking: Option<Marking>,
1062 _soundness: PhantomData<S>,
1063}
1064
1065impl WfNet<SoundnessUnknown> {
1066 /// Construct a WF-net in the [`SoundnessUnknown`] state from a Petri net and
1067 /// a final marking.
1068 ///
1069 /// ```
1070 /// use wasm4pm_compat::petri::{WfNet, PetriNet, Place, Transition, Arc, Marking};
1071 /// let net = PetriNet::new(
1072 /// [Place::new("src"), Place::new("snk")],
1073 /// [Transition::new("t", "a")],
1074 /// [Arc::place_to_transition("src", "t"), Arc::transition_to_place("t", "snk")],
1075 /// Marking::new([("src".to_string(), 1)]),
1076 /// );
1077 /// let wf = WfNet::new(net, Marking::new([("snk".to_string(), 1)]));
1078 /// assert!(wf.validate().is_ok());
1079 /// ```
1080 pub fn new(net: PetriNet, final_marking: Marking) -> Self {
1081 WfNet {
1082 net,
1083 final_marking: Some(final_marking),
1084 _soundness: PhantomData,
1085 }
1086 }
1087}
1088
1089impl<S> WfNet<S> {
1090 /// The underlying Petri net.
1091 pub fn net(&self) -> &PetriNet {
1092 &self.net
1093 }
1094
1095 /// The final marking, if declared.
1096 #[must_use]
1097 pub fn final_marking(&self) -> Option<&Marking> {
1098 self.final_marking.as_ref()
1099 }
1100
1101 /// Structurally validate the WF-net shape.
1102 ///
1103 /// Checks that the underlying net is structurally well-formed, that an
1104 /// initial marking places at least one token
1105 /// ([`PetriRefusal::MissingInitialMarking`]), and that a final marking is
1106 /// declared ([`PetriRefusal::MissingFinalMarking`]). It does **not** check
1107 /// soundness, safeness, or boundedness — those graduate to `wasm4pm`.
1108 ///
1109 /// ```
1110 /// use wasm4pm_compat::petri::{WfNet, PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
1111 /// // Initial marking is empty -> MissingInitialMarking.
1112 /// let net = PetriNet::new(
1113 /// [Place::new("src"), Place::new("snk")],
1114 /// [Transition::new("t", "a")],
1115 /// [Arc::place_to_transition("src", "t"), Arc::transition_to_place("t", "snk")],
1116 /// Marking::empty(),
1117 /// );
1118 /// let wf = WfNet::new(net, Marking::new([("snk".to_string(), 1)]));
1119 /// assert_eq!(wf.validate(), Err(PetriRefusal::MissingInitialMarking));
1120 /// ```
1121 #[must_use = "check the shape-check result"]
1122 pub fn validate(&self) -> Result<(), PetriRefusal> {
1123 self.net.validate()?;
1124 if self.net.initial_marking().is_empty() {
1125 return Err(PetriRefusal::MissingInitialMarking);
1126 }
1127 match &self.final_marking {
1128 Some(m) if !m.is_empty() => Ok(()),
1129 _ => Err(PetriRefusal::MissingFinalMarking),
1130 }
1131 }
1132
1133 /// Re-type this WF-net as carrying a soundness **claim** (unproven here).
1134 ///
1135 /// This is a *type-level re-tagging only* — it computes nothing. Use it to
1136 /// record that an upstream source asserts soundness; discharge the claim by
1137 /// graduating to `wasm4pm` and witnessing it.
1138 ///
1139 /// ```
1140 /// use wasm4pm_compat::petri::{WfNet, PetriNet, Marking, SoundnessClaimed};
1141 /// let wf = WfNet::new(PetriNet::default(), Marking::new([("snk".to_string(), 1)]));
1142 /// let _claimed: WfNet<SoundnessClaimed> = wf.claim_sound();
1143 /// ```
1144 pub fn claim_sound(self) -> WfNet<SoundnessClaimed> {
1145 WfNet {
1146 net: self.net,
1147 final_marking: self.final_marking,
1148 _soundness: PhantomData,
1149 }
1150 }
1151}
1152
1153impl WfNet<SoundnessClaimed> {
1154 /// Re-type a *claimed* WF-net as **witnessed** sound.
1155 ///
1156 /// **Deprecated** — this method freely advances the typestate without a
1157 /// [`SoundnessProof`] token, making the `Claimed → Witnessed` transition
1158 /// forgeable from outside this module. Use [`WfNetConst::witness_soundness`]
1159 /// instead, which requires a `SoundnessProof` that is only constructible
1160 /// inside this module or via the `wasm4pm` graduation bridge. That path is
1161 /// non-forgeable by construction.
1162 ///
1163 /// This method is retained for source compatibility but will be removed in a
1164 /// future version. Any call site that invokes this method is relying on a
1165 /// forgeability hole in the type law.
1166 ///
1167 /// ```rust,ignore
1168 /// // pub(crate): cannot be called from doctests. Use WfNetConst<Sane> and
1169 /// // its sealed construction path for the non-forgeable witnessed state.
1170 /// use wasm4pm_compat::petri::{WfNet, PetriNet, Marking, SoundnessClaimed, SoundnessWitnessed};
1171 /// let wf = WfNet::new(PetriNet::default(), Marking::new([("snk".to_string(), 1)]))
1172 /// .claim_sound();
1173 /// #[allow(deprecated)]
1174 /// let _w: WfNet<SoundnessWitnessed> = wf.attest_witnessed();
1175 /// ```
1176 #[deprecated(
1177 since = "26.6.5",
1178 note = "Forgeability hole: this method produces WfNet<SoundnessWitnessed> without \
1179 any structural verification. Use WfNetConst<Sane> and its sealed \
1180 construction path instead. See GAP_007 in sources/wasm4pm-compat/STRUCTURAL_GAPS.md."
1181 )]
1182 #[allow(dead_code)]
1183 pub(crate) fn attest_witnessed(self) -> WfNet<SoundnessWitnessed> {
1184 WfNet {
1185 net: self.net,
1186 final_marking: self.final_marking,
1187 _soundness: PhantomData,
1188 }
1189 }
1190}
1191
1192/// An object-centric Petri net (OC-Petri-net): a Petri net whose arcs are typed
1193/// by object type and may be variable, plus the declared object types.
1194///
1195/// [`ObjectCentricPetriNet::validate`] checks that every typed arc names a
1196/// declared object type ([`PetriRefusal::ObjectTypeNotPreserved`]) and that the
1197/// underlying net is structurally sound (arcs reference declared nodes). It does
1198/// not analyze object-centric behavior — that graduates to `wasm4pm`.
1199///
1200/// Structure-only.
1201#[derive(Clone, Debug, Default, PartialEq, Eq)]
1202pub struct ObjectCentricPetriNet {
1203 net: PetriNet,
1204 object_types: Vec<String>,
1205}
1206
1207impl ObjectCentricPetriNet {
1208 /// Construct an OC-Petri-net from a Petri net and its object types.
1209 ///
1210 /// ```
1211 /// use wasm4pm_compat::petri::{ObjectCentricPetriNet, PetriNet, Place, Transition, Arc, Marking};
1212 /// let net = PetriNet::new(
1213 /// [Place::new("p")],
1214 /// [Transition::new("t", "a")],
1215 /// [Arc::place_to_transition("p", "t").typed("order", false)],
1216 /// Marking::empty(),
1217 /// );
1218 /// let ocpn = ObjectCentricPetriNet::new(net, ["order".to_string()]);
1219 /// assert!(ocpn.validate().is_ok());
1220 /// ```
1221 pub fn new(net: PetriNet, object_types: impl IntoIterator<Item = String>) -> Self {
1222 ObjectCentricPetriNet {
1223 net,
1224 object_types: object_types.into_iter().collect(),
1225 }
1226 }
1227
1228 /// The underlying Petri net.
1229 pub fn net(&self) -> &PetriNet {
1230 &self.net
1231 }
1232
1233 /// The declared object types.
1234 pub fn object_types(&self) -> &[String] {
1235 &self.object_types
1236 }
1237
1238 /// Structurally validate the OC-Petri-net shape.
1239 ///
1240 /// Checks the underlying net structurally, then verifies every typed arc's
1241 /// object type is declared ([`PetriRefusal::ObjectTypeNotPreserved`]). No
1242 /// behavior analysis is performed.
1243 ///
1244 /// ```
1245 /// use wasm4pm_compat::petri::{ObjectCentricPetriNet, PetriNet, Place, Transition, Arc, Marking, PetriRefusal};
1246 /// // Arc typed "ghost" but only "order" is declared.
1247 /// let net = PetriNet::new(
1248 /// [Place::new("p")],
1249 /// [Transition::new("t", "a")],
1250 /// [Arc::place_to_transition("p", "t").typed("ghost", false)],
1251 /// Marking::empty(),
1252 /// );
1253 /// let ocpn = ObjectCentricPetriNet::new(net, ["order".to_string()]);
1254 /// assert_eq!(ocpn.validate(), Err(PetriRefusal::ObjectTypeNotPreserved));
1255 /// ```
1256 #[must_use = "check the shape-check result"]
1257 pub fn validate(&self) -> Result<(), PetriRefusal> {
1258 self.net.validate()?;
1259 for a in self.net.arcs() {
1260 if let Some(ot) = a.object_type() {
1261 if !self.object_types.iter().any(|t| t == ot) {
1262 return Err(PetriRefusal::ObjectTypeNotPreserved);
1263 }
1264 }
1265 }
1266 Ok(())
1267 }
1268}
1269
1270// ── YAWL cancellation-region shape ──────────────────────────────────────────
1271
1272/// A YAWL cancellation region: the named set of place/condition/task ids whose
1273/// tokens are vacuumed when the owning task fires.
1274///
1275/// YAWL Definition 1 (van der Aalst & ter Hofstede, 2004) specifies
1276/// `rem: T ⇸ P(T ∪ C \ {i, o})` — each task optionally names a cancellation
1277/// region. This struct carries the *shape* of that region: a named set of node
1278/// ids. Token-removal execution (the actual vacuuming) graduates to `wasm4pm`.
1279///
1280/// The `#[repr(transparent)]` newtype prevents a bare `Vec<String>` from being
1281/// accidentally passed where a `CancellationRegion` is required. It is zero-cost
1282/// to hold and clone.
1283///
1284/// Structure-only: carries ids, never fires.
1285///
1286/// ```
1287/// use wasm4pm_compat::petri::CancellationRegion;
1288/// let cr = CancellationRegion::new(["p1", "t2"]);
1289/// assert_eq!(cr.members(), &["p1".to_string(), "t2".to_string()]);
1290/// ```
1291#[repr(transparent)]
1292#[derive(Clone, Debug, PartialEq, Eq, Default)]
1293pub struct CancellationRegion {
1294 /// The ids of nodes (places, conditions, tasks) in this cancellation region,
1295 /// excluding the initial and final place of the net (i, o).
1296 pub members: Vec<String>,
1297}
1298
1299impl CancellationRegion {
1300 /// Construct a cancellation region from an iterator of node ids.
1301 ///
1302 /// ```
1303 /// use wasm4pm_compat::petri::CancellationRegion;
1304 /// let cr = CancellationRegion::new(["p1", "t2"]);
1305 /// assert_eq!(cr.members().len(), 2);
1306 /// ```
1307 pub fn new<I, S>(members: I) -> Self
1308 where
1309 I: IntoIterator<Item = S>,
1310 S: Into<String>,
1311 {
1312 CancellationRegion {
1313 members: members.into_iter().map(Into::into).collect(),
1314 }
1315 }
1316
1317 /// The node ids in this cancellation region.
1318 ///
1319 /// ```
1320 /// use wasm4pm_compat::petri::CancellationRegion;
1321 /// assert!(CancellationRegion::default().members().is_empty());
1322 /// ```
1323 pub fn members(&self) -> &[String] {
1324 &self.members
1325 }
1326}
1327
1328// ── YAWL multiple-instance task spec ────────────────────────────────────────
1329
1330/// The creation kind for a YAWL multiple-instance task.
1331///
1332/// YAWL Definition 1 defines `nofi: T ⇸ N × N^∞ × N^∞ × {dynamic, static}`.
1333/// The `{Static, Dynamic}` variant names whether child instances are created
1334/// once at firing time (`Static`) or may be created incrementally during
1335/// execution (`Dynamic`). This is the structural tag; execution semantics of
1336/// dynamic spawning graduate to `wasm4pm`.
1337///
1338/// Structure-only marker; carries no instantiation behavior.
1339///
1340/// ```
1341/// use wasm4pm_compat::petri::InstanceCreationKind;
1342/// let k = InstanceCreationKind::Static;
1343/// assert_eq!(k, InstanceCreationKind::Static);
1344/// ```
1345#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1346pub enum InstanceCreationKind {
1347 /// All child instances are created at the moment the task fires.
1348 Static,
1349 /// Child instances may be created incrementally during the task's lifetime.
1350 Dynamic,
1351}
1352
1353/// A YAWL multiple-instance task specification: the four-tuple
1354/// `(min_instances, max_instances, threshold, creation_kind)`.
1355///
1356/// YAWL Definition 1 (`nofi`) mandates that every multi-instance task carries:
1357/// - `min`: the minimum number of child instances that must complete (`N`).
1358/// - `max`: the maximum number of child instances (`N^∞`), `None` = unbounded.
1359/// - `threshold`: the minimum count for collective completion (`N^∞`),
1360/// `None` = all instances must complete.
1361/// - `creation`: whether spawning is [`InstanceCreationKind::Static`] or
1362/// [`InstanceCreationKind::Dynamic`].
1363///
1364/// The structural law `min <= max` (when `max` is bounded) is checked by
1365/// [`MultipleInstanceSpec::validate`]; a violation is refused as
1366/// [`PetriRefusal::InvalidInstanceBounds`].
1367///
1368/// Structure-only: instance creation and threshold enforcement graduate to
1369/// `wasm4pm`.
1370///
1371/// ```
1372/// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind};
1373/// let spec = MultipleInstanceSpec::new(1, Some(4), Some(2), InstanceCreationKind::Static);
1374/// assert!(spec.validate().is_ok());
1375/// ```
1376#[derive(Clone, Debug, PartialEq, Eq)]
1377pub struct MultipleInstanceSpec {
1378 /// Minimum number of instances that must complete.
1379 pub min: u32,
1380 /// Maximum number of instances (`None` = unbounded / ∞).
1381 pub max: Option<u32>,
1382 /// Threshold for collective completion (`None` = all instances).
1383 pub threshold: Option<u32>,
1384 /// Whether child instances are created statically or dynamically.
1385 pub creation: InstanceCreationKind,
1386}
1387
1388impl MultipleInstanceSpec {
1389 /// Construct a multiple-instance spec.
1390 ///
1391 /// ```
1392 /// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind};
1393 /// let spec = MultipleInstanceSpec::new(1, Some(3), None, InstanceCreationKind::Dynamic);
1394 /// assert_eq!(spec.min, 1);
1395 /// assert_eq!(spec.creation, InstanceCreationKind::Dynamic);
1396 /// ```
1397 pub fn new(
1398 min: u32,
1399 max: Option<u32>,
1400 threshold: Option<u32>,
1401 creation: InstanceCreationKind,
1402 ) -> Self {
1403 MultipleInstanceSpec {
1404 min,
1405 max,
1406 threshold,
1407 creation,
1408 }
1409 }
1410
1411 /// Structurally validate the instance bounds.
1412 ///
1413 /// Returns [`PetriRefusal::InvalidInstanceBounds`] if `max` is bounded and
1414 /// `min > max`, or if `min == 0` (at least one instance is required by YAWL).
1415 ///
1416 /// ```
1417 /// use wasm4pm_compat::petri::{MultipleInstanceSpec, InstanceCreationKind, PetriRefusal};
1418 /// let bad = MultipleInstanceSpec::new(5, Some(2), None, InstanceCreationKind::Static);
1419 /// assert_eq!(bad.validate(), Err(PetriRefusal::InvalidInstanceBounds));
1420 /// let zero = MultipleInstanceSpec::new(0, Some(1), None, InstanceCreationKind::Static);
1421 /// assert_eq!(zero.validate(), Err(PetriRefusal::InvalidInstanceBounds));
1422 /// ```
1423 #[must_use = "check the shape-check result"]
1424 pub fn validate(&self) -> Result<(), PetriRefusal> {
1425 if self.min == 0 {
1426 return Err(PetriRefusal::InvalidInstanceBounds);
1427 }
1428 if let Some(max) = self.max {
1429 if self.min > max {
1430 return Err(PetriRefusal::InvalidInstanceBounds);
1431 }
1432 }
1433 Ok(())
1434 }
1435}
1436
1437// ── YAWL multiple-instance bounds — compile-time law surface ────────────────
1438
1439/// A YAWL multiple-instance spec with bounds enforced **at compile time**.
1440///
1441/// `MultipleInstanceSpecConst<MIN, MAX>` encodes the YAWL Definition 1 `nofi`
1442/// invariant `1 ≤ MIN ≤ MAX` as const-generic where-bounds so that a violation
1443/// is a **compile error**, not a runtime refusal.
1444///
1445/// Law: YAWL Definition 1 nofi — `min: N`, `max: N^∞`, `1 ≤ min ≤ max`.
1446///
1447/// ## Compile-time negative receipt
1448///
1449/// `MultipleInstanceSpecConst<5, 2>` does **not compile** because `5 <= 2` is
1450/// false — the `Require<{ MIN <= MAX }>: IsTrue` bound fails at the type level.
1451/// `MultipleInstanceSpecConst<0, 4>` does **not compile** because `0 >= 1` is
1452/// false — `Require<{ MIN >= 1 }>: IsTrue` fails.
1453///
1454/// Use [`MultipleInstanceSpec`] for runtime-validated, dynamically constructed
1455/// specs. Use this type when the bounds are known at compile time and you want
1456/// the compiler to verify them.
1457///
1458/// Structure-only: zero-cost (`_private: ()`) marker that carries the min/max
1459/// law in its type. No engine logic.
1460///
1461/// ```
1462/// # #![feature(generic_const_exprs)]
1463/// # #![allow(incomplete_features)]
1464/// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1465/// // 1 <= 4: lawful at compile time.
1466/// let _: MultipleInstanceSpecConst<1, 4> = MultipleInstanceSpecConst::new();
1467/// ```
1468///
1469/// ```compile_fail
1470/// # #![feature(generic_const_exprs)]
1471/// # #![allow(incomplete_features)]
1472/// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1473/// // 5 > 2: compile error — Require<{ 5 <= 2 }>: IsTrue not satisfied.
1474/// let _: MultipleInstanceSpecConst<5, 2> = MultipleInstanceSpecConst::new();
1475/// ```
1476pub struct MultipleInstanceSpecConst<const MIN: u32, const MAX: u32>
1477where
1478 crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1479 crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1480{
1481 _private: (),
1482}
1483
1484impl<const MIN: u32, const MAX: u32> MultipleInstanceSpecConst<MIN, MAX>
1485where
1486 crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1487 crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1488{
1489 /// Construct a `MultipleInstanceSpecConst<MIN, MAX>` — only possible when
1490 /// `MIN >= 1` and `MIN <= MAX`.
1491 ///
1492 /// ```
1493 /// # #![feature(generic_const_exprs)]
1494 /// # #![allow(incomplete_features)]
1495 /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1496 /// let _: MultipleInstanceSpecConst<1, 1> = MultipleInstanceSpecConst::new();
1497 /// let _: MultipleInstanceSpecConst<2, 10> = MultipleInstanceSpecConst::new();
1498 /// ```
1499 pub const fn new() -> Self {
1500 MultipleInstanceSpecConst { _private: () }
1501 }
1502
1503 /// The minimum instance count encoded in the type.
1504 ///
1505 /// ```
1506 /// # #![feature(generic_const_exprs)]
1507 /// # #![allow(incomplete_features)]
1508 /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1509 /// assert_eq!(MultipleInstanceSpecConst::<2, 5>::new().min(), 2);
1510 /// ```
1511 pub const fn min(&self) -> u32 {
1512 MIN
1513 }
1514
1515 /// The maximum instance count encoded in the type.
1516 ///
1517 /// ```
1518 /// # #![feature(generic_const_exprs)]
1519 /// # #![allow(incomplete_features)]
1520 /// use wasm4pm_compat::petri::MultipleInstanceSpecConst;
1521 /// assert_eq!(MultipleInstanceSpecConst::<2, 5>::new().max(), 5);
1522 /// ```
1523 pub const fn max(&self) -> u32 {
1524 MAX
1525 }
1526}
1527
1528impl<const MIN: u32, const MAX: u32> Default for MultipleInstanceSpecConst<MIN, MAX>
1529where
1530 crate::law::Require<{ MIN >= 1 }>: crate::law::IsTrue,
1531 crate::law::Require<{ MIN <= MAX }>: crate::law::IsTrue,
1532{
1533 fn default() -> Self {
1534 Self::new()
1535 }
1536}
1537
1538/// The specific, named laws under which Petri-net / WF-net / OC-Petri-net
1539/// structure is refused.
1540///
1541/// Each variant cites a distinct law — never a bare "invalid input".
1542/// [`PetriRefusal::SoundnessNotWitnessed`] is the boundary law for evidence:
1543/// it marks the refusal to treat a [`SoundnessClaimed`] net as if it were
1544/// [`SoundnessWitnessed`] without a witness from `wasm4pm`.
1545#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1546#[non_exhaustive]
1547pub enum PetriRefusal {
1548 /// A WF-net declares no (non-empty) initial marking.
1549 MissingInitialMarking,
1550 /// A WF-net declares no (non-empty) final marking.
1551 MissingFinalMarking,
1552 /// A transition can never fire (a behavioral defect surfaced from `wasm4pm`).
1553 DeadTransition,
1554 /// The net is not safe (a place can hold more than one token where one was
1555 /// required) — surfaced from analysis at `wasm4pm`.
1556 UnsafeNet,
1557 /// The net is unbounded — surfaced from analysis at `wasm4pm`.
1558 UnboundedNet,
1559 /// An OC-Petri-net arc carries an object type that is not declared/preserved.
1560 ObjectTypeNotPreserved,
1561 /// An arc is structurally invalid (e.g. dangling endpoint, malformed
1562 /// variable arc).
1563 InvalidVariableArc,
1564 /// Soundness was relied upon but not witnessed — graduate to `wasm4pm` to
1565 /// obtain the witness.
1566 SoundnessNotWitnessed,
1567 /// A [`CancellationRegion`] references a node id not declared in the net.
1568 ///
1569 /// Law: YAWL Definition 1 rem(t) ⊆ T ∪ C \ {i, o}.
1570 InvalidCancellationRegion,
1571 /// A [`MultipleInstanceSpec`] has `min == 0` or `min > max`.
1572 ///
1573 /// Law: YAWL Definition 1 nofi invariant: `1 ≤ min ≤ max`.
1574 InvalidInstanceBounds,
1575}
1576
1577/// A WF-net tagged as **separable** — a necessary precondition for lossless
1578/// POWL 2.0 conversion (Kourani, Park & van der Aalst, 2026).
1579///
1580/// A WF-net is *separable* if it can be decomposed into a set of structurally
1581/// independent sub-nets whose composition reproduces the original language. Only
1582/// a separable WF-net can be faithfully represented as a POWL 2.0 model via the
1583/// POWL decomposition theorem (Kourani 2026, Definition 4.1).
1584///
1585/// ## Structure-only
1586///
1587/// This marker records the *claim* that a WF-net is separable; it does not
1588/// verify separability (that graduates to `wasm4pm`). The claim prevents code
1589/// from inadvertently feeding a non-separable net to a POWL 2.0 conversion
1590/// path at the type level.
1591///
1592/// ## How to use
1593///
1594/// Wrap your `WfNetConst` in a `SeparableWfNet` after verifying separability
1595/// via the `wasm4pm` engine and attaching the result here. The type-level
1596/// marker then flows through the rest of the pipeline.
1597///
1598/// ## Paper
1599///
1600/// Kourani, Park & van der Aalst (2026) — *Hierarchical Decomposition of
1601/// Separable Workflow-Nets*. Definition 4.1 (separable WF-net) and
1602/// Theorem 4.3 (POWL 2.0 language preservation under decomposition).
1603///
1604/// Structure-only: carries no behavior, no token semantics.
1605pub struct SeparableWfNet<const SOUNDNESS: crate::law::SoundnessState> {
1606 /// The underlying WF-net with its soundness state.
1607 pub net: WfNetConst<SOUNDNESS>,
1608 // Private seal — separability claim is not forgeable from outside this module.
1609 _seal: wfnet_seal::WfNetSeal,
1610}
1611
1612impl<const SOUNDNESS: crate::law::SoundnessState> SeparableWfNet<SOUNDNESS> {
1613 /// Construct a `SeparableWfNet` from a verified `WfNetConst`.
1614 ///
1615 /// This is `pub(crate)` — the only public construction path is through the
1616 /// `wasm4pm` graduation bridge that verifies separability.
1617 ///
1618 /// ```
1619 /// use wasm4pm_compat::petri::{WfNetConst, SeparableWfNet};
1620 /// use wasm4pm_compat::law::SoundnessState;
1621 /// let _: SeparableWfNet<{ SoundnessState::Unknown }> =
1622 /// SeparableWfNet::declare_separable(WfNetConst::new());
1623 /// ```
1624 pub fn declare_separable(net: WfNetConst<SOUNDNESS>) -> Self {
1625 SeparableWfNet {
1626 net,
1627 _seal: wfnet_seal::WfNetSeal,
1628 }
1629 }
1630}
1631
1632/// A standalone, named error type for the *missing final marking* law.
1633///
1634/// [`PetriRefusal::MissingFinalMarking`] names the law as a variant on the
1635/// shared refusal enum. `MissingFinalMarkingError` is a separate zero-sized
1636/// struct that names the same law as a **first-class type** — useful when a
1637/// function returns `Result<_, MissingFinalMarkingError>` instead of the broad
1638/// `Result<_, PetriRefusal>`, making the specific law visible in the return
1639/// type signature.
1640///
1641/// Law: a WF-net must have a declared, non-empty final marking (van der Aalst,
1642/// 1998 — "proper completion" requires a designated final state).
1643///
1644/// ## Conversion
1645///
1646/// `MissingFinalMarkingError` converts to [`PetriRefusal::MissingFinalMarking`]
1647/// via `From<MissingFinalMarkingError> for PetriRefusal`.
1648///
1649/// Structure-only: zero-sized, carries no data.
1650///
1651/// ```
1652/// use wasm4pm_compat::petri::{MissingFinalMarkingError, PetriRefusal};
1653/// let e = MissingFinalMarkingError;
1654/// let r: PetriRefusal = e.into();
1655/// assert_eq!(r, PetriRefusal::MissingFinalMarking);
1656/// ```
1657#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1658pub struct MissingFinalMarkingError;
1659
1660impl core::fmt::Display for MissingFinalMarkingError {
1661 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1662 write!(f, "WF-net refused by law: MissingFinalMarking")
1663 }
1664}
1665
1666impl From<MissingFinalMarkingError> for PetriRefusal {
1667 fn from(_: MissingFinalMarkingError) -> Self {
1668 PetriRefusal::MissingFinalMarking
1669 }
1670}
1671
1672/// An uninhabited type-level marker asserting the *separability claim* about a
1673/// WF-net, independent of the [`SeparableWfNet`] wrapper struct.
1674///
1675/// `SeparableWfNetMarker` is used as a `PhantomData` type parameter in
1676/// downstream types that need to carry a separability claim through a pipeline
1677/// without wrapping the whole WF-net. The marker is an empty enum and therefore
1678/// uninhabited — it can only appear as a `PhantomData` field.
1679///
1680/// ## Why both `SeparableWfNet` and `SeparableWfNetMarker`?
1681///
1682/// [`SeparableWfNet`] wraps a [`WfNetConst`] — it is a value-carrying container.
1683/// `SeparableWfNetMarker` is a *phantom* — it marks a type as "was derived from
1684/// a separable net" without copying the net value. Use the marker when the net
1685/// itself does not need to travel with the downstream type.
1686///
1687/// ## Paper
1688///
1689/// Kourani, Park & van der Aalst (2026) — *Hierarchical Decomposition of
1690/// Separable Workflow-Nets*. Definition 4.1.
1691///
1692/// Structure-only: uninhabited, zero-cost phantom marker.
1693///
1694/// ```
1695/// use core::marker::PhantomData;
1696/// use wasm4pm_compat::petri::SeparableWfNetMarker;
1697///
1698/// // A downstream type that remembers the net was separable.
1699/// struct PowlConversionResult<S>(PhantomData<S>);
1700/// let _: PowlConversionResult<SeparableWfNetMarker>;
1701/// ```
1702#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1703pub enum SeparableWfNetMarker {}
1704
1705// ── Stochastic Petri net structural annotations ──────────────────────────────
1706
1707/// An arc weight annotation carrying a stochastic firing rate or probability.
1708///
1709/// In a *stochastic Petri net* (SPN), each transition is associated with a
1710/// firing rate (exponentially distributed) or a weight (in Generalised SPNs,
1711/// for immediate transitions). `StochasticArcWeight` wraps the `f64` rate or
1712/// weight as a named structural annotation — it does **not** represent an
1713/// execution probability or trigger a firing.
1714///
1715/// Structure-only: a rate/weight annotation. Stochastic simulation and
1716/// steady-state analysis graduate to `wasm4pm`.
1717///
1718/// ## Paper / Reference
1719///
1720/// Molloy (1982) — *Performance Analysis Using Stochastic Petri Nets*.
1721/// Marsan et al. (1984) — *A class of Generalised Stochastic Petri Nets*.
1722///
1723/// ```
1724/// use wasm4pm_compat::petri::StochasticArcWeight;
1725/// let w = StochasticArcWeight(0.5);
1726/// assert_eq!(w.0, 0.5);
1727/// ```
1728#[derive(Clone, Copy, Debug, PartialEq)]
1729pub struct StochasticArcWeight(pub f64);
1730
1731/// A zero-time transition marker — an *immediate* transition in a
1732/// Generalised Stochastic Petri Net (GSPN).
1733///
1734/// Immediate transitions fire instantaneously (zero delay) and with a
1735/// priority that supersedes timed transitions when both are enabled.
1736/// `ImmediateTransition` is a zero-sized structural tag applied to a
1737/// [`Transition`] to assert that it is immediate. It carries no weight
1738/// or priority value — those graduate to `wasm4pm`.
1739///
1740/// Structure-only: a zero-sized marker. Priority resolution and firing
1741/// semantics graduate to `wasm4pm`.
1742///
1743/// ## Paper / Reference
1744///
1745/// Marsan et al. (1984) — GSPN Definition 2 (immediate vs. timed transitions).
1746///
1747/// ```
1748/// use wasm4pm_compat::petri::ImmediateTransition;
1749/// let _: ImmediateTransition;
1750/// ```
1751#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1752pub struct ImmediateTransition;
1753
1754/// A timed transition annotation carrying an exponential firing rate.
1755///
1756/// In a Stochastic Petri Net, a *timed* transition fires after an
1757/// exponentially-distributed delay with rate λ (`self.0`). Higher rates
1758/// mean shorter expected delays. `TimedTransition` wraps λ as a named
1759/// structural annotation; it does **not** compute delays or trigger firings.
1760///
1761/// Structure-only: a rate annotation. Simulation and analysis graduate
1762/// to `wasm4pm`.
1763///
1764/// ## Paper / Reference
1765///
1766/// Molloy (1982) — exponential firing-rate model. Marsan et al. (1984) —
1767/// GSPN Definition 2 (timed transitions with rate λ).
1768///
1769/// ```
1770/// use wasm4pm_compat::petri::TimedTransition;
1771/// let t = TimedTransition(2.5);
1772/// assert_eq!(t.0, 2.5);
1773/// ```
1774#[derive(Clone, Copy, Debug, PartialEq)]
1775pub struct TimedTransition(pub f64);
1776
1777impl core::fmt::Display for PetriRefusal {
1778 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1779 let law = match self {
1780 PetriRefusal::MissingInitialMarking => "MissingInitialMarking",
1781 PetriRefusal::MissingFinalMarking => "MissingFinalMarking",
1782 PetriRefusal::DeadTransition => "DeadTransition",
1783 PetriRefusal::UnsafeNet => "UnsafeNet",
1784 PetriRefusal::UnboundedNet => "UnboundedNet",
1785 PetriRefusal::ObjectTypeNotPreserved => "ObjectTypeNotPreserved",
1786 PetriRefusal::InvalidVariableArc => "InvalidVariableArc",
1787 PetriRefusal::SoundnessNotWitnessed => "SoundnessNotWitnessed",
1788 PetriRefusal::InvalidCancellationRegion => "InvalidCancellationRegion",
1789 PetriRefusal::InvalidInstanceBounds => "InvalidInstanceBounds",
1790 };
1791 write!(f, "Petri-net refused by law: {law}")
1792 }
1793}
1794
1795// ── FlatIncidenceMatrix ───────────────────────────────────────────────────────
1796
1797/// A flat (row-major) incidence matrix for a Petri net.
1798///
1799/// Rows are places, columns are transitions. Value at
1800/// `[place_idx * transitions_count + transition_idx]` is the net token change
1801/// (+1 produced, -1 consumed, 0 no arc).
1802///
1803/// This is structure only — it carries a precomputed matrix, never derives one.
1804/// Corresponds to `FlatIncidenceMatrix` in `wasm4pm-types::models`.
1805#[derive(Clone, Debug, PartialEq, Eq)]
1806pub struct FlatIncidenceMatrix {
1807 pub data: Vec<i32>,
1808 pub places_count: usize,
1809 pub transitions_count: usize,
1810}
1811
1812impl FlatIncidenceMatrix {
1813 pub fn get(&self, place_idx: usize, transition_idx: usize) -> i32 {
1814 self.data[place_idx * self.transitions_count + transition_idx]
1815 }
1816}
1817
1818// ── Van der Aalst mutable Petri net construction ──────────────────────────────
1819//
1820// PM4Py (van der Aalst's reference implementation) constructs Petri nets by
1821// creating an empty net, then adding places, transitions, and arcs one at a
1822// time:
1823//
1824// net = PetriNet()
1825// source = PetriNet.Place("source"); net.places.add(source)
1826// t = PetriNet.Transition("t_A", "A"); net.transitions.add(t)
1827// petri_utils.add_arc_from_to(source, t, net)
1828//
1829// PetriNetBuilder mirrors that API in Rust. Algorithms in `wasm4pm` that
1830// produce Petri nets (Alpha, Alpha++, Heuristics Miner, Inductive Miner) use
1831// this builder rather than constructing the final tuple in one shot.
1832
1833/// Mutable Petri net builder — van der Aalst's PM4Py construction pattern in Rust.
1834///
1835/// Construct a net by adding places, transitions, and arcs incrementally, then
1836/// call [`build`](PetriNetBuilder::build) to obtain a validated [`PetriNet`].
1837///
1838/// ## Example
1839///
1840/// ```
1841/// use wasm4pm_compat::petri::PetriNetBuilder;
1842///
1843/// let mut b = PetriNetBuilder::new();
1844/// b.place("source")
1845/// .place("p1")
1846/// .place("sink")
1847/// .transition("t_A", "register")
1848/// .transition("t_B", "approve")
1849/// .p_to_t("source", "t_A")
1850/// .t_to_p("t_A", "p1")
1851/// .p_to_t("p1", "t_B")
1852/// .t_to_p("t_B", "sink")
1853/// .initial_tokens([("source", 1)]);
1854/// let net = b.build().expect("valid net");
1855///
1856/// assert!(net.validate().is_ok());
1857/// assert_eq!(net.places().len(), 3);
1858/// assert_eq!(net.transitions().len(), 2);
1859/// ```
1860#[derive(Debug, Default)]
1861pub struct PetriNetBuilder {
1862 places: Vec<Place>,
1863 transitions: Vec<Transition>,
1864 arcs: Vec<Arc>,
1865 initial_tokens: Vec<(String, u32)>,
1866}
1867
1868impl PetriNetBuilder {
1869 /// Create an empty builder.
1870 pub fn new() -> Self {
1871 Self::default()
1872 }
1873
1874 /// Add a place by id. Builder-style (returns `&mut Self`).
1875 ///
1876 /// ```
1877 /// use wasm4pm_compat::petri::PetriNetBuilder;
1878 /// let mut b = PetriNetBuilder::new();
1879 /// b.place("p1").place("p2");
1880 /// let net = b.build_unchecked();
1881 /// assert_eq!(net.places().len(), 2);
1882 /// ```
1883 pub fn place(&mut self, id: impl Into<String>) -> &mut Self {
1884 self.places.push(Place::new(id));
1885 self
1886 }
1887
1888 /// Add a visible (labeled) transition.
1889 ///
1890 /// ```
1891 /// use wasm4pm_compat::petri::PetriNetBuilder;
1892 /// let mut b = PetriNetBuilder::new();
1893 /// b.place("p").transition("t", "A");
1894 /// let net = b.build_unchecked();
1895 /// assert!(!net.transitions()[0].is_silent());
1896 /// ```
1897 pub fn transition(&mut self, id: impl Into<String>, label: impl Into<String>) -> &mut Self {
1898 self.transitions.push(Transition::new(id, label));
1899 self
1900 }
1901
1902 /// Add a silent (tau / invisible) transition.
1903 ///
1904 /// Silent transitions model routing decisions without corresponding
1905 /// log events — van der Aalst's τ-transitions.
1906 ///
1907 /// ```
1908 /// use wasm4pm_compat::petri::PetriNetBuilder;
1909 /// let mut b = PetriNetBuilder::new();
1910 /// b.place("p").silent("tau_split");
1911 /// let net = b.build_unchecked();
1912 /// assert!(net.transitions()[0].is_silent());
1913 /// ```
1914 pub fn silent(&mut self, id: impl Into<String>) -> &mut Self {
1915 self.transitions.push(Transition::silent(id));
1916 self
1917 }
1918
1919 /// Add a place-to-transition arc (van der Aalst: `p → t`, input arc).
1920 ///
1921 /// Corresponds to `petri_utils.add_arc_from_to(place, transition, net)`.
1922 pub fn p_to_t(
1923 &mut self,
1924 place_id: impl Into<String>,
1925 transition_id: impl Into<String>,
1926 ) -> &mut Self {
1927 self.arcs
1928 .push(Arc::place_to_transition(place_id, transition_id));
1929 self
1930 }
1931
1932 /// Add a transition-to-place arc (van der Aalst: `t → p`, output arc).
1933 ///
1934 /// Corresponds to `petri_utils.add_arc_from_to(transition, place, net)`.
1935 pub fn t_to_p(
1936 &mut self,
1937 transition_id: impl Into<String>,
1938 place_id: impl Into<String>,
1939 ) -> &mut Self {
1940 self.arcs
1941 .push(Arc::transition_to_place(transition_id, place_id));
1942 self
1943 }
1944
1945 /// Add a weighted place-to-transition arc.
1946 pub fn p_to_t_w(
1947 &mut self,
1948 place_id: impl Into<String>,
1949 transition_id: impl Into<String>,
1950 weight: u32,
1951 ) -> &mut Self {
1952 self.arcs
1953 .push(Arc::place_to_transition(place_id, transition_id).with_weight(weight));
1954 self
1955 }
1956
1957 /// Add a weighted transition-to-place arc.
1958 pub fn t_to_p_w(
1959 &mut self,
1960 transition_id: impl Into<String>,
1961 place_id: impl Into<String>,
1962 weight: u32,
1963 ) -> &mut Self {
1964 self.arcs
1965 .push(Arc::transition_to_place(transition_id, place_id).with_weight(weight));
1966 self
1967 }
1968
1969 /// Set initial token counts for places.
1970 ///
1971 /// Each item is a `(place_id, token_count)` pair. A workflow net (WF-net)
1972 /// typically has exactly one token in the source place.
1973 pub fn initial_tokens<I, S>(&mut self, tokens: I) -> &mut Self
1974 where
1975 I: IntoIterator<Item = (S, u32)>,
1976 S: Into<String>,
1977 {
1978 self.initial_tokens
1979 .extend(tokens.into_iter().map(|(p, c)| (p.into(), c)));
1980 self
1981 }
1982
1983 /// Build the [`PetriNet`], validating structural shape.
1984 ///
1985 /// Returns [`PetriRefusal`] if any arc references an undeclared place or
1986 /// transition.
1987 pub fn build(self) -> Result<PetriNet, PetriRefusal> {
1988 let marking = Marking::new(self.initial_tokens);
1989 let net = PetriNet::new(self.places, self.transitions, self.arcs, marking);
1990 net.validate()?;
1991 Ok(net)
1992 }
1993
1994 /// Build without validation — for cases where the caller guarantees structure.
1995 ///
1996 /// Prefer [`build`](Self::build) for externally-supplied data.
1997 pub fn build_unchecked(self) -> PetriNet {
1998 let marking = Marking::new(self.initial_tokens);
1999 PetriNet::new(self.places, self.transitions, self.arcs, marking)
2000 }
2001
2002 /// Build a workflow net (WF-net) with a declared final marking.
2003 ///
2004 /// Structural shape is validated before returning.
2005 pub fn build_wf(self, final_marking: Marking) -> Result<WfNet<SoundnessUnknown>, PetriRefusal> {
2006 let net = self.build()?;
2007 Ok(WfNet::new(net, final_marking))
2008 }
2009}