#![allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct AutomatonId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ThreadStateId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct LeafId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct ClockId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ActionId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct LocalVarId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct LocalVarDecl {
pub width: u32,
pub signed: bool,
}
impl LocalVarDecl {
pub fn is_wide(&self) -> bool {
self.width > 64
}
pub fn limbs(&self) -> usize {
(self.width as usize).div_ceil(64).max(1)
}
pub fn slots(&self) -> usize {
if self.is_wide() { 2 * self.limbs() } else { 1 }
}
pub fn cast(&self, value: i64) -> i64 {
if self.width == 0 || self.width >= 64 {
return value;
}
let shift = 64 - self.width;
if self.signed {
(value << shift) >> shift
} else {
((value as u64) << shift >> shift) as i64
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Transition {
Delay { min: u32, max: Option<u32> },
Epsilon,
Abort { leaf: LeafId, mode: AbortMode },
Match { actions: Vec<ActionId> },
LocalVarWrite { var: LocalVarId, value: LeafId },
LocalVarCopy { dst: LocalVarId, src: LocalVarId },
FirstMatchBegin { group: FirstMatchId },
FirstMatchEnd { group: FirstMatchId },
LocalVarStep { var: LocalVarId, delta: i64 },
SubroutineCall { call: CallId, args: Vec<CallArg> },
EndPoint {
point: EndPointId,
flag: Option<LocalVarId>,
},
ClockSync { to: ClockId, strict: bool },
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct EndPointId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum EndMethod {
Triggered,
Matched,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EndPoint {
pub method: EndMethod,
pub site: u32,
pub automaton: Automaton,
pub flow_out: Vec<(LocalVarId, LocalVarId)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct CallId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum CallArg {
Value(LeafId),
Ref,
Const,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct FirstMatchId(pub u32);
impl Transition {
pub fn kind_name(&self) -> &'static str {
match self {
Transition::Delay { .. } => "Delay",
Transition::Epsilon => "Epsilon",
Transition::Abort { .. } => "Abort",
Transition::Match { .. } => "Match",
Transition::LocalVarWrite { .. } => "LocalVarWrite",
Transition::LocalVarCopy { .. } => "LocalVarCopy",
Transition::FirstMatchBegin { .. } => "FirstMatchBegin",
Transition::FirstMatchEnd { .. } => "FirstMatchEnd",
Transition::LocalVarStep { .. } => "LocalVarStep",
Transition::SubroutineCall { .. } => "SubroutineCall",
Transition::EndPoint { .. } => "EndPoint",
Transition::ClockSync { .. } => "ClockSync",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum AbortMode {
Accept,
Reject,
SyncAccept,
SyncReject,
}
impl AbortMode {
pub const COUNT: usize = 4;
pub const ALL: [AbortMode; Self::COUNT] = [
AbortMode::Accept,
AbortMode::Reject,
AbortMode::SyncAccept,
AbortMode::SyncReject,
];
pub fn is_sync(&self) -> bool {
matches!(self, AbortMode::SyncAccept | AbortMode::SyncReject)
}
pub fn accepts(&self) -> bool {
matches!(self, AbortMode::Accept | AbortMode::SyncAccept)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Edge {
pub guard: Option<LeafId>,
pub label: Transition,
pub target: ThreadStateId,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct State {
pub edges: Vec<Edge>,
}
impl State {
pub fn is_accepting(&self) -> bool {
self.edges
.iter()
.any(|e| matches!(e.label, Transition::Match { .. }))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Automaton {
pub states: Vec<State>,
pub start: ThreadStateId,
pub admits_empty: bool,
pub first_match_groups: u32,
pub local_vars: Vec<LocalVarDecl>,
pub end_points: Vec<EndPoint>,
pub entry_clock: ClockId,
}
impl Automaton {
pub fn empty() -> Self {
let mut a = Automaton {
states: Vec::new(),
start: ThreadStateId(0),
admits_empty: false,
first_match_groups: 0,
local_vars: Vec::new(),
end_points: Vec::new(),
entry_clock: ClockId(0),
};
let s = a.new_state();
a.start = s;
a
}
pub fn new_local_var(&mut self, width: u32, signed: bool) -> LocalVarId {
let id = LocalVarId(self.local_vars.len() as u32);
let decl = LocalVarDecl { width, signed };
self.local_vars.push(decl);
for _ in 1..decl.slots() {
self.local_vars.push(LocalVarDecl {
width: 64,
signed: false,
});
}
id
}
pub fn local_var(&self, id: LocalVarId) -> &LocalVarDecl {
&self.local_vars[id.0 as usize]
}
pub fn new_end_point(&mut self, point: EndPoint) -> EndPointId {
let id = EndPointId(self.end_points.len() as u32);
self.end_points.push(point);
id
}
pub fn end_point(&self, id: EndPointId) -> &EndPoint {
&self.end_points[id.0 as usize]
}
pub fn new_first_match_group(&mut self) -> FirstMatchId {
let id = FirstMatchId(self.first_match_groups);
self.first_match_groups += 1;
id
}
pub fn new_state(&mut self) -> ThreadStateId {
let id = ThreadStateId(self.states.len() as u32);
self.states.push(State::default());
id
}
pub fn add_edge(
&mut self,
from: ThreadStateId,
guard: Option<LeafId>,
label: Transition,
target: ThreadStateId,
) {
self.states[from.0 as usize].edges.push(Edge {
guard,
label,
target,
});
}
pub fn state(&self, id: ThreadStateId) -> &State {
&self.states[id.0 as usize]
}
pub fn max_span(&self) -> Option<u32> {
for (u, state) in self.states.iter().enumerate() {
for edge in &state.edges {
let Transition::Delay { min, max } = &edge.label else {
continue;
};
if (*max).unwrap_or(u32::MAX).max(*min) == 0 {
continue;
}
let mut seen = vec![false; self.states.len()];
let mut stack = vec![edge.target.0 as usize];
while let Some(s) = stack.pop() {
if s == u {
return None;
}
if std::mem::replace(&mut seen[s], true) {
continue;
}
stack.extend(self.states[s].edges.iter().map(|e| e.target.0 as usize));
}
}
}
let mut total: u32 = 0;
for edge in self.states.iter().flat_map(|s| &s.edges) {
match &edge.label {
Transition::Delay { min, max } => {
total = total.checked_add((*max)?.max(*min))?;
}
Transition::ClockSync { .. } => return None,
Transition::EndPoint { .. }
| Transition::Epsilon
| Transition::Abort { .. }
| Transition::Match { .. }
| Transition::LocalVarWrite { .. }
| Transition::LocalVarCopy { .. }
| Transition::FirstMatchBegin { .. }
| Transition::FirstMatchEnd { .. }
| Transition::LocalVarStep { .. }
| Transition::SubroutineCall { .. } => {}
}
}
Some(total)
}
pub fn end_point_lookback(&self) -> Vec<(ClockId, Option<u32>)> {
let mut out = Vec::new();
for ep in &self.end_points {
let inner = ep.automaton.end_point_lookback();
let deepest = inner.iter().try_fold(0u32, |m, (_, n)| n.map(|n| m.max(n)));
let span = ep
.automaton
.max_span()
.zip(deepest)
.and_then(|(own, d)| own.checked_add(d));
out.push((ep.automaton.entry_clock, span));
out.extend(inner);
}
out
}
pub fn has_calls(&self) -> bool {
self.states
.iter()
.flat_map(|s| &s.edges)
.any(|e| matches!(e.label, Transition::SubroutineCall { .. }))
}
pub fn end_points_have_calls(&self) -> bool {
self.end_points
.iter()
.any(|p| p.automaton.has_calls() || p.automaton.end_points_have_calls())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transition_kind_name_covers_every_variant() {
assert_eq!(Transition::Delay { min: 0, max: None }.kind_name(), "Delay");
assert_eq!(Transition::Epsilon.kind_name(), "Epsilon");
assert_eq!(
Transition::Abort {
leaf: LeafId(0),
mode: AbortMode::Accept
}
.kind_name(),
"Abort"
);
assert_eq!(Transition::Match { actions: vec![] }.kind_name(), "Match");
assert_eq!(
Transition::LocalVarWrite {
var: LocalVarId(0),
value: LeafId(0)
}
.kind_name(),
"LocalVarWrite"
);
assert_eq!(
Transition::FirstMatchBegin {
group: FirstMatchId(0)
}
.kind_name(),
"FirstMatchBegin"
);
assert_eq!(
Transition::FirstMatchEnd {
group: FirstMatchId(0)
}
.kind_name(),
"FirstMatchEnd"
);
assert_eq!(
Transition::ClockSync {
to: ClockId(1),
strict: true
}
.kind_name(),
"ClockSync"
);
}
#[test]
fn clock_ids_order_and_default_to_the_assertion_clock() {
assert!(ClockId(0) < ClockId(1));
assert_eq!(Automaton::empty().entry_clock, ClockId(0));
}
#[test]
fn match_item_edges_have_their_own_kind_names() {
assert_eq!(
Transition::LocalVarStep {
var: LocalVarId(0),
delta: 1
}
.kind_name(),
"LocalVarStep"
);
let call = Transition::SubroutineCall {
call: CallId(3),
args: vec![CallArg::Value(LeafId(1)), CallArg::Ref, CallArg::Const],
};
assert_eq!(call.kind_name(), "SubroutineCall");
assert_ne!(CallArg::Value(LeafId(0)), CallArg::Ref);
assert_ne!(CallArg::Const, CallArg::Ref);
assert!(CallId(1) < CallId(2));
}
#[test]
fn call_edges_are_found_on_the_automaton_and_in_its_end_points() {
let call = || Transition::SubroutineCall {
call: CallId(0),
args: vec![],
};
let mut plain = Automaton::empty();
let s1 = plain.new_state();
plain.add_edge(plain.start, None, Transition::Epsilon, s1);
assert!(!plain.has_calls() && !plain.end_points_have_calls());
let mut direct = plain.clone();
direct.add_edge(s1, None, call(), s1);
assert!(direct.has_calls() && !direct.end_points_have_calls());
let mut inner = plain.clone();
let s2 = inner.new_state();
inner.add_edge(s1, None, call(), s2);
let mut outer = plain.clone();
outer.new_end_point(EndPoint {
method: EndMethod::Triggered,
site: 0,
automaton: inner,
flow_out: Vec::new(),
});
assert!(!outer.has_calls());
assert!(outer.end_points_have_calls());
}
#[test]
fn end_points_are_minted_densely_and_have_a_kind_name() {
let mut a = Automaton::empty();
assert!(a.end_points.is_empty());
let sub = Automaton::empty();
let p0 = a.new_end_point(EndPoint {
method: EndMethod::Triggered,
site: 0,
automaton: sub.clone(),
flow_out: Vec::new(),
});
let p1 = a.new_end_point(EndPoint {
method: EndMethod::Matched,
site: 1,
automaton: sub,
flow_out: vec![(LocalVarId(0), LocalVarId(1))],
});
assert_eq!((p0, p1), (EndPointId(0), EndPointId(1)));
assert_eq!(a.end_point(p0).method, EndMethod::Triggered);
assert_eq!(a.end_point(p1).method, EndMethod::Matched);
assert_eq!(a.end_point(p1).flow_out, [(LocalVarId(0), LocalVarId(1))]);
assert!(p0 < p1);
let edge = Transition::EndPoint {
point: p0,
flag: Some(LocalVarId(2)),
};
assert_eq!(edge.kind_name(), "EndPoint");
assert_ne!(
edge,
Transition::EndPoint {
point: p0,
flag: None
}
);
}
#[test]
fn local_var_copy_has_its_own_kind_name() {
assert_eq!(
Transition::LocalVarCopy {
dst: LocalVarId(1),
src: LocalVarId(0)
}
.kind_name(),
"LocalVarCopy"
);
}
#[test]
fn local_vars_are_minted_densely_with_their_declared_type() {
let mut a = Automaton::empty();
assert!(a.local_vars.is_empty());
let x = a.new_local_var(32, true);
let y = a.new_local_var(4, false);
assert_eq!((x, y), (LocalVarId(0), LocalVarId(1)));
assert_eq!(
a.local_var(x),
&LocalVarDecl {
width: 32,
signed: true
}
);
assert_eq!(
a.local_var(y),
&LocalVarDecl {
width: 4,
signed: false
}
);
assert!(LocalVarId(0) < LocalVarId(1));
}
#[test]
fn local_var_decl_cast_truncates_then_extends() {
let u4 = LocalVarDecl {
width: 4,
signed: false,
};
assert_eq!(u4.cast(0x1f), 0xf, "unsigned keeps the low bits");
assert_eq!(u4.cast(-1), 0xf, "a negative value wraps");
let s4 = LocalVarDecl {
width: 4,
signed: true,
};
assert_eq!(s4.cast(0xf), -1, "bit 3 set sign-extends");
assert_eq!(s4.cast(0x7), 7);
assert_eq!(s4.cast(0x18), -8, "0x18 truncates to 0x8, which is -8");
let i32d = LocalVarDecl {
width: 32,
signed: true,
};
assert_eq!(i32d.cast(1 << 31), i64::from(i32::MIN));
assert_eq!(i32d.cast(-5), -5);
let u1 = LocalVarDecl {
width: 1,
signed: false,
};
assert_eq!((u1.cast(2), u1.cast(3)), (0, 1));
let w64 = LocalVarDecl {
width: 64,
signed: false,
};
assert_eq!(w64.cast(-1), -1);
let w128 = LocalVarDecl {
width: 128,
signed: true,
};
assert_eq!(w128.cast(i64::MIN), i64::MIN);
}
#[test]
fn first_match_groups_are_minted_densely() {
let mut a = Automaton::empty();
assert_eq!(a.first_match_groups, 0);
assert_eq!(a.new_first_match_group(), FirstMatchId(0));
assert_eq!(a.new_first_match_group(), FirstMatchId(1));
assert_eq!(a.first_match_groups, 2);
assert!(FirstMatchId(0) < FirstMatchId(1));
}
#[test]
fn abort_mode_arity_and_16_12_14_cross_product() {
assert_eq!(AbortMode::ALL.len(), AbortMode::COUNT);
assert!(!AbortMode::Accept.is_sync());
assert!(AbortMode::Accept.accepts());
assert!(!AbortMode::Reject.is_sync());
assert!(!AbortMode::Reject.accepts());
assert!(AbortMode::SyncAccept.is_sync());
assert!(AbortMode::SyncAccept.accepts());
assert!(AbortMode::SyncReject.is_sync());
assert!(!AbortMode::SyncReject.accepts());
}
#[test]
fn ids_are_distinct_and_hashable() {
use std::collections::HashSet;
let mut set = HashSet::new();
for i in 0..4u32 {
assert!(set.insert(AutomatonId(i)));
}
assert_ne!(ThreadStateId(0), ThreadStateId(1));
assert_ne!(LeafId(0), LeafId(1));
assert_ne!(ActionId(0), ActionId(1));
assert_ne!(LocalVarId(0), LocalVarId(1));
}
#[test]
fn automaton_wiring_and_is_accepting() {
let mut a = Automaton::empty();
assert!(
!a.admits_empty,
"a fresh automaton admits no empty match until a builder says so"
);
let start = a.start;
let mid = a.new_state();
let accept = a.new_state();
a.add_edge(start, Some(LeafId(0)), Transition::Epsilon, mid);
assert!(
!a.state(start).is_accepting(),
"start has only a guarded Epsilon edge, no Match edge yet"
);
a.add_edge(mid, None, Transition::Match { actions: vec![] }, accept);
assert!(
a.state(mid).is_accepting(),
"mid now has a Match edge among its others"
);
assert!(
!a.state(accept).is_accepting(),
"accept itself has no outgoing edges at all yet"
);
assert_eq!(a.state(start).edges.len(), 1);
assert_eq!(a.state(start).edges[0].guard, Some(LeafId(0)));
assert_eq!(a.state(start).edges[0].target, mid);
}
#[test]
fn max_span_is_unbounded_through_a_delay_loop() {
let mut a = Automaton::empty();
let (s0, s1) = (a.start, a.new_state());
a.add_edge(
s0,
None,
Transition::Delay {
min: 2,
max: Some(2),
},
s1,
);
assert_eq!(a.max_span(), Some(2));
let s2 = a.new_state();
a.add_edge(
s1,
None,
Transition::Delay {
min: 1,
max: Some(1),
},
s2,
);
a.add_edge(s2, None, Transition::Epsilon, s1);
assert_eq!(
a.max_span(),
None,
"a delay inside a loop repeats unboundedly"
);
let mut b = Automaton::empty();
let (t0, t1) = (b.start, b.new_state());
b.add_edge(
t0,
None,
Transition::Delay {
min: 1,
max: Some(1),
},
t1,
);
b.add_edge(t1, None, Transition::Epsilon, t1);
assert_eq!(b.max_span(), Some(1));
}
}