use crate::fst::{Fst, StateId};
use crate::semiring::Semiring;
use bitflags::bitflags;
use std::collections::HashSet;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PropertyFlags: u64 {
const ACCEPTOR = 1 << 0;
const NO_EPSILONS = 1 << 1;
const EPSILONS = 1 << 2;
const NO_INPUT_EPSILONS = 1 << 3;
const INPUT_EPSILONS = 1 << 4;
const NO_OUTPUT_EPSILONS = 1 << 5;
const OUTPUT_EPSILONS = 1 << 6;
const INPUT_DETERMINISTIC = 1 << 7;
const OUTPUT_DETERMINISTIC = 1 << 8;
const FUNCTIONAL = 1 << 9;
const UNAMBIGUOUS = 1 << 10;
const ACCESSIBLE = 1 << 11;
const COACCESSIBLE = 1 << 12;
const CONNECTED = 1 << 13;
const CYCLIC = 1 << 14;
const ACYCLIC = 1 << 15;
const INITIAL_ACYCLIC = 1 << 16;
const TOP_SORTED = 1 << 17;
const INPUT_SORTED = 1 << 18;
const OUTPUT_SORTED = 1 << 19;
const WEIGHTED = 1 << 20;
const UNWEIGHTED = 1 << 21;
const STRING = 1 << 22;
}
}
#[derive(Debug, Clone, Copy)]
pub struct FstProperties {
pub known: PropertyFlags,
pub properties: PropertyFlags,
}
impl Default for FstProperties {
fn default() -> Self {
Self {
known: PropertyFlags::empty(),
properties: PropertyFlags::empty(),
}
}
}
impl FstProperties {
pub const ACCEPTOR: PropertyFlags = PropertyFlags::ACCEPTOR;
pub const NO_EPSILONS: PropertyFlags = PropertyFlags::NO_EPSILONS;
pub const EPSILONS: PropertyFlags = PropertyFlags::EPSILONS;
pub const NO_INPUT_EPSILONS: PropertyFlags = PropertyFlags::NO_INPUT_EPSILONS;
pub const I_EPSILONS: PropertyFlags = PropertyFlags::INPUT_EPSILONS;
pub const NO_OUTPUT_EPSILONS: PropertyFlags = PropertyFlags::NO_OUTPUT_EPSILONS;
pub const O_EPSILONS: PropertyFlags = PropertyFlags::OUTPUT_EPSILONS;
pub const INPUT_DETERMINISTIC: PropertyFlags = PropertyFlags::INPUT_DETERMINISTIC;
pub const I_DETERMINISTIC: PropertyFlags = PropertyFlags::INPUT_DETERMINISTIC;
pub const OUTPUT_DETERMINISTIC: PropertyFlags = PropertyFlags::OUTPUT_DETERMINISTIC;
pub const FUNCTIONAL: PropertyFlags = PropertyFlags::FUNCTIONAL;
pub const UNAMBIGUOUS: PropertyFlags = PropertyFlags::UNAMBIGUOUS;
pub const ACCESSIBLE: PropertyFlags = PropertyFlags::ACCESSIBLE;
pub const COACCESSIBLE: PropertyFlags = PropertyFlags::COACCESSIBLE;
pub const CONNECTED: PropertyFlags = PropertyFlags::CONNECTED;
pub const CYCLIC: PropertyFlags = PropertyFlags::CYCLIC;
pub const ACYCLIC: PropertyFlags = PropertyFlags::ACYCLIC;
pub const INITIAL_ACYCLIC: PropertyFlags = PropertyFlags::INITIAL_ACYCLIC;
pub const TOP_SORTED: PropertyFlags = PropertyFlags::TOP_SORTED;
pub const INPUT_SORTED: PropertyFlags = PropertyFlags::INPUT_SORTED;
pub const OUTPUT_SORTED: PropertyFlags = PropertyFlags::OUTPUT_SORTED;
pub const WEIGHTED: PropertyFlags = PropertyFlags::WEIGHTED;
pub const UNWEIGHTED: PropertyFlags = PropertyFlags::UNWEIGHTED;
pub const STRING: PropertyFlags = PropertyFlags::STRING;
pub fn is_known(&self, flag: PropertyFlags) -> bool {
self.known.contains(flag)
}
pub fn has_property(&self, flag: PropertyFlags) -> bool {
self.is_known(flag) && self.properties.contains(flag)
}
pub fn contains(&self, flag: PropertyFlags) -> bool {
self.has_property(flag)
}
pub fn set_property(&mut self, flag: PropertyFlags, value: bool) {
self.known |= flag;
if value {
self.properties |= flag;
} else {
self.properties &= !flag;
}
}
pub fn invalidate_all(&mut self) {
self.known = PropertyFlags::empty();
self.properties = PropertyFlags::empty();
}
}
pub fn compute_properties<W: Semiring, F: Fst<W>>(fst: &F) -> FstProperties {
let mut props = FstProperties::default();
if fst.start().is_none() {
props.set_property(PropertyFlags::ACCEPTOR, true);
props.set_property(PropertyFlags::UNWEIGHTED, true);
props.set_property(PropertyFlags::WEIGHTED, false);
props.set_property(PropertyFlags::ACYCLIC, true);
props.set_property(PropertyFlags::INITIAL_ACYCLIC, true);
props.set_property(PropertyFlags::TOP_SORTED, true);
props.set_property(PropertyFlags::ACCESSIBLE, true);
props.set_property(PropertyFlags::COACCESSIBLE, true);
props.set_property(PropertyFlags::CONNECTED, true);
props.set_property(PropertyFlags::INPUT_DETERMINISTIC, true);
props.set_property(PropertyFlags::OUTPUT_DETERMINISTIC, true);
props.set_property(PropertyFlags::FUNCTIONAL, true);
props.set_property(PropertyFlags::NO_EPSILONS, true);
props.set_property(PropertyFlags::EPSILONS, false);
props.set_property(PropertyFlags::NO_INPUT_EPSILONS, true);
props.set_property(PropertyFlags::INPUT_EPSILONS, false);
props.set_property(PropertyFlags::NO_OUTPUT_EPSILONS, true);
props.set_property(PropertyFlags::OUTPUT_EPSILONS, false);
return props;
}
let mut has_epsilons = false;
let mut has_input_epsilons = false;
let mut has_output_epsilons = false;
let mut is_acceptor = true;
let mut is_unweighted = true;
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
fn has_cycle<W: Semiring, F: Fst<W>>(
fst: &F,
state: StateId,
visited: &mut HashSet<StateId>,
rec_stack: &mut HashSet<StateId>,
) -> bool {
visited.insert(state);
rec_stack.insert(state);
for arc in fst.arcs(state) {
if !visited.contains(&arc.nextstate) {
if has_cycle(fst, arc.nextstate, visited, rec_stack) {
return true;
}
} else if rec_stack.contains(&arc.nextstate) {
return true;
}
}
rec_stack.remove(&state);
false
}
let is_acyclic = if let Some(start_state) = fst.start() {
!has_cycle(fst, start_state, &mut visited, &mut rec_stack)
} else {
true
};
let accessible_states = visited.clone();
let is_accessible = if fst.start().is_some() {
accessible_states.len() == fst.num_states()
} else {
fst.num_states() == 0
};
let is_coaccessible = compute_coaccessible(fst);
let mut is_string = true;
for state in fst.states() {
let num_arcs = fst.num_arcs(state);
if num_arcs > 1 {
is_string = false;
break;
}
}
if !is_acyclic {
is_string = false;
}
let (is_input_deterministic, is_output_deterministic) = compute_determinism(fst);
let is_functional = compute_functional(fst);
for state in fst.states() {
for arc in fst.arcs(state) {
if arc.is_epsilon() {
has_epsilons = true;
}
if arc.is_epsilon_input() {
has_input_epsilons = true;
}
if arc.is_epsilon_output() {
has_output_epsilons = true;
}
if arc.ilabel != arc.olabel {
is_acceptor = false;
}
if !<W as num_traits::One>::is_one(&arc.weight) {
is_unweighted = false;
}
}
}
props.set_property(PropertyFlags::NO_EPSILONS, !has_epsilons);
props.set_property(PropertyFlags::EPSILONS, has_epsilons);
props.set_property(PropertyFlags::NO_INPUT_EPSILONS, !has_input_epsilons);
props.set_property(PropertyFlags::INPUT_EPSILONS, has_input_epsilons);
props.set_property(PropertyFlags::NO_OUTPUT_EPSILONS, !has_output_epsilons);
props.set_property(PropertyFlags::OUTPUT_EPSILONS, has_output_epsilons);
props.set_property(PropertyFlags::ACCEPTOR, is_acceptor);
props.set_property(PropertyFlags::UNWEIGHTED, is_unweighted);
props.set_property(PropertyFlags::WEIGHTED, !is_unweighted);
props.set_property(PropertyFlags::ACYCLIC, is_acyclic);
props.set_property(PropertyFlags::CYCLIC, !is_acyclic);
props.set_property(PropertyFlags::ACCESSIBLE, is_accessible);
props.set_property(PropertyFlags::COACCESSIBLE, is_coaccessible);
props.set_property(PropertyFlags::CONNECTED, is_accessible && is_coaccessible);
props.set_property(PropertyFlags::INPUT_DETERMINISTIC, is_input_deterministic);
props.set_property(PropertyFlags::OUTPUT_DETERMINISTIC, is_output_deterministic);
props.set_property(PropertyFlags::FUNCTIONAL, is_functional);
props.set_property(PropertyFlags::INITIAL_ACYCLIC, is_acyclic);
props.set_property(PropertyFlags::TOP_SORTED, is_acyclic);
props.set_property(PropertyFlags::STRING, is_string);
props
}
fn compute_coaccessible<W: Semiring, F: Fst<W>>(fst: &F) -> bool {
if fst.num_states() == 0 {
return true; }
let mut final_states = Vec::new();
for state in fst.states() {
if fst.is_final(state) {
final_states.push(state);
}
}
if final_states.is_empty() {
return false; }
let mut reverse_graph: std::collections::HashMap<StateId, Vec<StateId>> =
std::collections::HashMap::new();
for state in fst.states() {
for arc in fst.arcs(state) {
reverse_graph.entry(arc.nextstate).or_default().push(state);
}
}
let mut visited = std::collections::HashSet::new();
let mut stack = final_states.clone();
while let Some(state) = stack.pop() {
if visited.insert(state) {
if let Some(predecessors) = reverse_graph.get(&state) {
for &pred in predecessors {
if !visited.contains(&pred) {
stack.push(pred);
}
}
}
}
}
visited.len() == fst.num_states()
}
fn compute_determinism<W: Semiring, F: Fst<W>>(fst: &F) -> (bool, bool) {
let mut is_input_deterministic = true;
let mut is_output_deterministic = true;
for state in fst.states() {
let mut input_labels = std::collections::HashSet::new();
let mut output_labels = std::collections::HashSet::new();
for arc in fst.arcs(state) {
if !input_labels.insert(arc.ilabel) {
is_input_deterministic = false;
}
if !output_labels.insert(arc.olabel) {
is_output_deterministic = false;
}
if !is_input_deterministic && !is_output_deterministic {
return (false, false);
}
}
}
(is_input_deterministic, is_output_deterministic)
}
fn compute_functional<W: Semiring, F: Fst<W>>(fst: &F) -> bool {
let (is_input_deterministic, _) = compute_determinism(fst);
if !is_input_deterministic {
return false;
}
if let Some(start_state) = fst.start() {
let mut visited: std::collections::HashSet<StateId> = std::collections::HashSet::new();
let mut rec_stack: std::collections::HashSet<StateId> = std::collections::HashSet::new();
fn has_epsilon_cycle_dfs<W: Semiring, F: Fst<W>>(
fst: &F,
state: StateId,
visited: &mut std::collections::HashSet<StateId>,
rec_stack: &mut std::collections::HashSet<StateId>,
) -> bool {
if !visited.insert(state) {
return false;
}
rec_stack.insert(state);
for arc in fst.arcs(state) {
if arc.is_epsilon() {
if rec_stack.contains(&arc.nextstate) {
return true;
}
if !visited.contains(&arc.nextstate)
&& has_epsilon_cycle_dfs(fst, arc.nextstate, visited, rec_stack)
{
return true;
}
}
if !arc.is_epsilon()
&& !visited.contains(&arc.nextstate)
&& has_epsilon_cycle_dfs(fst, arc.nextstate, visited, rec_stack)
{
return true;
}
}
rec_stack.remove(&state);
false
}
!has_epsilon_cycle_dfs(fst, start_state, &mut visited, &mut rec_stack)
} else {
true }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use num_traits::One;
#[test]
fn test_empty_fst_properties() {
let fst = VectorFst::<TropicalWeight>::new();
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::ACCEPTOR));
assert!(props.contains(PropertyFlags::UNWEIGHTED));
assert!(props.contains(PropertyFlags::ACYCLIC));
assert!(props.contains(PropertyFlags::INITIAL_ACYCLIC));
assert!(props.contains(PropertyFlags::TOP_SORTED));
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
}
#[test]
fn test_single_state_fst_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
fst.set_start(s0);
fst.set_final(s0, TropicalWeight::one());
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::ACCEPTOR));
assert!(props.contains(PropertyFlags::UNWEIGHTED));
assert!(props.contains(PropertyFlags::ACYCLIC));
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
}
#[test]
fn test_simple_acceptor_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::ACCEPTOR));
assert!(props.contains(PropertyFlags::UNWEIGHTED));
assert!(props.contains(PropertyFlags::ACYCLIC));
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
}
#[test]
fn test_transducer_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 2, TropicalWeight::one(), s1));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::ACCEPTOR));
assert!(props.contains(PropertyFlags::UNWEIGHTED));
}
#[test]
fn test_weighted_fst_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(2.5), s1));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::UNWEIGHTED));
assert!(props.contains(PropertyFlags::ACCEPTOR));
}
#[test]
fn test_cyclic_fst_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::one(), s0));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::ACYCLIC));
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
}
#[test]
fn test_epsilon_fst_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::epsilon(TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(1, 1, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::EPSILONS));
assert!(props.contains(PropertyFlags::INPUT_EPSILONS));
assert!(props.contains(PropertyFlags::OUTPUT_EPSILONS));
}
#[test]
fn test_deterministic_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s0, Arc::new(1, 2, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::INPUT_DETERMINISTIC));
assert!(!props.contains(PropertyFlags::FUNCTIONAL));
}
#[test]
fn test_input_deterministic_fst() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s0, Arc::new(2, 2, TropicalWeight::one(), s2));
fst.add_arc(s1, Arc::new(3, 3, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::INPUT_DETERMINISTIC));
assert!(props.contains(PropertyFlags::FUNCTIONAL));
}
#[test]
fn test_output_deterministic_fst() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 10, TropicalWeight::one(), s1));
fst.add_arc(s0, Arc::new(2, 10, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::INPUT_DETERMINISTIC));
assert!(!props.contains(PropertyFlags::OUTPUT_DETERMINISTIC));
}
#[test]
fn test_coaccessible_property() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::one(), s2));
fst.add_arc(s0, Arc::new(3, 3, TropicalWeight::one(), s3));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(!props.contains(PropertyFlags::COACCESSIBLE));
assert!(!props.contains(PropertyFlags::CONNECTED));
}
#[test]
fn test_fully_connected_fst() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
assert!(props.contains(PropertyFlags::CONNECTED));
}
#[test]
fn test_functional_with_epsilon_cycles() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.set_final(s3, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::epsilon(TropicalWeight::one(), s0)); fst.add_arc(s1, Arc::new(1, 2, TropicalWeight::one(), s2)); fst.add_arc(s0, Arc::new(1, 3, TropicalWeight::one(), s3));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::FUNCTIONAL));
assert!(!props.contains(PropertyFlags::INPUT_DETERMINISTIC)); assert!(props.contains(PropertyFlags::EPSILONS));
assert!(props.contains(PropertyFlags::CYCLIC));
}
#[test]
fn test_empty_fst_special_cases() {
let fst = VectorFst::<TropicalWeight>::new();
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::INPUT_DETERMINISTIC));
assert!(props.contains(PropertyFlags::OUTPUT_DETERMINISTIC));
assert!(props.contains(PropertyFlags::FUNCTIONAL));
assert!(props.contains(PropertyFlags::ACCESSIBLE));
assert!(props.contains(PropertyFlags::COACCESSIBLE));
assert!(props.contains(PropertyFlags::CONNECTED));
}
#[test]
fn test_no_final_states() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
let props = compute_properties(&fst);
assert!(!props.contains(PropertyFlags::COACCESSIBLE));
assert!(!props.contains(PropertyFlags::CONNECTED));
}
#[test]
fn test_string_properties() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::one(), s2));
let props = compute_properties(&fst);
assert!(props.contains(PropertyFlags::STRING));
assert!(props.contains(PropertyFlags::ACYCLIC));
assert!(props.contains(PropertyFlags::TOP_SORTED));
}
#[test]
fn test_properties_bitwise_operations() {
let props1 = PropertyFlags::ACCEPTOR | PropertyFlags::UNWEIGHTED;
let props2 = PropertyFlags::ACYCLIC | PropertyFlags::UNWEIGHTED;
let intersection = props1 & props2;
assert!(intersection.contains(PropertyFlags::UNWEIGHTED));
assert!(!intersection.contains(PropertyFlags::ACCEPTOR));
assert!(!intersection.contains(PropertyFlags::ACYCLIC));
let union = props1 | props2;
assert!(union.contains(PropertyFlags::ACCEPTOR));
assert!(union.contains(PropertyFlags::UNWEIGHTED));
assert!(union.contains(PropertyFlags::ACYCLIC));
let complement = !props1;
assert!(!complement.contains(PropertyFlags::ACCEPTOR));
assert!(!complement.contains(PropertyFlags::UNWEIGHTED));
}
#[test]
fn test_properties_compatibility() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
fst.set_start(s0);
fst.set_final(s0, TropicalWeight::one());
let props = compute_properties(&fst);
if props.contains(PropertyFlags::ACCESSIBLE) {
assert!(props.contains(PropertyFlags::COACCESSIBLE));
}
if props.contains(PropertyFlags::STRING) {
assert!(props.contains(PropertyFlags::ACYCLIC));
}
if props.contains(PropertyFlags::TOP_SORTED) {
assert!(props.contains(PropertyFlags::ACYCLIC));
}
}
}