tamer: asg: Add expression edges

This introduces a number of abstractions, whose concepts are not fully
documented yet since I want to see how it evolves in practice first.

This introduces the concept of edge ontology (similar to a schema) using the
type system.  Even though we are not able to determine what the graph will
look like statically---since that's determined by data fed to us at
runtime---we _can_ ensure that the code _producing_ the graph from those
data will produce a graph that adheres to its ontology.

Because of the typed `ObjectIndex`, we're also able to implement operations
that are specific to the type of object that we're operating on.  Though,
since the type is not (yet?) stored on the edge itself, it is possible to
walk the graph without looking at node weights (the `ObjectContainer`) and
therefore avoid panics for invalid type assumptions, which is bad, but I
don't think that'll happen in practice, since we'll want to be resolving
nodes at some point.  But I'll addres that more in the future.

Another thing to note is that walking edges is only done in tests right now,
and so there's no filtering or anything; once there are nodes (if there are
nodes) that allow for different outgoing edge types, we'll almost certainly
want filtering as well, rather than panicing.  We'll also want to be able to
query for any object type, but filter only to what's permitted by the
ontology.

DEV-13160
main
Mike Gerwitz 2023-01-11 15:49:37 -05:00
parent 5e13c93a8f
commit f1cf35f499
5 changed files with 317 additions and 11 deletions

View File

@ -443,7 +443,7 @@ impl ParseState for AirAggregate {
}
(BuildingExpr(es, poi), OpenExpr(op, span)) => {
let oi = asg.create(Expr::new(op, span));
let oi = poi.create_subexpr(asg, Expr::new(op, span));
Transition(BuildingExpr(es.push(poi), oi)).incomplete()
}

View File

@ -22,7 +22,7 @@
use super::*;
use crate::{
asg::Ident,
asg::{object::ObjectRelTo, Ident, ObjectKind},
parse::{ParseError, Parsed},
span::dummy::*,
};
@ -582,3 +582,107 @@ fn expr_bind_to_empty() {
let expr = asg.expect_ident_obj::<Expr>(id_good);
assert_eq!(expr.span(), S2.merge(S4).unwrap());
}
// Subexpressions should not only have edges to their parent,
// but those edges ought to be ordered,
// allowing TAME to handle non-commutative expressions.
// We must further understand the relative order in which edges are stored
// for non-associative expressions.
#[test]
fn sibling_subexprs_have_ordered_edges_to_parent() {
let id_root = SPair("root".into(), S1);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// Identify the root so that it is not dangling.
Air::IdentExpr(id_root),
// Sibling A
Air::OpenExpr(ExprOp::Sum, S3),
Air::CloseExpr(S4),
// Sibling B
Air::OpenExpr(ExprOp::Sum, S5),
Air::CloseExpr(S6),
// Sibling C
Air::OpenExpr(ExprOp::Sum, S7),
Air::CloseExpr(S8),
Air::CloseExpr(S9),
];
let asg = asg_from_toks(toks);
// The root is the parent expression that should contain edges to each
// subexpression
// (the siblings above).
// Note that we retrieve its _index_,
// not the object itself.
let oi_root = asg.expect_ident_oi::<Expr>(id_root);
let siblings = oi_root
.edges::<Expr>(&asg)
.map(|oi| oi.resolve(&asg))
.collect::<Vec<_>>();
// The reversal here is an implementation detail with regards to how
// Petgraph stores its edges as effectively linked lists,
// using node indices instead of pointers.
// It is very important that we understand this behavior.
assert_eq!(siblings.len(), 3);
assert_eq!(siblings[2].span(), S3.merge(S4).unwrap());
assert_eq!(siblings[1].span(), S5.merge(S6).unwrap());
assert_eq!(siblings[0].span(), S7.merge(S8).unwrap());
}
#[test]
fn nested_subexprs_related_to_relative_parent() {
let id_root = SPair("root".into(), S1);
let id_suba = SPair("suba".into(), S2);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1), // 0
Air::IdentExpr(id_root),
Air::OpenExpr(ExprOp::Sum, S2), // 1
Air::IdentExpr(id_suba),
Air::OpenExpr(ExprOp::Sum, S3), // 2
Air::CloseExpr(S4),
Air::CloseExpr(S5),
Air::CloseExpr(S6),
];
let asg = asg_from_toks(toks);
let oi_0 = asg.expect_ident_oi::<Expr>(id_root);
let subexprs_0 = collect_subexprs(&asg, oi_0);
// Subexpr 1
assert_eq!(subexprs_0.len(), 1);
let (oi_1, subexpr_1) = subexprs_0[0];
assert_eq!(subexpr_1.span(), S2.merge(S5).unwrap());
let subexprs_1 = collect_subexprs(&asg, oi_1);
// Subexpr 2
assert_eq!(subexprs_1.len(), 1);
let (_, subexpr_2) = subexprs_1[0];
assert_eq!(subexpr_2.span(), S3.merge(S4).unwrap());
}
fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
where
I::IntoIter: Debug,
{
let mut sut = Sut::parse(toks.into_iter());
assert!(sut.all(|x| x.is_ok()));
sut.finalize().unwrap().into_context()
}
fn collect_subexprs<O: ObjectKind>(
asg: &Asg,
oi: ObjectIndex<O>,
) -> Vec<(ObjectIndex<O>, &O)>
where
O: ObjectRelTo<O>,
{
oi.edges::<O>(&asg)
.map(|oi| (oi, oi.resolve(&asg)))
.collect::<Vec<_>>()
}

View File

@ -21,6 +21,7 @@
use std::fmt::Display;
use super::{Asg, ObjectIndex};
use crate::{f::Functor, num::Dim, span::Span};
/// Expression.
@ -180,3 +181,21 @@ impl Display for DimState {
}
}
}
impl ObjectIndex<Expr> {
/// Create a new subexpression as the next child of this expression and
/// return the [`ObjectIndex`] of the new subexpression.
///
/// Sub-expressions maintain relative order to accommodate
/// non-associative and non-commutative expressions.
pub fn create_subexpr(
self,
asg: &mut Asg,
expr: Expr,
) -> ObjectIndex<Expr> {
let oi_subexpr = asg.create(expr);
self.add_edge_to(asg, oi_subexpr);
oi_subexpr
}
}

View File

@ -19,7 +19,7 @@
//! Abstract graph as the basis for concrete ASGs.
use super::object::ObjectContainer;
use super::object::{ObjectContainer, ObjectRelTo};
use super::{
AsgError, FragmentText, Ident, IdentKind, Object, ObjectIndex, ObjectKind,
Source, TransitionResult,
@ -33,7 +33,11 @@ use crate::parse::util::SPair;
use crate::parse::Token;
use crate::span::UNKNOWN_SPAN;
use crate::sym::SymbolId;
use petgraph::graph::{DiGraph, Graph, NodeIndex};
use petgraph::{
graph::{DiGraph, Graph, NodeIndex},
visit::EdgeRef,
Direction,
};
use std::fmt::Debug;
use std::result::Result;
@ -373,6 +377,21 @@ impl Asg {
ObjectIndex::new(node_id, span)
}
/// Add an edge from the [`Object`] represented by the
/// [`ObjectIndex`] `from_oi` to the object represented by `to_oi`.
///
/// For more information on how the ASG's ontology is enforced statically,
/// see [`ObjectRelTo`].
pub(super) fn add_edge<OA: ObjectKind, OB: ObjectKind>(
&mut self,
from_oi: ObjectIndex<OA>,
to_oi: ObjectIndex<OB>,
) where
OA: ObjectRelTo<OB>,
{
self.graph.add_edge(from_oi.into(), to_oi.into(), ());
}
/// Retrieve an object from the graph by [`ObjectIndex`].
///
/// Since an [`ObjectIndex`] should only be produced by an [`Asg`],
@ -433,6 +452,39 @@ impl Asg {
index.overwrite(obj_container.get::<Object>().span())
}
/// Create an iterator over the [`ObjectIndex`]es of the outgoing edges
/// of `self`.
///
/// This is a generic method that simply returns an [`ObjectKind`] of
/// [`Object`] for each [`ObjectIndex`];
/// it is the responsibility of the caller to narrow the type to
/// what is intended.
/// This is sufficient in practice,
/// since the graph cannot be constructed without adhering to the edge
/// ontology defined by [`ObjectRelTo`],
/// but this API is not helpful for catching problems at
/// compile-time.
///
/// The reason for providing a generic index to [`Object`] is that it
/// allows the caller to determine how strict it wants to be with
/// reading from the graph;
/// for example,
/// it may prefer to filter unwanted objects rather than panicing
/// if they do not match a given [`ObjectKind`],
/// depending on its ontology.
///
/// You should prefer methods on [`ObjectIndex`] instead,
/// with this method expected to be used only in those
/// implementations.
pub(super) fn edges<'a, O: ObjectKind + '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))
}
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
/// if any.
///
@ -445,12 +497,10 @@ impl Asg {
///
/// This will return [`None`] if the identifier is either opaque or does
/// not exist.
fn get_ident_obj_oi<O: ObjectKind>(
fn get_ident_oi<O: ObjectKind>(
&self,
ident: SPair,
) -> Option<ObjectIndex<O>> {
use petgraph::Direction;
self.lookup(ident.symbol())
.and_then(|identi| {
self.graph
@ -464,6 +514,27 @@ impl Asg {
.map(|ni| ObjectIndex::<O>::new(ni, ident.span()))
}
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
/// panicing if the identifier is either opaque or does not exist.
///
/// Panics
/// ======
/// This method will panic if the identifier is opaque
/// (has no edge to the object to which it is bound)
/// or does not exist on the graph.
pub fn expect_ident_oi<O: ObjectKind>(
&self,
ident: SPair,
) -> ObjectIndex<O> {
self.get_ident_oi(ident).diagnostic_expect(
diagnostic_opaque_ident_desc(ident),
&format!(
"opaque identifier: {} has no object binding",
TtQuote::wrap(ident),
),
)
}
/// Attempt to retrieve the [`Object`] to which the given `ident` is bound.
///
/// If the identifier either does not exist on the graph or is opaque
@ -483,8 +554,7 @@ impl Asg {
/// (that allows for the invariant to be violated)
/// or direct manipulation of the underlying graph.
pub fn get_ident_obj<O: ObjectKind>(&self, ident: SPair) -> Option<&O> {
self.get_ident_obj_oi::<O>(ident)
.map(|oi| self.expect_obj(oi))
self.get_ident_oi::<O>(ident).map(|oi| self.expect_obj(oi))
}
pub(super) fn expect_obj<O: ObjectKind>(&self, oi: ObjectIndex<O>) -> &O {

View File

@ -272,8 +272,83 @@ impl<O: ObjectKind> Clone for ObjectIndex<O> {
impl<O: ObjectKind> Copy for ObjectIndex<O> {}
impl<O: ObjectKind> ObjectIndex<O> {
pub fn new(index: NodeIndex, span: Span) -> Self {
Self(index, span, PhantomData::default())
pub fn new<S: Into<Span>>(index: NodeIndex, span: S) -> Self {
Self(index, span.into(), PhantomData::default())
}
/// Add an edge from `self` to `to_oi` on the provided [`Asg`].
///
/// An edge can only be added if ontologically valid;
/// see [`ObjectRelTo`] for more information.
///
/// See also [`Self::add_edge_to`].
pub fn add_edge_to<OB: ObjectKind>(
self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
) -> Self
where
O: ObjectRelTo<OB>,
{
asg.add_edge(self, to_oi);
self
}
/// Add an edge from `from_oi` to `self` on the provided [`Asg`].
///
/// An edge can only be added if ontologically valid;
/// see [`ObjectRelTo`] for more information.
///
/// See also [`Self::add_edge_to`].
pub fn add_edge_from<OB: ObjectKind>(
self,
asg: &mut Asg,
from_oi: ObjectIndex<OB>,
) -> Self
where
OB: ObjectRelTo<O>,
{
from_oi.add_edge_to(asg, self);
self
}
/// Create an iterator over the [`ObjectIndex`]es of the outgoing edges
/// of `self`.
///
/// 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>(
self,
asg: &'a Asg,
) -> impl Iterator<Item = ObjectIndex<OB>> + 'a
where
O: ObjectRelTo<OB> + 'a,
{
asg.edges(self).map(ObjectIndex::must_narrow_into::<OB>)
}
/// Resolve `self` to the object that it references.
///
/// Panics
/// ======
/// If our [`ObjectKind`] `O` does not match the actual type of the
/// object on the graph,
/// the system will panic.
pub fn resolve(self, asg: &Asg) -> &O {
asg.expect_obj(self)
}
}
impl ObjectIndex<Object> {
/// Indicate that the [`Object`] referenced by this index must be
/// narrowed into [`ObjectKind`] `O` when resolved.
///
/// This simply narrows the expected [`ObjectKind`].
pub fn must_narrow_into<O: ObjectKind>(self) -> ObjectIndex<O> {
match self {
Self(index, span, _) => ObjectIndex::new(index, span),
}
}
}
@ -316,6 +391,44 @@ impl<O: ObjectKind> From<ObjectIndex<O>> for Span {
}
}
/// Indicate that an [`ObjectKind`] `Self` can be related to
/// [`ObjectKind`] `OB` by creating an edge from `Self` to `OB`.
///
/// This trait defines a portion of the graph ontology,
/// allowing [`Self`] to be related to `OB` by creating a directed edge
/// from [`Self`] _to_ `OB`, as in:
///
/// ```text
/// (Self) -> (OB)
/// ```
///
/// While the data on the graph itself is dynamic and provided at runtime,
/// 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 {}
/// Indicate that an [`ObjectKind`] `Self` can be related to
/// [`ObjectKind`] `OB` by creating an edge from `OB` to `Self`.
///
/// _This trait exists for notational convenience and is intended only to
/// derive a blanket [`ObjectRelTo`] implementation._
/// This is because `impl`s are of the form `impl T for O`,
/// but it is more natural to reason about directed edges left-to-write as
/// `(From) -> (To)`;
/// this trait allows `impl ObjectRelFrom<OA> for OB` rather than the
/// equivalent `impl ObjectRelTo<OB> for OA`.
trait ObjectRelFrom<OA: ObjectKind>: ObjectKind {}
impl<OA: ObjectKind, OB: ObjectKind> ObjectRelTo<OB> for OA where
OB: ObjectRelFrom<OA>
{
}
// This describes the object relationship portion of the ASG's ontology.
impl ObjectRelFrom<Ident> for Expr {}
impl ObjectRelFrom<Expr> for Expr {}
/// A container for an [`Object`] allowing for owned borrowing of data.
///
/// The purpose of allowing this owned borrowing is to permit a functional