wasm4pm_compat/nightly_foundry.rs
1//! Nightly foundry — zero-cost type-law surfaces derived from process-mining papers.
2//!
3//! Always compiled (the crate is nightly-only). No cfg gate, no `RUSTFLAGS`.
4//! This is an experimental staging area; the main type law lives in
5//! [`crate::law`], [`crate::petri`], [`crate::conformance`], [`crate::process_tree`],
6//! [`crate::powl`], [`crate::formats`], and [`crate::strict`].
7//!
8//! ## Four surfaces, four nightly features, four paper mappings
9//!
10//! | Surface | Feature | Paper |
11//! |---------|---------|-------|
12//! | [`petri_law`] | `generic_const_exprs` | Murata (1989) §2 incidence matrices W⁻, W⁺ |
13//! | [`powl_law`] | `adt_const_params` | Kourani (2505.07052) §3 POWL fragment kinds |
14//! | [`evidence_law`] | `min_specialization` | Blue River Dam — admitted vs raw label |
15//! | [`token_law`] | `portable_simd` | Murata §2 enabling condition `∀p: M[p] ≥ W⁻[p][t]` |
16//!
17//! ## Zero-cost guarantee
18//!
19//! Every type in this module is `#[repr(transparent)]` over a fixed-size array
20//! or a `u32`, or is a zero-sized marker. There is no heap allocation, no
21//! runtime dispatch, and no branch in the hot path. The nightly features move
22//! paper-derived invariants into the *type system*, not into runtime machinery.
23//!
24//! [`petri_law`]: crate::nightly_foundry::petri_law
25//! [`powl_law`]: crate::nightly_foundry::powl_law
26//! [`evidence_law`]: crate::nightly_foundry::evidence_law
27//! [`token_law`]: crate::nightly_foundry::token_law
28
29// ─────────────────────────────────────────────────────────────────────────────
30// Surface 1: Bipartite Petri-net arc matrices (generic_const_exprs)
31// Paper: Murata (1989) IEEE Proc. 77(4) "Petri Nets: Properties, Analysis …"
32// §2: N = (P, T, F) is bipartite; arcs in F ⊆ (P×T) ∪ (T×P).
33// W⁻: P×T→ℕ pre-incidence, W⁺: T×P→ℕ post-incidence.
34// Enabling: ∀p: M[p] ≥ W⁻(p,t). Firing: M'[p] = M[p]−W⁻(p,t)+W⁺(t,p).
35//
36// `generic_const_exprs` lets us write `[u8; P * T]` as a struct field —
37// the flat arc matrix is zero-cost and bipartite-direction-safe at the type level:
38// PreMatrix<P, T> ≠ PostMatrix<P, T> (same count, opposite semantics).
39// ─────────────────────────────────────────────────────────────────────────────
40
41/// **Compile-pass law**: `Marking<P>::EMPTY` is a const-generic compile-time constant.
42///
43/// ```
44/// use wasm4pm_compat::nightly_foundry::petri_law::Marking;
45/// const M0: Marking<3> = Marking::EMPTY;
46/// assert_eq!(M0.total_tokens(), 0);
47/// let m1 = Marking([1u32, 2u32, 0u32]);
48/// assert_eq!(m1.total_tokens(), 3);
49/// ```
50///
51/// **Compile-pass law**: pre-matrix enabling check and firing are sound.
52///
53/// ```
54/// use wasm4pm_compat::nightly_foundry::petri_law::{Marking, PreMatrix, PostMatrix};
55/// // 2 places, 1 transition. p0 → t0 → p1.
56/// let mut pre = PreMatrix::<2, 1>::ZERO;
57/// pre.weights[0] = 1; // W⁻(p0,t0) = 1
58/// let mut post = PostMatrix::<2, 1>::ZERO;
59/// post.weights[1] = 1; // W⁺(t0,p1) = 1
60/// let m = Marking([1u32, 0u32]);
61/// assert!(pre.is_enabled(0, &m));
62/// let m2 = post.fire(0, m, &pre);
63/// assert_eq!(m2, Marking([0u32, 1u32]));
64/// ```
65pub mod petri_law {
66 /// Token marking of exactly `P` places — M: P → ℕ.
67 ///
68 /// Paper: Murata (1989) §2 Def. 2 — M₀ ∈ ℕᴾ.
69 /// Zero-cost: `#[repr(transparent)]` over `[u32; P]`.
70 #[repr(transparent)]
71 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
72 pub struct Marking<const P: usize>(pub [u32; P]);
73
74 impl<const P: usize> Marking<P> {
75 /// Zero marking: no tokens anywhere.
76 pub const EMPTY: Self = Self([0u32; P]);
77
78 /// Total token count across all places.
79 #[inline]
80 pub fn total_tokens(&self) -> u32 {
81 let mut s = 0u32;
82 let mut i = 0;
83 while i < P {
84 s += self.0[i];
85 i += 1;
86 }
87 s
88 }
89
90 /// Token count at place `p`. Returns `None` if `p >= P`.
91 #[must_use]
92 #[inline]
93 pub fn at(&self, p: usize) -> Option<u32> {
94 self.0.get(p).copied()
95 }
96 }
97
98 impl<const P: usize> Default for Marking<P> {
99 fn default() -> Self {
100 Self::EMPTY
101 }
102 }
103
104 /// Pre-incidence matrix W⁻: P×T→ℕ, stored flat row-major as `[u8; P * T]`.
105 ///
106 /// Paper: Murata (1989) §2 — W⁻(p,t) = arc weight from place p to transition t.
107 /// Enabling condition: `∀p: M[p] ≥ W⁻(p,t)`.
108 ///
109 /// **Requires `generic_const_exprs`**: `P * T` is a const expression in a
110 /// where-bound and in the array-length field. Zero-cost flat array, no heap.
111 pub struct PreMatrix<const P: usize, const T: usize>
112 where
113 [(); P * T]: Sized,
114 {
115 /// Row-major weights; index `p * T + t`.
116 pub weights: [u8; P * T],
117 }
118
119 impl<const P: usize, const T: usize> PreMatrix<P, T>
120 where
121 [(); P * T]: Sized,
122 {
123 /// Zero arc-weight matrix.
124 pub const ZERO: Self = Self {
125 weights: [0u8; P * T],
126 };
127
128 /// Arc weight W⁻(p, t).
129 #[inline]
130 pub fn w(&self, p: usize, t: usize) -> u8 {
131 self.weights[p * T + t]
132 }
133
134 /// Is transition `t` enabled in marking `m`?
135 ///
136 /// Paper: Murata §2 Rule 1 — t is enabled iff `∀p: M[p] ≥ W⁻(p,t)`.
137 #[inline]
138 pub fn is_enabled(&self, t: usize, m: &Marking<P>) -> bool {
139 (0..P).all(|p| m.0[p] >= self.weights[p * T + t] as u32)
140 }
141 }
142
143 impl<const P: usize, const T: usize> Default for PreMatrix<P, T>
144 where
145 [(); P * T]: Sized,
146 {
147 fn default() -> Self {
148 Self::ZERO
149 }
150 }
151
152 /// Post-incidence matrix W⁺: T×P→ℕ, stored flat row-major as `[u8; T * P]`.
153 ///
154 /// Paper: Murata §2 — W⁺(t,p) = arc weight from transition t to place p.
155 ///
156 /// Note: `PostMatrix<P,T>` and `PreMatrix<P,T>` are **distinct types** even
157 /// though `P*T == T*P` arithmetically. The bipartite direction is in the type.
158 pub struct PostMatrix<const P: usize, const T: usize>
159 where
160 [(); T * P]: Sized,
161 {
162 /// Row-major weights; index `t * P + p`.
163 pub weights: [u8; T * P],
164 }
165
166 impl<const P: usize, const T: usize> PostMatrix<P, T>
167 where
168 [(); T * P]: Sized,
169 {
170 /// Zero arc-weight matrix.
171 pub const ZERO: Self = Self {
172 weights: [0u8; T * P],
173 };
174
175 /// Arc weight W⁺(t, p).
176 #[inline]
177 pub fn w(&self, t: usize, p: usize) -> u8 {
178 self.weights[t * P + p]
179 }
180
181 /// Fire transition `t` on `m`, returning the new marking.
182 ///
183 /// Paper: Murata §2 Rule 2 — `M'[p] = M[p] − W⁻(p,t) + W⁺(t,p)`.
184 /// **Caller must first verify `pre.is_enabled(t, &m)`.**
185 #[inline]
186 pub fn fire(&self, t: usize, m: Marking<P>, pre: &PreMatrix<P, T>) -> Marking<P>
187 where
188 [(); P * T]: Sized,
189 {
190 let mut next = m;
191 let mut p = 0;
192 while p < P {
193 next.0[p] =
194 next.0[p] - pre.weights[p * T + t] as u32 + self.weights[t * P + p] as u32;
195 p += 1;
196 }
197 next
198 }
199 }
200
201 impl<const P: usize, const T: usize> Default for PostMatrix<P, T>
202 where
203 [(); T * P]: Sized,
204 {
205 fn default() -> Self {
206 Self::ZERO
207 }
208 }
209}
210
211// ─────────────────────────────────────────────────────────────────────────────
212// Surface 2: Typed POWL nodes (adt_const_params)
213// Paper: Kourani (arXiv:2505.07052) §3 — POWL recursive grammar:
214// POWL ::= A | X(M₁,M₂) | L(M₁,M₂) | P(M⁺, ≺) | τ
215//
216// `adt_const_params` + `ConstParamTy` let an enum variant become a const
217// generic: `TypedNode<{ PowlKind::Atom }>` vs `TypedNode<{ PowlKind::Partial }>`.
218// The compiler rejects calling atom-only APIs on a partial-order node.
219// Zero-cost: KIND is fully erased at runtime; the struct is just a `u32` id.
220// ─────────────────────────────────────────────────────────────────────────────
221
222/// **Compile-fail law**: an `Atom` node must NOT expose the partial-order API.
223///
224/// The `compile_fail` annotation verifies that the type system refuses the call.
225/// This module is always compiled (the crate is nightly-only; no cfg gate).
226///
227/// ```compile_fail
228/// use wasm4pm_compat::nightly_foundry::powl_law::TypedNode;
229/// let atom = TypedNode::atom(1u32);
230/// // E0599: no method `are_concurrent` found for `TypedNode<{PowlKind::Atom}>`
231/// let _ = atom.are_concurrent(&[], 1, 2);
232/// ```
233///
234/// **Compile-fail law**: `Atom` and `Silent` are distinct types; assignment must fail.
235///
236/// ```compile_fail
237/// use wasm4pm_compat::nightly_foundry::powl_law::{TypedNode, PowlKind};
238/// // E0308: mismatched types — `TypedNode<{Atom}>` ≠ `TypedNode<{Silent}>`
239/// let _: TypedNode<{ PowlKind::Silent }> = TypedNode::atom(0u32);
240/// ```
241///
242/// **Compile-pass law**: a well-formed atom node is admitted.
243///
244/// ```
245/// use wasm4pm_compat::nightly_foundry::powl_law::TypedNode;
246/// let a = TypedNode::atom(42u32);
247/// assert!(a.is_observable());
248/// assert_eq!(a.id(), 42);
249/// ```
250pub mod powl_law {
251 use core::marker::ConstParamTy;
252
253 /// POWL fragment kind — used as a const generic parameter.
254 ///
255 /// Paper: Kourani (2505.07052) §3.
256 #[derive(Debug, Clone, Copy, PartialEq, Eq, ConstParamTy)]
257 pub enum PowlKind {
258 /// Atom: single activity node (leaf), observable.
259 Atom,
260 /// Exclusive choice (xor): exactly one branch fires.
261 Xor,
262 /// Loop: `do`-body with optional `redo`.
263 Loop,
264 /// Partial order: DAG of children with precedence edges.
265 Partial,
266 /// Silent step: tau, no observable activity.
267 Silent,
268 }
269
270 /// A POWL node with its fragment kind encoded at the type level.
271 ///
272 /// `KIND` is erased at runtime — the value is just a `u32` id.
273 /// Fragment-specific APIs are only available on the correct variant.
274 #[repr(transparent)]
275 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
276 pub struct TypedNode<const KIND: PowlKind>(pub u32);
277
278 // ── Atom ──────────────────────────────────────────────────────────────────
279 impl TypedNode<{ PowlKind::Atom }> {
280 #[inline]
281 pub const fn atom(id: u32) -> Self {
282 Self(id)
283 }
284 /// Atoms are always observable (carry an activity label).
285 #[inline]
286 pub const fn is_observable(&self) -> bool {
287 true
288 }
289 }
290
291 // ── Silent ────────────────────────────────────────────────────────────────
292 impl TypedNode<{ PowlKind::Silent }> {
293 #[inline]
294 pub const fn silent(id: u32) -> Self {
295 Self(id)
296 }
297 /// Silent steps are never observable.
298 #[inline]
299 pub const fn is_observable(&self) -> bool {
300 false
301 }
302 }
303
304 // ── Partial order ─────────────────────────────────────────────────────────
305
306 /// Precedence edge a ≺ b within a POWL partial-order node.
307 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
308 pub struct OrderEdge {
309 pub before: u32,
310 pub after: u32,
311 }
312
313 impl TypedNode<{ PowlKind::Partial }> {
314 #[inline]
315 pub const fn partial(id: u32) -> Self {
316 Self(id)
317 }
318
319 /// Are `a` and `b` concurrent (neither precedes the other)?
320 /// Paper: Kourani §3 — concurrency = absence of precedence in both directions.
321 #[inline]
322 pub fn are_concurrent(&self, edges: &[OrderEdge], a: u32, b: u32) -> bool {
323 let ab = edges.iter().any(|e| e.before == a && e.after == b);
324 let ba = edges.iter().any(|e| e.before == b && e.after == a);
325 !ab && !ba
326 }
327 }
328
329 // ── Xor ───────────────────────────────────────────────────────────────────
330 impl TypedNode<{ PowlKind::Xor }> {
331 #[inline]
332 pub const fn xor(id: u32) -> Self {
333 Self(id)
334 }
335 /// Minimum branch count for a well-formed choice node (≥ 2).
336 #[inline]
337 pub const fn min_branches() -> usize {
338 2
339 }
340 }
341
342 // ── Loop ──────────────────────────────────────────────────────────────────
343 impl TypedNode<{ PowlKind::Loop }> {
344 #[inline]
345 pub const fn loop_node(id: u32) -> Self {
346 Self(id)
347 }
348 }
349
350 // ── Universal id accessor (macro avoids repeated `where` complexity) ──────
351 macro_rules! impl_id {
352 ($kind:expr) => {
353 impl TypedNode<{ $kind }> {
354 #[inline]
355 pub const fn id(&self) -> u32 {
356 self.0
357 }
358 }
359 };
360 }
361 impl_id!(PowlKind::Atom);
362 impl_id!(PowlKind::Silent);
363 impl_id!(PowlKind::Partial);
364 impl_id!(PowlKind::Xor);
365 impl_id!(PowlKind::Loop);
366}
367
368// ─────────────────────────────────────────────────────────────────────────────
369// Surface 3: Evidence-kind label via specialization (min_specialization)
370// Doctrine: Blue River Dam — `Admitted` and `Raw` are first-class, distinct states.
371//
372// The blanket impl gives every T the label "raw".
373// The specialised impl overrides that for T: AdmittedMarker to "admitted".
374// Resolution is at compile time: no vtable, no branch, no heap.
375// ─────────────────────────────────────────────────────────────────────────────
376
377pub mod evidence_law {
378 /// Compile-time evidence-kind label — `"raw"` or `"admitted"`.
379 ///
380 /// Uses `min_specialization` to override the blanket `"raw"` impl
381 /// with `"admitted"` for any `Admitted<T>` wrapper — resolved at compile
382 /// time with no vtable and no branch.
383 pub trait EvidenceKind {
384 fn kind_label(&self) -> &'static str;
385 }
386
387 // Blanket: every T that is not Admitted<_> is "raw".
388 impl<T> EvidenceKind for T {
389 default fn kind_label(&self) -> &'static str {
390 "raw"
391 }
392 }
393
394 /// Newtype wrapper that marks a value as having crossed a named boundary.
395 /// Zero-cost: `#[repr(transparent)]` — same ABI as `T`.
396 #[repr(transparent)]
397 pub struct Admitted<T>(pub T);
398
399 // Specialization on the concrete type constructor `Admitted<T>`.
400 // `min_specialization` allows this because `Admitted<T>` is strictly
401 // more specific than the blanket `T` — it narrows on the type constructor,
402 // not on an arbitrary trait bound.
403 impl<T> EvidenceKind for Admitted<T> {
404 fn kind_label(&self) -> &'static str {
405 "admitted"
406 }
407 }
408}
409
410// ─────────────────────────────────────────────────────────────────────────────
411// Surface 4: SIMD token-enabling check (portable_simd)
412// Paper: Murata (1989) §2 Rule 1 — t is enabled iff ∀p: M[p] ≥ W⁻(p,t).
413//
414// With portable_simd we check 4 or 8 places at once via u32x4 / u32x8.
415// For small Petri subnets this is the entire enabling condition in one
416// SIMD lane comparison + mask reduction — zero branches, no heap.
417// ─────────────────────────────────────────────────────────────────────────────
418
419pub mod token_law {
420 use core::simd::{cmp::SimdPartialOrd, u32x4, u32x8};
421
422 /// Check enabling for a 4-place subnet — single SIMD vector comparison.
423 ///
424 /// Returns `true` iff ∀p ∈ {0..4}: `marking[p] >= pre_weights[p]`.
425 #[inline]
426 pub fn transition_enabled_4(marking: [u32; 4], pre_weights: [u32; 4]) -> bool {
427 u32x4::from_array(marking)
428 .simd_ge(u32x4::from_array(pre_weights))
429 .all()
430 }
431
432 /// Check enabling for an 8-place subnet.
433 #[inline]
434 pub fn transition_enabled_8(marking: [u32; 8], pre_weights: [u32; 8]) -> bool {
435 u32x8::from_array(marking)
436 .simd_ge(u32x8::from_array(pre_weights))
437 .all()
438 }
439
440 /// Fire a transition on a 4-place marking via SIMD arithmetic.
441 ///
442 /// Paper: Murata §2 Rule 2 — `M'[p] = M[p] − W⁻[p] + W⁺[p]`.
443 /// **Requires `transition_enabled_4` was true.** No runtime check.
444 #[inline]
445 pub fn fire_4(marking: [u32; 4], pre: [u32; 4], post: [u32; 4]) -> [u32; 4] {
446 (u32x4::from_array(marking) - u32x4::from_array(pre) + u32x4::from_array(post)).to_array()
447 }
448}
449
450// ─────────────────────────────────────────────────────────────────────────────
451// Tests — always compiled (nightly-only crate, no cfg gate required)
452// ─────────────────────────────────────────────────────────────────────────────
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 // petri_law ────────────────────────────────────────────────────────────────
459
460 #[test]
461 fn marking_empty_is_zero() {
462 assert_eq!(petri_law::Marking::<4>::EMPTY.total_tokens(), 0);
463 }
464
465 #[test]
466 fn pre_matrix_enables_and_blocks() {
467 // 2 places, 2 transitions.
468 // W⁻(p0,t0)=1, W⁻(p1,t1)=1; all others zero.
469 let mut pre = petri_law::PreMatrix::<2, 2>::ZERO;
470 pre.weights[0] = 1; // W⁻(p0,t0): p=0,t=0 → index p*T+t = 0*2+0 = 0
471 pre.weights[3] = 1; // W⁻(p1,t1): p=1,t=1 → index p*T+t = 1*2+1 = 3
472
473 let m = petri_law::Marking([1u32, 0u32]);
474 assert!(pre.is_enabled(0, &m)); // t0 enabled: M[p0]=1 ≥ 1
475 assert!(!pre.is_enabled(1, &m)); // t1 blocked: M[p1]=0 < 1
476 }
477
478 #[test]
479 fn firing_token_moves_correctly() {
480 // 2 places, 1 transition. p0 → t0 → p1.
481 let mut pre = petri_law::PreMatrix::<2, 1>::ZERO;
482 pre.weights[0] = 1; // W⁻(p0,t0) = 1
483 let mut post = petri_law::PostMatrix::<2, 1>::ZERO;
484 post.weights[1] = 1; // W⁺(t0,p1) = 1
485
486 let m = petri_law::Marking([1u32, 0u32]);
487 assert!(pre.is_enabled(0, &m));
488 let m2 = post.fire(0, m, &pre);
489 assert_eq!(m2, petri_law::Marking([0u32, 1u32]));
490 }
491
492 // powl_law ────────────────────────────────────────────────────────────────
493
494 #[test]
495 fn atom_observable_silent_not() {
496 assert!(powl_law::TypedNode::atom(1).is_observable());
497 assert!(!powl_law::TypedNode::silent(2).is_observable());
498 }
499
500 #[test]
501 fn partial_concurrency_correct() {
502 let p = powl_law::TypedNode::partial(0);
503 let edges = [powl_law::OrderEdge {
504 before: 1,
505 after: 2,
506 }];
507 assert!(!p.are_concurrent(&edges, 1, 2)); // 1 ≺ 2: not concurrent
508 assert!(p.are_concurrent(&edges, 1, 3)); // no edge: concurrent
509 }
510
511 #[test]
512 fn xor_min_branches_is_two() {
513 assert_eq!(
514 powl_law::TypedNode::<{ powl_law::PowlKind::Xor }>::min_branches(),
515 2
516 );
517 }
518
519 // evidence_law ────────────────────────────────────────────────────────────
520
521 #[test]
522 fn raw_u32_labels_raw() {
523 use evidence_law::EvidenceKind;
524 assert_eq!(42u32.kind_label(), "raw");
525 }
526
527 #[test]
528 fn admitted_wrapper_labels_admitted() {
529 use evidence_law::{Admitted, EvidenceKind};
530 assert_eq!(Admitted(42u32).kind_label(), "admitted");
531 }
532
533 // token_law ───────────────────────────────────────────────────────────────
534
535 #[test]
536 fn simd_enabled_all_met() {
537 assert!(token_law::transition_enabled_4([5, 3, 1, 0], [1, 1, 1, 0]));
538 }
539
540 #[test]
541 fn simd_enabled_one_unmet() {
542 assert!(!token_law::transition_enabled_4([5, 0, 1, 0], [1, 1, 1, 0]));
543 }
544
545 #[test]
546 fn simd_fire_moves_tokens() {
547 let m = token_law::fire_4([2, 0, 0, 0], [1, 0, 0, 0], [0, 1, 0, 0]);
548 assert_eq!(m, [1, 1, 0, 0]);
549 }
550
551 #[test]
552 fn simd_enabled_8_all_met() {
553 assert!(token_law::transition_enabled_8(
554 [9, 8, 7, 6, 5, 4, 3, 2],
555 [1, 1, 1, 1, 1, 1, 1, 1],
556 ));
557 }
558}