tamer: asg::graph: Static- and runtime-enforced multi-kind edge ontolgoy
This allows for edges to be multiple types, and gives us two important benefits: (a) Compiler-verified correctness to ensure that we don't generate graphs that do not adhere to the ontology; and (b) Runtime verification of types, so that bugs are still memory safe. There is a lot more information in the documentation within the patch. This took a lot of iterating to get something that was tolerable. There's quite a bit of boilerplate here, and maybe that'll be abstracted away better in the future as the graph grows. In particular, it was challenging to determine how I wanted to actually go about narrowing and looking up edges. Initially I had hoped to represent the subsets as `ObjectKind`s as well so that you could use them anywhere `ObjectKind` was expected, but that proved to be far too difficult because I cannot return a reference to a subset of `Object` (the value would be owned on generation). And while in a language like C maybe I'd pad structures and cast between them safely, since they _do_ overlap, I can't confidently do that here since Rust's discriminant and layout are not under my control. I tried playing around with `std::mem::Discriminant` as well, but `discriminant` (the function) requires a _value_, meaning I couldn't get the discriminant of a static `Object` variant without some dummy value; wasn't worth it over `ObjectRelTy.` We further can't assign values to enum variants unless they hold no data. Rust a decade from now may be different and will be interesting to look back on this struggle. DEV-13597main
parent
8739c2c570
commit
8735c2fca3
|
@ -20,9 +20,9 @@
|
|||
//! These are tested as if they are another API directly atop of the ASG,
|
||||
//! since that is how they are used.
|
||||
|
||||
use super::super::graph::object::{ObjectKind, ObjectRelTo};
|
||||
use super::super::Ident;
|
||||
use super::*;
|
||||
use crate::asg::graph::object::ObjectRel;
|
||||
use crate::{
|
||||
parse::{ParseError, Parsed},
|
||||
span::dummy::*,
|
||||
|
@ -610,8 +610,9 @@ fn sibling_subexprs_have_ordered_edges_to_parent() {
|
|||
let oi_root = asg.expect_ident_oi::<Expr>(id_root);
|
||||
|
||||
let siblings = oi_root
|
||||
.edges::<Expr>(&asg)
|
||||
.map(|oi| oi.resolve(&asg))
|
||||
.edges(&asg)
|
||||
.filter_map(ObjectRel::narrow::<Expr>)
|
||||
.map(ObjectIndex::cresolve(&asg))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The reversal here is an implementation detail with regards to how
|
||||
|
@ -791,14 +792,12 @@ where
|
|||
sut.finalize().unwrap().into_context()
|
||||
}
|
||||
|
||||
fn collect_subexprs<O: ObjectKind>(
|
||||
fn collect_subexprs(
|
||||
asg: &Asg,
|
||||
oi: ObjectIndex<O>,
|
||||
) -> Vec<(ObjectIndex<O>, &O)>
|
||||
where
|
||||
O: ObjectRelTo<O>,
|
||||
{
|
||||
oi.edges::<O>(&asg)
|
||||
oi: ObjectIndex<Expr>,
|
||||
) -> Vec<(ObjectIndex<Expr>, &Expr)> {
|
||||
oi.edges(&asg)
|
||||
.filter_map(|rel| rel.narrow::<Expr>())
|
||||
.map(|oi| (oi, oi.resolve(&asg)))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
//! Abstract semantic graph.
|
||||
|
||||
use self::object::{ObjectRelTy, ObjectRelatable};
|
||||
|
||||
use super::{
|
||||
AsgError, FragmentText, Ident, IdentKind, Object, ObjectIndex, ObjectKind,
|
||||
Source, TransitionResult,
|
||||
|
@ -52,7 +54,7 @@ pub trait IndexType = petgraph::graph::IndexType;
|
|||
pub type AsgResult<T> = Result<T, AsgError>;
|
||||
|
||||
/// There are currently no data stored on edges ("edge weights").
|
||||
pub type AsgEdge = ();
|
||||
pub type AsgEdge = ObjectRelTy;
|
||||
|
||||
/// Each node of the graph.
|
||||
pub type Node = ObjectContainer;
|
||||
|
@ -271,7 +273,7 @@ impl Asg {
|
|||
/// See also [`IdentKind::is_auto_root`].
|
||||
pub fn add_root(&mut self, identi: ObjectIndex<Ident>) {
|
||||
self.graph
|
||||
.add_edge(self.root_node, identi.into(), Default::default());
|
||||
.add_edge(self.root_node, identi.into(), ObjectRelTy::Ident);
|
||||
}
|
||||
|
||||
/// Whether an object is rooted.
|
||||
|
@ -394,7 +396,8 @@ impl Asg {
|
|||
) where
|
||||
OA: ObjectRelTo<OB>,
|
||||
{
|
||||
self.graph.add_edge(from_oi.into(), to_oi.into(), ());
|
||||
self.graph
|
||||
.add_edge(from_oi.into(), to_oi.into(), OB::rel_ty());
|
||||
}
|
||||
|
||||
/// Retrieve an object from the graph by [`ObjectIndex`].
|
||||
|
@ -473,13 +476,16 @@ impl Asg {
|
|||
/// it may prefer to filter unwanted objects rather than panicing
|
||||
/// if they do not match a given [`ObjectKind`],
|
||||
/// depending on its ontology.
|
||||
fn edges<'a, O: ObjectKind + 'a>(
|
||||
fn edges<'a, O: ObjectKind + ObjectRelatable + 'a>(
|
||||
&'a self,
|
||||
oi: ObjectIndex<O>,
|
||||
) -> impl Iterator<Item = ObjectIndex<Object>> + 'a {
|
||||
self.graph
|
||||
.edges(oi.into())
|
||||
.map(move |edge| ObjectIndex::new(edge.target(), oi))
|
||||
) -> impl Iterator<Item = O::Rel> + 'a {
|
||||
self.graph.edges(oi.into()).map(move |edge| {
|
||||
O::new_rel_dyn(
|
||||
*edge.weight(),
|
||||
ObjectIndex::<Object>::new(edge.target(), oi),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
|
||||
|
@ -632,9 +638,11 @@ impl Asg {
|
|||
&mut self,
|
||||
identi: ObjectIndex<Ident>,
|
||||
depi: ObjectIndex<O>,
|
||||
) {
|
||||
) where
|
||||
Ident: ObjectRelTo<O>,
|
||||
{
|
||||
self.graph
|
||||
.update_edge(identi.into(), depi.into(), Default::default());
|
||||
.update_edge(identi.into(), depi.into(), O::rel_ty());
|
||||
}
|
||||
|
||||
/// Check whether `dep` is a dependency of `ident`.
|
||||
|
@ -669,7 +677,7 @@ impl Asg {
|
|||
let depi = self.lookup_or_missing(dep);
|
||||
|
||||
self.graph
|
||||
.update_edge(identi.into(), depi.into(), Default::default());
|
||||
.update_edge(identi.into(), depi.into(), Ident::rel_ty());
|
||||
|
||||
(identi, depi)
|
||||
}
|
||||
|
|
|
@ -26,15 +26,21 @@
|
|||
//! The ASG does not benefit from the same type-level guarantees that the
|
||||
//! rest of the system does at compile-time.
|
||||
//!
|
||||
//! However,
|
||||
//! we _are_ able to utilize the type system to ensure statically that
|
||||
//! there exists no code path that is able to generated an invalid graph
|
||||
//! (a graph that does not adhere to its ontology as described below).
|
||||
//!
|
||||
//! Any node on the graph can represent any type of [`Object`].
|
||||
//! An [`ObjectIndex`] contains an index into the graph,
|
||||
//! _not_ a reference;
|
||||
//! consequently,
|
||||
//! it is possible (though avoidable) for objects to be modified out
|
||||
//! from underneath references.
|
||||
//! it is therefore possible (though avoidable) for objects to be
|
||||
//! modified out from underneath references.
|
||||
//! Consequently,
|
||||
//! we cannot trust that an [`ObjectIndex`] is what we expect it to be when
|
||||
//! performing an operation on the graph using that index.
|
||||
//! performing an operation on the graph using that index,
|
||||
//! though the system is designed to uphold an invariant that the _type_
|
||||
//! of [`Object`] cannot be changed.
|
||||
//!
|
||||
//! To perform an operation on a particular type of object,
|
||||
//! we must first _narrow_ it.
|
||||
|
@ -60,6 +66,46 @@
|
|||
//! using whatever [`Span`]s we have available.
|
||||
//! [`ObjectIndex`] is associated with a span derived from the point of its
|
||||
//! creation to handle this diagnostic situation automatically.
|
||||
//!
|
||||
//! Edge Types and Narrowing
|
||||
//! ------------------------
|
||||
//! Unlike nodes,
|
||||
//! edges may reference [`Object`]s of many different types,
|
||||
//! as defined by the graph's ontology.
|
||||
//!
|
||||
//! The set [`ObjectKind`] types that may be related _to_
|
||||
//! (via edges)
|
||||
//! from other objects are the variants of [`ObjectRelTy`].
|
||||
//! Each such [`ObjectKind`] must implement [`ObjectRelatable`],
|
||||
//! where [`ObjectRelatable::Rel`] is an enum whose variants represent a
|
||||
//! _subset_ of [`Object`]'s variants that are valid targets for edges
|
||||
//! from that object type.
|
||||
//! If some [`ObjectKind`] `OA` is able to be related to another
|
||||
//! [`ObjectKind`] `OB`,
|
||||
//! then [`ObjectRelTo::<OB>`](ObjectRelTo) is implemented for `OA`.
|
||||
//!
|
||||
//! When querying the graph for edges using [`ObjectIndex::edges`],
|
||||
//! the corresponding [`ObjectRelatable::Rel`] type is provided,
|
||||
//! which may then be acted upon or filtered by the caller.
|
||||
//! Unlike nodes,
|
||||
//! it is difficult to statically expect exact edge types in most code
|
||||
//! paths
|
||||
//! (beyond the `Rel` object itself),
|
||||
//! and so [`ObjectRel::narrow`] produces an [`Option`] of the inner
|
||||
//! [`ObjectIndex`],
|
||||
//! rather than panicing.
|
||||
//! This `Option` is convenient to use with `Iterator::filter_map` to query
|
||||
//! for specific edge types.
|
||||
//!
|
||||
//! Using [`ObjectRelTo`],
|
||||
//! we are able to ensure statically that all code paths only add edges to
|
||||
//! the [`Asg`] that adhere to the ontology described above;
|
||||
//! it should therefore not be possible for an edge to exist on the
|
||||
//! graph that is not represented by [`ObjectRelatable::Rel`],
|
||||
//! provided that it is properly defined.
|
||||
//! Since [`ObjectRel`] narrows into an [`ObjectIndex`],
|
||||
//! the system will produce runtime panics if there is ever any attempt to
|
||||
//! follow an edge to an unexpected [`ObjectKind`].
|
||||
|
||||
use super::Asg;
|
||||
use crate::{
|
||||
|
@ -99,6 +145,22 @@ pub enum Object {
|
|||
Expr(Expr),
|
||||
}
|
||||
|
||||
/// Object types corresponding to variants in [`Object`] that are able to
|
||||
/// serve as targets of object relations
|
||||
/// (edges on the graph).
|
||||
///
|
||||
/// These are used as small tags for [`ObjectRelatable`].
|
||||
/// Rust unfortunately makes working with its internal tags difficult,
|
||||
/// despite their efforts with [`std::mem::Discriminant`],
|
||||
/// which requires a _value_ to produce.
|
||||
///
|
||||
/// TODO: `pub(super)` when the graph can be better encapsulated.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ObjectRelTy {
|
||||
Ident,
|
||||
Expr,
|
||||
}
|
||||
|
||||
impl Display for Object {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
|
@ -323,14 +385,14 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
/// Note that the [`ObjectKind`] `OB` indicates what type of
|
||||
/// [`ObjectIndex`]es will be yielded by the returned iterator;
|
||||
/// this method does nothing to filter non-matches.
|
||||
pub fn edges<'a, OB: ObjectKind + 'a>(
|
||||
pub fn edges<'a>(
|
||||
self,
|
||||
asg: &'a Asg,
|
||||
) -> impl Iterator<Item = ObjectIndex<OB>> + 'a
|
||||
) -> impl Iterator<Item = <O as ObjectRelatable>::Rel> + 'a
|
||||
where
|
||||
O: ObjectRelTo<OB> + 'a,
|
||||
O: ObjectRelatable + 'a,
|
||||
{
|
||||
asg.edges(self).map(ObjectIndex::must_narrow_into::<OB>)
|
||||
asg.edges(self)
|
||||
}
|
||||
|
||||
/// Resolve `self` to the object that it references.
|
||||
|
@ -344,6 +406,11 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
asg.expect_obj(self)
|
||||
}
|
||||
|
||||
/// Curried [`Self::resolve`].
|
||||
pub fn cresolve<'a>(asg: &'a Asg) -> impl FnMut(Self) -> &'a O {
|
||||
move |oi| oi.resolve(asg)
|
||||
}
|
||||
|
||||
/// Resolve the identifier and map over the resulting [`Object`]
|
||||
/// narrowed to [`ObjectKind`] `O`,
|
||||
/// replacing the object on the given [`Asg`].
|
||||
|
@ -381,6 +448,32 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
Err::<_, Infallible>(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lift [`Self`] into [`Option`] and [`filter`](Option::filter) based
|
||||
/// on whether the [`ObjectRelatable::rel_ty`] of [`Self`]'s `O`
|
||||
/// matches that of `OB`.
|
||||
///
|
||||
/// More intuitively:
|
||||
/// if `OB` is the same [`ObjectKind`] associated with [`Self`],
|
||||
/// return [`Some(Self)`](Some).
|
||||
/// Otherwise,
|
||||
/// return [`None`].
|
||||
fn filter_rel<OB: ObjectKind + ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndex<OB>>
|
||||
where
|
||||
O: ObjectRelatable,
|
||||
{
|
||||
let Self(index, span, _pd) = self;
|
||||
|
||||
// Rust doesn't know that `OB` and `O` will be the same,
|
||||
// but this will be the case.
|
||||
// If it weren't,
|
||||
// then [`ObjectIndex`] protects us at runtime,
|
||||
// so there are no safety issues here.
|
||||
Some(ObjectIndex::<OB>(index, span, PhantomData::default()))
|
||||
.filter(|_| O::rel_ty() == OB::rel_ty())
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Object> {
|
||||
|
@ -449,11 +542,179 @@ impl<O: ObjectKind> From<ObjectIndex<O>> for Span {
|
|||
/// the systems that _construct_ the graph using the runtime data can be
|
||||
/// statically analyzed by the type system to ensure that they only
|
||||
/// construct graphs that adhere to this schema.
|
||||
pub trait ObjectRelTo<OB: ObjectKind>: ObjectKind {}
|
||||
pub trait ObjectRelTo<OB: ObjectKind + ObjectRelatable> =
|
||||
ObjectRelatable where <Self as ObjectRelatable>::Rel: From<ObjectIndex<OB>>;
|
||||
|
||||
// This describes the object relationship portion of the ASG's ontology.
|
||||
impl ObjectRelTo<Expr> for Ident {}
|
||||
impl ObjectRelTo<Expr> for Expr {}
|
||||
/// Identify [`Self::Rel`] as a sum type consisting of the subset of
|
||||
/// [`Object`] variants representing the valid _target_ edges of
|
||||
/// [`Self`].
|
||||
///
|
||||
/// This is used to derive [`ObjectRelTo``],
|
||||
/// which can be used as a trait bound to assert a valid relationship
|
||||
/// between two [`Object`]s.
|
||||
pub trait ObjectRelatable: ObjectKind {
|
||||
/// Sum type representing a subset of [`Object`] variants that are valid
|
||||
/// targets for edges from [`Self`].
|
||||
///
|
||||
/// See [`ObjectRel`] for more information.
|
||||
type Rel: ObjectRel;
|
||||
|
||||
/// The [`ObjectRelTy`] tag used to identify this [`ObjectKind`] as a
|
||||
/// target of a relation.
|
||||
fn rel_ty() -> ObjectRelTy;
|
||||
|
||||
/// Represent a relation to another [`ObjectKind`] that cannot be
|
||||
/// statically known and must be handled at runtime.
|
||||
///
|
||||
/// See [`ObjectRel`] for more information.
|
||||
fn new_rel_dyn(ty: ObjectRelTy, oi: ObjectIndex<Object>) -> Self::Rel;
|
||||
}
|
||||
|
||||
/// A relationship to another [`ObjectKind`].
|
||||
///
|
||||
/// This trait is intended to be implemented by enums that represent the
|
||||
/// subset of [`ObjectKind`]s that are able to serve as edge targets for
|
||||
/// the [`ObjectRelatable`] that utilizes it as its
|
||||
/// [`ObjectRelatable::Rel`].
|
||||
///
|
||||
/// As described in the [module-level documentation](super),
|
||||
/// the concrete [`ObjectKind`] of an edge is generally not able to be
|
||||
/// determined statically outside of code paths that created the
|
||||
/// [`Object`] anew.
|
||||
/// But we _can_ at least narrow the types of [`ObjectKind`]s to those
|
||||
/// [`ObjectRelTo`]s that we know are valid,
|
||||
/// since the system is restricted (statically) to those edges when
|
||||
/// performing operations on the graph.
|
||||
///
|
||||
/// This [`ObjectRel`] represents that subset of [`ObjectKind`]s.
|
||||
/// A caller may decide to dispatch based on the type of edge it receives,
|
||||
/// or it may filter edges with [`Self::narrow`] in conjunction with
|
||||
/// [`Iterator::filter_map`]
|
||||
/// (for example).
|
||||
/// Since the wrapped value is an [`ObjectIndex`],
|
||||
/// the system will eventually panic if it attempts to reference a node
|
||||
/// that is not of the type expected by the edge,
|
||||
/// which can only happen if the edge has an incorrect [`ObjectRelTy`],
|
||||
/// meaning the graph is somehow corrupt
|
||||
/// (because system invariants were not upheld).
|
||||
///
|
||||
/// This affords us both runtime memory safety and static guarantees that
|
||||
/// the system is not able to generate an invalid graph that does not
|
||||
/// adhere to the prescribed ontology,
|
||||
/// provided that invariants are properly upheld by the
|
||||
/// [`asg`](crate::asg) module.
|
||||
pub trait ObjectRel {
|
||||
/// Attempt to narrow into the [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// Unlike [`Object`] nodes,
|
||||
/// _this operation does not panic_,
|
||||
/// instead returning an [`Option`].
|
||||
/// If the relationship is of type `OB`,
|
||||
/// then [`Some`] will be returned with an inner
|
||||
/// [`ObjectIndex<OB>`](ObjectIndex).
|
||||
/// If the narrowing fails,
|
||||
/// [`None`] will be returned instead.
|
||||
///
|
||||
/// This return value is well-suited for [`Iterator::filter_map`] to
|
||||
/// query for edges of particular kinds.
|
||||
fn narrow<OB: ObjectKind + ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndex<OB>>;
|
||||
}
|
||||
|
||||
/// Subset of [`ObjectKind`]s that are valid targets for edges from
|
||||
/// [`Ident`].
|
||||
///
|
||||
/// See [`ObjectRel`] for more information.
|
||||
pub enum IdentRel {
|
||||
Ident(ObjectIndex<Ident>),
|
||||
Expr(ObjectIndex<Expr>),
|
||||
}
|
||||
|
||||
impl ObjectRel for IdentRel {
|
||||
fn narrow<OB: ObjectKind + ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndex<OB>> {
|
||||
match self {
|
||||
Self::Ident(oi) => oi.filter_rel(),
|
||||
Self::Expr(oi) => oi.filter_rel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectRelatable for Ident {
|
||||
type Rel = IdentRel;
|
||||
|
||||
fn rel_ty() -> ObjectRelTy {
|
||||
ObjectRelTy::Ident
|
||||
}
|
||||
|
||||
fn new_rel_dyn(ty: ObjectRelTy, oi: ObjectIndex<Object>) -> IdentRel {
|
||||
match ty {
|
||||
ObjectRelTy::Ident => IdentRel::Ident(oi.must_narrow_into()),
|
||||
ObjectRelTy::Expr => IdentRel::Expr(oi.must_narrow_into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIndex<Ident>> for IdentRel {
|
||||
fn from(value: ObjectIndex<Ident>) -> Self {
|
||||
Self::Ident(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIndex<Expr>> for IdentRel {
|
||||
fn from(value: ObjectIndex<Expr>) -> Self {
|
||||
Self::Expr(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Subset of [`ObjectKind`]s that are valid targets for edges from
|
||||
/// [`Expr`].
|
||||
///
|
||||
/// See [`ObjectRel`] for more information.
|
||||
pub enum ExprRel {
|
||||
Ident(ObjectIndex<Ident>),
|
||||
Expr(ObjectIndex<Expr>),
|
||||
}
|
||||
|
||||
impl ObjectRel for ExprRel {
|
||||
fn narrow<OB: ObjectKind + ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndex<OB>> {
|
||||
match self {
|
||||
Self::Ident(oi) => oi.filter_rel(),
|
||||
Self::Expr(oi) => oi.filter_rel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectRelatable for Expr {
|
||||
type Rel = ExprRel;
|
||||
|
||||
fn rel_ty() -> ObjectRelTy {
|
||||
ObjectRelTy::Expr
|
||||
}
|
||||
|
||||
fn new_rel_dyn(ty: ObjectRelTy, oi: ObjectIndex<Object>) -> ExprRel {
|
||||
match ty {
|
||||
ObjectRelTy::Ident => ExprRel::Ident(oi.must_narrow_into()),
|
||||
ObjectRelTy::Expr => ExprRel::Expr(oi.must_narrow_into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIndex<Ident>> for ExprRel {
|
||||
fn from(value: ObjectIndex<Ident>) -> Self {
|
||||
Self::Ident(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjectIndex<Expr>> for ExprRel {
|
||||
fn from(value: ObjectIndex<Expr>) -> Self {
|
||||
Self::Expr(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A container for an [`Object`] allowing for owned borrowing of data.
|
||||
///
|
||||
|
|
|
@ -37,9 +37,12 @@
|
|||
//!
|
||||
//! Graph Structure
|
||||
//! ===============
|
||||
//! Each node (vector) in the graph represents an [`Object`],
|
||||
//! Each node (vertex) in the graph represents an [`Object`],
|
||||
//! such as an identifier or an expression.
|
||||
//! Each directed edge `(A->B)` represents that `A` depends upon `B`.
|
||||
//! For information on how [`Object`]s are stored and represented on the
|
||||
//! graph,
|
||||
//! and for information on relationships between objects,
|
||||
//! see the [`graph::object`] module.
|
||||
//!
|
||||
//! Graphs may contain cycles for recursive functions—that is,
|
||||
//! TAME's ASG is _not_ a DAG.
|
||||
|
@ -50,9 +53,6 @@
|
|||
//! [graph]: https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)
|
||||
//! [scc]: https://en.wikipedia.org/wiki/Strongly_connected_component
|
||||
//!
|
||||
//! Each object may have a number of valid states;
|
||||
//! see [`Ident`] for valid object states and transitions.
|
||||
//!
|
||||
//! Missing Identifiers
|
||||
//! -------------------
|
||||
//! Since identifiers in TAME can be defined in any order relative to their
|
||||
|
|
Loading…
Reference in New Issue