tamer: Initial concept for AIR/ASG Expr

This begins to place expressions on the graph---something that I've been
thinking about for a couple of years now, so it's interesting to finally be
doing it.

This is going to evolve; I want to get some things committed so that it's
clear how I'm moving forward.  The ASG makes things a bit awkward for a
number of reasons:

  1. I'm dealing with older code where I had a different model of doing
       things;
  2. It's mutable, rather than the mostly-functional lowering pipeline;
  3. We're dealing with an aggregate ever-evolving blob of data (the graph)
       rather than a stream of tokens; and
  4. We don't have as many type guarantees.

I've shown with the lowering pipeline that I'm able to take a mutable
reference and convert it into something that's both functional and
performant, where I remove it from its container (an `Option`), create a new
version of it, and place it back.  Rust is able to optimize away the memcpys
and such and just directly manipulate the underlying value, which is often a
register with all of the inlining.

_But_ this is a different scenario now.  The lowering pipeline has a narrow
context.  The graph has to keep hitting memory.  So we'll see how this
goes.  But it's most important to get this working and measure how it
performs; I'm not trying to prematurely optimize.  My attempts right now are
for the way that I wish to develop.

Speaking to #4 above, it also sucks that I'm not able to type the
relationships between nodes on the graph.  Rather, it's not that I _can't_,
but a project to created a typed graph library is beyond the scope of this
work and would take far too much time.  I'll leave that to a personal,
non-work project.  Instead, I'm going to have to narrow the type any time
the graph is accessed.  And while that sucks, I'm going to do my best to
encapsulate those details to make it as seamless as possible API-wise.  The
performance hit of performing the narrowing I'm hoping will be very small
relative to all the business logic going on (a single cache miss is bound to
be far more expensive than many narrowings which are just integer
comparisons and branching)...but we'll see.  Introducing branching sucks,
but branch prediction is pretty damn good in modern CPUs.

DEV-13160
main
Mike Gerwitz 2022-12-21 16:47:04 -05:00
parent 97685fd146
commit 646633883f
11 changed files with 744 additions and 55 deletions

View File

@ -17,11 +17,14 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::{Asg, AsgError, FragmentText, IdentKind, Source};
use super::{
Asg, AsgError, ExprOp, FragmentText, IdentKind, ObjectRef, Source,
};
use crate::{
asg::Expr,
fmt::{DisplayWrapper, TtQuote},
parse::{self, util::SPair, ParseState, Token, Transition, Transitionable},
span::UNKNOWN_SPAN,
span::{Span, UNKNOWN_SPAN},
sym::SymbolId,
};
use std::fmt::{Debug, Display};
@ -65,6 +68,38 @@ pub enum Air {
/// the ASG.
Todo,
/// Create a new [`Expr`] on the graph and place it atop of the
/// expression stack.
///
/// If there was previously an expression ρ atop of the stack before
/// this operation,
/// a reference to this new expression will be automatically added
/// to ρ,
/// treating it as a child expression.
/// Otherwise,
/// the expression will be dangling unless bound to an identifier,
/// which will produce an error.
///
/// All expressions have an associated [`ExprOp`] that determines how
/// the expression will be evaluated.
/// An expression is associated with a source location,
/// but is anonymous unless assigned an identifier using
/// [`Air::IdentExpr`].
///
/// Expressions are composed of references to other expressions.
OpenExpr(ExprOp, Span),
/// Complete the expression atop of the expression stack and pop it from
/// the stack.
CloseExpr(Span),
/// Assign an identifier to the expression atop of the expression stack.
///
/// An expression may be bound to multiple identifiers,
/// but an identifier can only be bound to a single expression.
/// Binding an identifier will declare it.
IdentExpr(SPair),
/// Declare a resolved identifier.
IdentDecl(SPair, IdentKind, Source),
@ -118,6 +153,14 @@ impl Display for Air {
match self {
Todo => write!(f, "TODO"),
OpenExpr(op, _) => write!(f, "open {op} expression"),
CloseExpr(_) => write!(f, "close expression"),
IdentExpr(id) => {
write!(f, "identify expression as {}", TtQuote::wrap(id))
}
IdentDecl(spair, _, _) => {
write!(f, "declaration of identifier {}", TtQuote::wrap(spair))
}
@ -152,6 +195,32 @@ impl Display for Air {
pub enum AirAggregate {
#[default]
Empty,
/// Building an expression that is yet not reachable from any other
/// object.
///
/// Dangling expressions are expected to transition into
/// [`Self::ReachableExpr`] after being bound to an identifier.
/// Closing a dangling expression will result in a
/// [`AsgError::DanglingExpr`].
DanglingExpr(ObjectRef),
/// Building an expression that is reachable from another object.
///
/// See also [`Self::DanglingExpr`].
ReachableExpr(ObjectRef),
}
impl Display for AirAggregate {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use AirAggregate::*;
match self {
Empty => write!(f, "awaiting AIR input for ASG"),
DanglingExpr(_) => write!(f, "building dangling expression"),
ReachableExpr(_) => write!(f, "building reachable expression"),
}
}
}
impl ParseState for AirAggregate {
@ -173,7 +242,43 @@ impl ParseState for AirAggregate {
use AirAggregate::*;
match (self, tok) {
(Empty, Todo) => Transition(Empty).incomplete(),
(_, Todo) => Transition(Empty).incomplete(),
(Empty, OpenExpr(op, span)) => {
let oi = asg.create(Expr::new(op, span));
Transition(DanglingExpr(oi)).incomplete()
}
(Empty, CloseExpr(_)) => todo!("no matching expr to end"),
(DanglingExpr(oi), CloseExpr(end)) => {
let start: Span = oi.into();
Transition(Empty).err(AsgError::DanglingExpr(
start.merge(end).unwrap_or(start),
))
}
(ReachableExpr(oi), CloseExpr(end)) => {
let _ = asg.mut_map_obj::<Expr>(oi, |expr| {
expr.map_span(|span| span.merge(end).unwrap_or(span))
});
Transition(Empty).incomplete()
}
(Empty, IdentExpr(_)) => todo!("cannot bind ident to nothing"),
(DanglingExpr(oi), IdentExpr(id)) => {
// TODO: error on existing ident
let identi = asg.lookup_or_missing(id);
asg.add_dep(identi, oi);
Transition(ReachableExpr(oi)).incomplete()
}
(st, tok @ (OpenExpr(..) | IdentExpr(..))) => {
todo!("{st:?} -> {tok:?}")
}
(Empty, IdentDecl(name, kind, src)) => {
asg.declare(name, kind, src).map(|_| ()).transition(Empty)
@ -199,6 +304,12 @@ impl ParseState for AirAggregate {
Transition(Empty).incomplete()
}
(
st,
tok @ (IdentDecl(..) | IdentExternDecl(..) | IdentDep(..)
| IdentFragment(..) | IdentRoot(..)),
) => todo!("{st:?}, {tok:?}"),
}
}
@ -207,17 +318,5 @@ impl ParseState for AirAggregate {
}
}
impl Display for AirAggregate {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use AirAggregate::*;
// This is not terribly useful beyond indicating which parser caused
// an error.
match self {
Empty => write!(f, "awaiting AIR input for ASG"),
}
}
}
#[cfg(test)]
mod test;

View File

@ -258,3 +258,55 @@ fn ident_root_existing() {
// ...should have been subsequently rooted.
assert!(asg.is_rooted(ident_node));
}
#[test]
fn expr_empty() {
let id = SPair("foo".into(), S2);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
Air::IdentExpr(id),
Air::CloseExpr(S3),
];
let mut sut = Sut::parse(toks.into_iter());
assert!(sut.all(|x| x.is_ok()));
let mut asg = sut.finalize().unwrap().into_context();
// The expression should have been bound to this identifier so that
// we're able to retrieve it from the graph by name.
asg.mut_map_obj_by_ident::<Expr>(id, |expr| {
assert_eq!(expr.span(), S1.merge(S3).unwrap());
expr
});
}
// Danging expressions are unreachable and therefore not useful
// constructions.
// Prohibit them,
// since they're either mistakes or misconceptions.
#[test]
fn expr_dangling() {
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// No `IdentExpr`,
// so this expression is dangling.
Air::CloseExpr(S2),
];
// The error span should encompass the entire expression.
// TODO: ...let's actually have something inside this expression.
let full_span = S1.merge(S2).unwrap();
assert_eq!(
vec![
Ok(Parsed::Incomplete),
Err(ParseError::StateError(AsgError::DanglingExpr(full_span)))
],
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
);
// TODO: recovery, which will probably mean that we need to have some
// successful tests first to support it
}

View File

@ -24,7 +24,10 @@ use std::{
fmt::{self, Display},
};
use crate::diagnose::{AnnotatedSpan, Diagnostic};
use crate::{
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
span::Span,
};
use super::TransitionError;
@ -33,20 +36,39 @@ use super::TransitionError;
pub enum AsgError {
/// An object could not change state in the manner requested.
IdentTransition(TransitionError),
/// An expresion is not reachable by any other expression or
/// identifier.
///
/// A dangling expression has no incoming edge from any other object and
/// can therefore not be referenced.
///
/// Since the expression is dangling,
/// it must be anonymous,
/// and can therefore only be identified meaningfully to the user by
/// its span.
/// The span should encompass the entirety of the expression.
DanglingExpr(Span),
}
impl Display for AsgError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use AsgError::*;
match self {
Self::IdentTransition(err) => Display::fmt(&err, fmt),
IdentTransition(err) => Display::fmt(&err, f),
DanglingExpr(_) => write!(f, "dangling expression"),
}
}
}
impl Error for AsgError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use AsgError::*;
match self {
Self::IdentTransition(err) => err.source(),
IdentTransition(err) => err.source(),
DanglingExpr(_) => None,
}
}
}
@ -59,7 +81,20 @@ impl From<TransitionError> for AsgError {
impl Diagnostic for AsgError {
fn describe(&self) -> Vec<AnnotatedSpan> {
// TODO: This won't be useful until we have spans.
vec![]
use AsgError::*;
match self {
// TODO: need spans
IdentTransition(_) => vec![],
DanglingExpr(span) => vec![
span.error("expression has no parent or identifier"),
span.help("an expression must either be the child of another "),
span.help(
" expression or be assigned an identifier, otherwise ",
),
span.help(" its value cannot referenced."),
],
}
}
}

View File

@ -0,0 +1,176 @@
// Expressions represented on the ASG
//
// Copyright (C) 2014-2022 Ryan Specialty Group, LLC.
//
// This file is part of TAME.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Expressions on the ASG.
use std::fmt::Display;
use crate::{num::Dim, span::Span};
/// Expression.
///
/// The [`Span`] of an expression should be expanded to encompass not only
/// all child expressions,
/// but also any applicable closing span.
#[derive(Debug, PartialEq, Eq)]
pub struct Expr(ExprOp, ExprDim, Span);
impl Expr {
pub fn new(op: ExprOp, span: Span) -> Self {
Self(op, ExprDim::default(), span)
}
pub fn span(&self) -> Span {
match self {
Expr(_, _, span) => *span,
}
}
pub fn map_span(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(op, dim, span) => Self(op, dim, f(span)),
}
}
}
impl Into<Span> for &Expr {
fn into(self) -> Span {
self.span()
}
}
impl Display for Expr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self(op, dim, _) => write!(f, "{op} expression with {dim}"),
}
}
}
/// Expression operation type.
#[derive(Debug, PartialEq, Eq)]
pub enum ExprOp {
Sum,
Product,
}
impl Display for ExprOp {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use ExprOp::*;
match self {
Sum => write!(f, "sum"),
Product => write!(f, "product"),
}
}
}
/// The dimensionality of the expression itself and the target
/// dimensionality required by its context.
///
/// If the target dimensionality is greater,
/// then the expression's value will be broadcast;
/// if it is less,
/// than it will be folded according to the expression's [`ExprOp`].
///
/// A value of [`None`] means that the dimensionality has not yet been
/// constrained either through inference or explicit specification by the
/// user.
#[derive(Debug, PartialEq, Eq, Default)]
pub struct ExprDim(InnerDim, OuterDim);
impl Display for ExprDim {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self(inner, outer) => write!(f, "{inner} and {outer}"),
}
}
}
/// Dimensionality of the inner expression.
///
/// This represents the dimensionality after any necessary broadcasting of
/// all values referenced by this expression.
/// This does not necessarily correspond to the dimensionality of the
/// final value of this expression;
/// see [`OuterDim`].
#[derive(Debug, PartialEq, Eq, Default)]
pub struct InnerDim(DimState);
impl Display for InnerDim {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self(dim_st) => write!(f, "inner dimensionality {dim_st}"),
}
}
}
/// Final dimensionality of the expression as observed by others.
///
/// This represents what other expressions will see as the dimensionality of
/// this expression.
/// For example,
/// [`InnerDim`] may require broadcasting,
/// but this outer dimensionality may require subsequent folding.
#[derive(Debug, PartialEq, Eq, Default)]
pub struct OuterDim(DimState);
impl Display for OuterDim {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self(dim_st) => write!(f, "outer dimensionality {dim_st}"),
}
}
}
#[derive(Debug, PartialEq, Eq, Default)]
pub enum DimState {
/// Dimensionality has been explicitly constrained via user
/// specification and cannot be changed.
///
/// The provided [`Span`] references the location of the user-specified
/// constraint.
/// Unification must honor this value.
_Constraint(Dim, Span),
/// Dimensionality is still being inferred,
/// but it is known to be at least this size.
///
/// The provide [`Span`] must serve as evidence for this inference by
/// referencing a value of this dimensionality.
_AtLeast(Dim, Span),
/// Dimensionality is not constrained and has not yet been inferred.
///
/// This also serves as a TODO until we infer dimensions.
#[default]
Unknown,
}
impl Display for DimState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use DimState::*;
match self {
_Constraint(dim, _) => write!(f, "constrained to {dim}"),
_AtLeast(dim, _) => write!(f, "of at least {dim}"),
Unknown => write!(f, "unknown"),
}
}
}

View File

@ -20,13 +20,19 @@
//! Abstract graph as the basis for concrete ASGs.
use super::{
AsgError, FragmentText, Ident, IdentKind, Object, Source, TransitionResult,
AsgError, FragmentText, Ident, IdentKind, Object, ObjectRef, Source,
TransitionResult,
};
use crate::diagnose::panic::DiagnosticPanic;
use crate::diagnose::Annotate;
use crate::fmt::{DisplayWrapper, TtQuote};
use crate::global;
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::Direction;
use std::fmt::Debug;
use std::result::Result;
@ -187,7 +193,7 @@ impl Asg {
let index = self.graph.add_node(Some(Ident::declare(ident).into()));
self.index_identifier(sym, index);
ObjectRef::new(index)
ObjectRef::new(index, ident.span())
})
}
@ -359,6 +365,18 @@ impl Asg {
self.with_ident_lookup(name, |obj| obj.set_fragment(text))
}
/// Create a new object on the graph.
///
/// The provided [`ObjectRef`] will be augmented with the span
/// of `obj`.
pub(super) fn create<O: Into<Object>>(&mut self, obj: O) -> ObjectRef {
let o = obj.into();
let span = o.span();
let node_id = self.graph.add_node(Some(o));
ObjectRef::new(node_id, span)
}
/// Retrieve an object from the graph by [`ObjectRef`].
///
/// Since an [`ObjectRef`] should only be produced by an [`Asg`],
@ -374,6 +392,140 @@ impl Asg {
})
}
/// Map over an inner [`Object`] referenced by [`ObjectRef`].
///
/// The type `O` is the expected type of the [`Object`],
/// which should be known to the caller.
/// This method will attempt to narrow to that object type,
/// panicing if there is a mismatch;
/// see the [`object` module documentation](super::object) for more
/// information and rationale on this behavior.
///
/// The `mut_` prefix of this method is intended to emphasize that,
/// unlike traditional `map` methods,
/// this does not take and return ownership;
/// the ASG is most often interacted with via mutable reference.
///
/// Panics
/// ======
/// This method chooses to simplify the API by choosing panics for
/// situations that ought never to occur and represent significant bugs
/// in the compiler.
/// Those situations are:
///
/// 1. If the provided [`ObjectRef`] references a node index that is
/// not present on the graph;
/// 2. If the node referenced by [`ObjectRef`] exists but its container
/// is empty because an object was taken but never returned; and
/// 3. If an object cannot be narrowed (downcast) to type `O`,
/// representing a type mismatch between what the caller thinks
/// this object represents and what the object actually is.
#[must_use = "returned ObjectRef has a possibly-updated and more relevant span"]
pub fn mut_map_obj<O>(
&mut self,
index: ObjectRef,
f: impl FnOnce(O) -> O,
) -> ObjectRef
where
Object: Into<O>,
Object: From<O>,
{
let obj_container =
self.graph.node_weight_mut(index.into()).diagnostic_expect(
vec![
index.internal_error("this object is missing from the ASG"),
index.help(
"this means that either an ObjectRef was malformed, or",
),
index.help(
" the object no longer exists on the graph, both of",
),
index.help(
" which are unexpected and possibly represent data",
),
index.help(" corruption."),
index.help("The system cannot proceed with confidence."),
],
"invalid ObjectRef: data are missing from the ASG",
);
// Any function borrowing from the graph ought to also be responsible
// for returning it in all possible code paths,
// as we are.
// And error here means that this must not be the case,
// or that we're breaking encapsulation somewhere.
let cur_obj = obj_container.take().diagnostic_expect(
vec![
index.internal_error(
"this object was borrowed from the graph and \
was not returned",
),
index.help(
"this means that some operation used take() on the object",
),
index.help(
" container but never replaced it with an updated object",
),
index.help(
" after the operation completed, which should not \
be possible.",
),
],
"inaccessible ObjectRef: object has not been returned to the ASG",
);
let new_obj: Object = f(cur_obj.into()).into();
let new_span = new_obj.span();
// This is where we replace the object that we borrowed,
// as referenced in the above panic.
obj_container.replace(new_obj);
index.map_span(|_| new_span)
}
pub fn mut_map_obj_by_ident<O>(
&mut self,
ident: SPair,
f: impl FnOnce(O) -> O,
) where
Object: Into<O>,
Object: From<O>,
{
let oi = self
.lookup(ident.symbol())
.and_then(|identi| {
self.graph
.neighbors_directed(identi.into(), Direction::Outgoing)
.next()
})
.map(|ni| ObjectRef::new(ni, ident.span()))
.diagnostic_expect(
vec![
ident.internal_error(
"this identifier is not bound to any object on the ASG",
),
ident.help(
"the system expects to be able to reach the object that"
),
ident.help(
" this identifies, but this identifier has no"
),
ident.help(
" corresponding object present on the graph."
),
],
&format!(
"opaque identifier: {} has no object binding",
TtQuote::wrap(ident),
),
);
// We do not care about the updated ObjectRef from `mut_map_obj`
// since it was ephemeral for this operation.
let _ = self.mut_map_obj(oi, f);
}
/// Retrieve an identifier from the graph by [`ObjectRef`].
///
/// If the object exists but is not an identifier,
@ -396,7 +548,7 @@ impl Asg {
self.index
.get(i)
.filter(|ni| ni.index() > 0)
.map(|ni| ObjectRef::new(*ni))
.map(|ni| ObjectRef::new(*ni, UNKNOWN_SPAN))
}
/// Declare that `dep` is a dependency of `ident`.
@ -447,32 +599,6 @@ impl Asg {
}
}
/// Reference to an [object][super::object] stored within the [`Asg`].
///
/// Ident references are integer offsets,
/// not pointers.
/// See the [module-level documentation][self] for more information.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct ObjectRef(NodeIndex);
impl ObjectRef {
pub fn new(index: NodeIndex) -> Self {
Self(index)
}
}
impl From<NodeIndex> for ObjectRef {
fn from(index: NodeIndex) -> Self {
Self(index)
}
}
impl From<ObjectRef> for NodeIndex {
fn from(objref: ObjectRef) -> Self {
objref.0
}
}
#[cfg(test)]
mod test {
use super::super::error::AsgError;

View File

@ -19,6 +19,8 @@
//! Identifiers (a type of [object][super::object]).
use std::fmt::Display;
use crate::{
diagnose::{Annotate, Diagnostic},
fmt::{DisplayWrapper, TtQuote},
@ -91,6 +93,27 @@ pub enum Ident {
IdentFragment(SPair, IdentKind, Source, FragmentText),
}
impl Display for Ident {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Missing(id) => {
write!(f, "missing identifier {}", TtQuote::wrap(id))
}
Self::Ident(id, kind, _) => {
write!(f, "{kind} identifier {}", TtQuote::wrap(id))
}
Self::Extern(id, kind, _) => {
write!(f, "{kind} extern identifier {}", TtQuote::wrap(id))
}
Self::IdentFragment(id, kind, _, _) => write!(
f,
"{kind} identifier {} with compiled fragment",
TtQuote::wrap(id)
),
}
}
}
impl Ident {
/// Identifier name.
pub fn name(&self) -> SPair {

View File

@ -64,6 +64,7 @@
//! if either identifier has not yet been declared.
mod error;
mod expr;
mod graph;
mod ident;
mod object;
@ -71,12 +72,13 @@ mod object;
pub mod air;
pub use error::AsgError;
pub use graph::{Asg, AsgResult, IndexType, ObjectRef};
pub use expr::{Expr, ExprDim, ExprOp};
pub use graph::{Asg, AsgResult, IndexType};
pub use ident::{
FragmentText, Ident, IdentKind, Source, TransitionError, TransitionResult,
UnresolvedError,
};
pub use object::Object;
pub use object::{Object, ObjectRef};
/// Default concrete ASG implementation.
pub type DefaultAsg = graph::Asg;

View File

@ -18,10 +18,57 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Objects represented by the ASG.
//!
//! Dynamic Object Types and Narrowing
//! ==================================
//! Unlike the functional lowering pipeline that precedes it,
//! the ASG is a mutable, ever-evolving graph of dynamic data.
//! The ASG does not benefit from the same type-level guarantees that the
//! rest of the system does at compile-time.
//!
//! Any node on the graph can represent any type of [`Object`].
//! An [`ObjectRef`] contains an index into the graph,
//! _not_ a reference;
//! consequently,
//! it is possible (though avoidable) for objects to be modified out
//! from underneath references.
//! Consequently,
//! we cannot trust that an [`ObjectRef`] is what we expect it to be when
//! performing an operation on the graph using that index.
//!
//! To perform an operation on a particular type of object,
//! we must first _narrow_ it.
//! Narrowing converts from the [`Object`] sum type into a more specific
//! inner type,
//! such as [`Ident`] or [`Expr`].
//! This operation _should_,
//! if the compiler is operating correctly,
//! always succeed,
//! because the type of object should always match our expectations;
//! the explicit narrowing is to ensure memory safety in case that
//! assumption does not hold.
//!
//! Since a type mismatch represents a bug in the compiler,
//! the API favors [`Result`]-free narrowing rather than burdening every
//! caller with additional complexity---we
//! will attempt to narrow and panic in the event of a failure,
//! including a diagnostic message that helps to track down the issue
//! using whatever [`Span`]s we have available.
//! [`ObjectRef`] is associated with a span derived from the point of its
//! creation to handle this diagnostic situation automatically.
use super::Ident;
use super::{Expr, Ident};
use crate::{
diagnose::Annotate,
diagnostic_panic,
span::{Span, UNKNOWN_SPAN},
};
use petgraph::graph::NodeIndex;
use std::fmt::Display;
/// Some object on the ASG.
/// An object on the ASG.
///
/// See the [module-level documentation](super) for more information.
#[derive(Debug, PartialEq)]
pub enum Object {
/// Represents the root of all reachable identifiers.
@ -34,9 +81,32 @@ pub enum Object {
/// Identifier (a named object).
Ident(Ident),
/// Expression.
///
/// An expression may optionally be named by one or more [`Ident`]s.
Expr(Expr),
}
impl Display for Object {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Root => write!(f, "root ASG node"),
Self::Ident(ident) => Display::fmt(ident, f),
Self::Expr(expr) => Display::fmt(expr, f),
}
}
}
impl Object {
pub fn span(&self) -> Span {
match self {
Self::Root => UNKNOWN_SPAN,
Self::Ident(ident) => ident.span(),
Self::Expr(expr) => expr.span(),
}
}
/// Retrieve an [`Ident`] reference,
/// or [`None`] if the object is not an identifier.
pub fn as_ident_ref(&self) -> Option<&Ident> {
@ -82,3 +152,96 @@ impl From<Ident> for Object {
Self::Ident(ident)
}
}
impl From<Expr> for Object {
fn from(expr: Expr) -> Self {
Self::Expr(expr)
}
}
impl Into<Expr> for Object {
/// Narrow an object into an [`Expr`],
/// panicing if the object is not of that type.
fn into(self) -> Expr {
match self {
Self::Expr(expr) => expr,
_ => diagnostic_panic!(
self.span()
.internal_error("expected this object to be an expression")
.into(),
"expected expression, found {self}",
),
}
}
}
/// Index representing an [`Object`] stored on the [`Asg`](super::Asg).
///
/// Ident references are integer offsets,
/// not pointers.
/// See the [module-level documentation][self] for more information.
///
/// This object is associated with a [`Span`] that identifies the source
/// location from which this object was derived;
/// this is intended to be used to provide diagnostic information in the
/// event that the object somehow becomes unavailable for later
/// operations.
///
/// _The span is not accounted for in [`PartialEq`]_,
/// since it represents the context in which the [`ObjectRef`] was
/// retrieved,
/// and the span associated with the underlying [`Object`] may evolve
/// over time.
#[derive(Debug, Clone, Copy)]
pub struct ObjectRef(NodeIndex, Span);
impl ObjectRef {
pub fn new(index: NodeIndex, span: Span) -> Self {
Self(index, span)
}
pub fn map_span(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(index, span) => Self(index, f(span)),
}
}
}
impl PartialEq for ObjectRef {
/// Compare two [`ObjectRef`]s' indicies,
/// without concern for its associated [`Span`].
///
/// See [`ObjectRef`] for more information on why the span is not
/// accounted for in this comparison.
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self(index_a, _), Self(index_b, _)) => index_a == index_b,
}
}
}
impl Eq for ObjectRef {}
// TODO: Remove this,
// since we need thought to be put into providing spans.
impl From<NodeIndex> for ObjectRef {
fn from(index: NodeIndex) -> Self {
Self(index, UNKNOWN_SPAN)
}
}
impl From<ObjectRef> for NodeIndex {
fn from(objref: ObjectRef) -> Self {
match objref {
ObjectRef(index, _) => index,
}
}
}
impl From<ObjectRef> for Span {
fn from(value: ObjectRef) -> Self {
match value {
ObjectRef(_, span) => span,
}
}
}

View File

@ -27,6 +27,7 @@ use super::section::{SectionsError, XmleSections};
use crate::{
asg::{Asg, Ident, IdentKind, Object},
diagnose::{Annotate, Diagnostic},
diagnostic_unreachable,
fmt::{
AndConjList, DisplayWrapper, JoinListWrap, ListDisplayWrapper, Raw,
TtQuote,
@ -65,6 +66,14 @@ where
match asg.get(index).expect("missing object") {
Object::Root => (),
Object::Ident(ident) => dest.push(ident)?,
Object::Expr(expr) => diagnostic_unreachable!(
expr.internal_error(
"this expression should not be present on the graph"
)
.into(),
"linker graph should not contain expressions",
),
}
}

View File

@ -34,6 +34,8 @@ pub enum Dim {
Matrix = 2,
}
assert_eq_size!(Option<Dim>, Dim);
impl From<Dim> for u8 {
fn from(dim: Dim) -> Self {
dim as u8

View File

@ -474,7 +474,9 @@ impl Span {
/// within another.
/// See test cases for more information.
/// (TODO: Maybe we should move the test cases into these docs?)
pub fn merge(self, b: Span) -> Option<Span> {
pub fn merge<S: Into<Span>>(self, other: S) -> Option<Span> {
let b = other.into();
if self.context() != b.context() {
return None;
}