Skip to content

Commit fd93e5c

Browse files
committed
fix(introspection): derive graph from compiled transitions
1 parent 738a557 commit fd93e5c

File tree

118 files changed

+878
-360
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+878
-360
lines changed

macro_registry/src/analysis.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,12 @@ fn attribute_names(attrs: &[syn::Attribute]) -> Vec<String> {
120120
names
121121
}
122122

123-
fn find_item_line_from(contents: &str, kind: &str, item_name: &str, start_line: usize) -> Option<usize> {
123+
fn find_item_line_from(
124+
contents: &str,
125+
kind: &str,
126+
item_name: &str,
127+
start_line: usize,
128+
) -> Option<usize> {
124129
for (idx, line) in contents
125130
.lines()
126131
.enumerate()

statum-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[dependencies]
2+
linkme = "0.3"
23

34
[package]
45
authors = ["Eran Boodnero <eran@eran.codes>"]

statum-core/src/introspection.rs

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,50 @@ pub trait MachineIntrospection {
1010
const GRAPH: &'static MachineGraph<Self::StateId, Self::TransitionId>;
1111
}
1212

13+
/// Runtime accessor for transition descriptors that may be supplied by a
14+
/// distributed registration surface.
15+
#[derive(Clone, Copy)]
16+
pub struct TransitionInventory<S: 'static, T: 'static> {
17+
get: fn() -> &'static [TransitionDescriptor<S, T>],
18+
}
19+
20+
impl<S, T> TransitionInventory<S, T> {
21+
/// Creates a transition inventory from a `'static` getter.
22+
pub const fn new(get: fn() -> &'static [TransitionDescriptor<S, T>]) -> Self {
23+
Self { get }
24+
}
25+
26+
/// Returns the transition descriptors as a slice.
27+
pub fn as_slice(&self) -> &'static [TransitionDescriptor<S, T>] {
28+
(self.get)()
29+
}
30+
}
31+
32+
impl<S, T> core::ops::Deref for TransitionInventory<S, T> {
33+
type Target = [TransitionDescriptor<S, T>];
34+
35+
fn deref(&self) -> &Self::Target {
36+
self.as_slice()
37+
}
38+
}
39+
40+
impl<S, T> core::fmt::Debug for TransitionInventory<S, T> {
41+
fn fmt(
42+
&self,
43+
formatter: &mut core::fmt::Formatter<'_>,
44+
) -> core::result::Result<(), core::fmt::Error> {
45+
formatter.debug_tuple("TransitionInventory").finish()
46+
}
47+
}
48+
49+
impl<S, T> core::cmp::PartialEq for TransitionInventory<S, T> {
50+
fn eq(&self, other: &Self) -> bool {
51+
core::ptr::eq(self.as_slice(), other.as_slice())
52+
}
53+
}
54+
55+
impl<S, T> core::cmp::Eq for TransitionInventory<S, T> {}
56+
1357
/// Identity for one concrete machine state.
1458
pub trait MachineStateIdentity: MachineIntrospection {
1559
/// The state id for this concrete machine instantiation.
@@ -204,7 +248,7 @@ pub struct MachineGraph<S: 'static, T: 'static> {
204248
/// All states known to the machine.
205249
pub states: &'static [StateDescriptor<S>],
206250
/// All transition sites known to the machine.
207-
pub transitions: &'static [TransitionDescriptor<S, T>],
251+
pub transitions: TransitionInventory<S, T>,
208252
}
209253

210254
impl<S, T> MachineGraph<S, T>
@@ -300,7 +344,7 @@ mod tests {
300344
MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation,
301345
MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder,
302346
RecordedTransition, StateDescriptor, StatePresentation, TransitionDescriptor,
303-
TransitionPresentation,
347+
TransitionInventory, TransitionPresentation,
304348
};
305349
use core::marker::PhantomData;
306350

@@ -311,14 +355,47 @@ mod tests {
311355
Published,
312356
}
313357

314-
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
315-
enum TransitionId {
316-
SubmitFromDraft,
317-
PublishFromReview,
358+
#[derive(Clone, Copy)]
359+
struct TransitionId(&'static crate::__private::TransitionToken);
360+
361+
impl TransitionId {
362+
const fn from_token(token: &'static crate::__private::TransitionToken) -> Self {
363+
Self(token)
364+
}
365+
}
366+
367+
impl core::fmt::Debug for TransitionId {
368+
fn fmt(
369+
&self,
370+
formatter: &mut core::fmt::Formatter<'_>,
371+
) -> core::result::Result<(), core::fmt::Error> {
372+
formatter.write_str("TransitionId(..)")
373+
}
374+
}
375+
376+
impl core::cmp::PartialEq for TransitionId {
377+
fn eq(&self, other: &Self) -> bool {
378+
core::ptr::eq(self.0, other.0)
379+
}
380+
}
381+
382+
impl core::cmp::Eq for TransitionId {}
383+
384+
impl core::hash::Hash for TransitionId {
385+
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
386+
let ptr = core::ptr::from_ref(self.0) as usize;
387+
<usize as core::hash::Hash>::hash(&ptr, state);
388+
}
318389
}
319390

320391
static REVIEW_TARGETS: [StateId; 1] = [StateId::Review];
321392
static PUBLISH_TARGETS: [StateId; 1] = [StateId::Published];
393+
static SUBMIT_FROM_DRAFT_TOKEN: crate::__private::TransitionToken =
394+
crate::__private::TransitionToken::new();
395+
static PUBLISH_FROM_REVIEW_TOKEN: crate::__private::TransitionToken =
396+
crate::__private::TransitionToken::new();
397+
const SUBMIT_FROM_DRAFT: TransitionId = TransitionId::from_token(&SUBMIT_FROM_DRAFT_TOKEN);
398+
const PUBLISH_FROM_REVIEW: TransitionId = TransitionId::from_token(&PUBLISH_FROM_REVIEW_TOKEN);
322399
static STATES: [StateDescriptor<StateId>; 3] = [
323400
StateDescriptor {
324401
id: StateId::Draft,
@@ -338,13 +415,13 @@ mod tests {
338415
];
339416
static TRANSITIONS: [TransitionDescriptor<StateId, TransitionId>; 2] = [
340417
TransitionDescriptor {
341-
id: TransitionId::SubmitFromDraft,
418+
id: SUBMIT_FROM_DRAFT,
342419
method_name: "submit",
343420
from: StateId::Draft,
344421
to: &REVIEW_TARGETS,
345422
},
346423
TransitionDescriptor {
347-
id: TransitionId::PublishFromReview,
424+
id: PUBLISH_FROM_REVIEW,
348425
method_name: "publish",
349426
from: StateId::Review,
350427
to: &PUBLISH_TARGETS,
@@ -425,7 +502,7 @@ mod tests {
425502
],
426503
transitions: &[
427504
TransitionPresentation {
428-
id: TransitionId::SubmitFromDraft,
505+
id: SUBMIT_FROM_DRAFT,
429506
label: Some("Submit"),
430507
description: Some("Move work into review."),
431508
metadata: TransitionMeta {
@@ -434,7 +511,7 @@ mod tests {
434511
},
435512
},
436513
TransitionPresentation {
437-
id: TransitionId::PublishFromReview,
514+
id: PUBLISH_FROM_REVIEW,
438515
label: Some("Publish"),
439516
description: Some("Complete the workflow."),
440517
metadata: TransitionMeta {
@@ -455,7 +532,7 @@ mod tests {
455532
rust_type_path: "workflow::Machine",
456533
},
457534
states: &STATES,
458-
transitions: &TRANSITIONS,
535+
transitions: TransitionInventory::new(|| &TRANSITIONS),
459536
};
460537
}
461538

@@ -479,7 +556,7 @@ mod tests {
479556
rust_type_path: "workflow::Machine",
480557
},
481558
states: &STATES,
482-
transitions: &TRANSITIONS,
559+
transitions: TransitionInventory::new(|| &TRANSITIONS),
483560
};
484561

485562
assert_eq!(
@@ -488,18 +565,18 @@ mod tests {
488565
);
489566
assert_eq!(
490567
graph
491-
.transition(TransitionId::PublishFromReview)
568+
.transition(PUBLISH_FROM_REVIEW)
492569
.map(|transition| transition.method_name),
493570
Some("publish")
494571
);
495572
assert_eq!(
496573
graph
497574
.transition_from_method(StateId::Draft, "submit")
498575
.map(|transition| transition.id),
499-
Some(TransitionId::SubmitFromDraft)
576+
Some(SUBMIT_FROM_DRAFT)
500577
);
501578
assert_eq!(
502-
graph.legal_targets(TransitionId::SubmitFromDraft),
579+
graph.legal_targets(SUBMIT_FROM_DRAFT),
503580
Some(REVIEW_TARGETS.as_slice())
504581
);
505582
assert_eq!(graph.transitions_from(StateId::Draft).count(), 1);
@@ -509,7 +586,7 @@ mod tests {
509586
#[test]
510587
fn runtime_transition_recording_joins_back_to_static_graph() {
511588
let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
512-
TransitionId::SubmitFromDraft,
589+
SUBMIT_FROM_DRAFT,
513590
)
514591
.expect("valid runtime transition");
515592

@@ -521,7 +598,7 @@ mod tests {
521598
rust_type_path: "workflow::Machine",
522599
},
523600
StateId::Draft,
524-
TransitionId::SubmitFromDraft,
601+
SUBMIT_FROM_DRAFT,
525602
StateId::Review,
526603
)
527604
);
@@ -544,13 +621,13 @@ mod tests {
544621
#[test]
545622
fn runtime_transition_recording_rejects_illegal_target_or_site() {
546623
assert!(Workflow::<DraftMarker>::try_record_transition(
547-
TransitionId::PublishFromReview,
624+
PUBLISH_FROM_REVIEW,
548625
StateId::Published,
549626
)
550627
.is_none());
551628
assert!(
552629
Workflow::<ReviewMarker>::try_record_transition_to::<Workflow<PublishedMarker>>(
553-
TransitionId::SubmitFromDraft,
630+
SUBMIT_FROM_DRAFT,
554631
)
555632
.is_none()
556633
);
@@ -559,7 +636,7 @@ mod tests {
559636
#[test]
560637
fn presentation_queries_join_with_runtime_transitions() {
561638
let event = Workflow::<DraftMarker>::try_record_transition_to::<Workflow<ReviewMarker>>(
562-
TransitionId::SubmitFromDraft,
639+
SUBMIT_FROM_DRAFT,
563640
)
564641
.expect("valid runtime transition");
565642

@@ -576,7 +653,7 @@ mod tests {
576653
assert_eq!(
577654
PRESENTATION.transition(event.transition),
578655
Some(&TransitionPresentation {
579-
id: TransitionId::SubmitFromDraft,
656+
id: SUBMIT_FROM_DRAFT,
580657
label: Some("Submit"),
581658
description: Some("Move work into review."),
582659
metadata: TransitionMeta {

statum-core/src/lib.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,27 @@ mod introspection;
1212

1313
pub mod projection;
1414

15+
#[doc(hidden)]
16+
pub mod __private {
17+
pub use linkme;
18+
19+
#[derive(Debug)]
20+
pub struct TransitionToken {
21+
_private: u8,
22+
}
23+
24+
impl TransitionToken {
25+
pub const fn new() -> Self {
26+
Self { _private: 0 }
27+
}
28+
}
29+
}
30+
1531
pub use introspection::{
1632
MachineDescriptor, MachineGraph, MachineIntrospection, MachinePresentation,
1733
MachinePresentationDescriptor, MachineStateIdentity, MachineTransitionRecorder,
1834
RecordedTransition, StateDescriptor, StatePresentation, TransitionDescriptor,
19-
TransitionPresentation,
35+
TransitionInventory, TransitionPresentation,
2036
};
2137

2238
/// A generated state marker type.

statum-macros/src/lib.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ moddef::moddef!(
2727

2828
pub(crate) use syntax::{ItemTarget, ModulePath, extract_derives};
2929

30-
use crate::{MachinePath, ensure_machine_loaded_by_name};
30+
use crate::{MachinePath, ensure_machine_loaded_by_name, unique_loaded_machine_elsewhere};
3131
use macro_registry::callsite::current_module_path_opt;
3232
use proc_macro::TokenStream;
3333
use proc_macro2::Span;
@@ -123,7 +123,10 @@ pub fn transition(
123123
};
124124

125125
let machine_path: MachinePath = module_path.clone().into();
126-
let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name);
126+
// `include!` gives the transition macro the included file as its source context,
127+
// so exact module lookup can miss the already-loaded parent machine.
128+
let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name)
129+
.or_else(|| unique_loaded_machine_elsewhere(&tr_impl.machine_name));
127130
let machine_info = match machine_info_owned.as_ref() {
128131
Some(info) => info,
129132
None => {
@@ -141,7 +144,7 @@ pub fn transition(
141144
}
142145

143146
// -- Step 3: Generate new code
144-
let expanded = generate_transition_impl(&input, &tr_impl, machine_info, &module_path);
147+
let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
145148

146149
// Combine expanded code with the original `impl` if needed
147150
// or simply return the expanded code

0 commit comments

Comments
 (0)