tamer: asg::air::AirAggregate: Initial impl of nested exprs

This introduces a number of concepts together, again to demonstrate that
they were derived.

This introduces support for nested expressions, extending the previous
work.  It also supports error recovery for dangling expressions.

The parser states are a mess; there is a lot of duplicate code here that
needs refactoring, but I wanted to commit this first at a known-good state
so that the diff will demonstrate the need for the change that will
follow; the opportunities for abstraction are plainly visible.

The immutable stack introduced here could be generalized, if needed, in the
future.

Another important note is that Rust optimizes away the `memcpy`s for the
stack that was introduced here.  The initial Parser Context was introduced
because of `ArrayVec` inhibiting that elision, but Vec never had that
problem.  In the future, I may choose to go back and remove ArrayVec, but I
had wanted to keep memory allocation out of the picture as much as possible
to make the disassembly and call graph easier to reason about and to have
confidence that optimizations were being performed as intended.

With that said---it _should_ be eliding in tamec, since we're not doing
anything meaningful yet with the graph.  It does also elide in tameld, but
it's possible that Rust recognizes that those code paths are never taken
because tameld does nothing with expressions.  So I'll have to monitor this
as I progress and adjust accordingly; it's possible a future commit will
call BS on everything I just said.

Of course, the counter-point to that is that Rust is optimizing them away
anyway, but Vec _does_ still require allocation; I was hoping to keep such
allocation at the fringes.  But another counter-point is that it _still_ is
allocated at the fringe, when the context is initialized for the parser as
part of the lowering pipeline.  But I didn't know how that would all come
together back then.

...alright, enough rambling.

DEV-13160
main
Mike Gerwitz 2023-01-05 15:57:06 -05:00
parent b8a7a78f43
commit 40c941d348
3 changed files with 475 additions and 133 deletions

View File

@ -198,29 +198,113 @@ impl Display for Air {
}
}
/// AIR parser state.
/// Stack of held expressions,
/// with the root expression at the bottom of the stack.
///
/// This currently has no parsing state;
/// all state is stored on the ASG itself,
/// which is the parsing context.
#[derive(Debug, PartialEq, Eq, Default)]
pub enum AirAggregate {
#[default]
Empty,
/// Expression [`ObjectIndex`]es are pushed onto this stack when
/// parsing a subexpression,
/// and are popped when the subexpression terminates.
/// The active expression is _not_ stored on this stack to avoid unnecessary
/// indirection.
///
/// Despite the immutable interface,
/// this does modify the inner [`Vec`] in-place;
/// it does not reallocate unless its capacity has been reached.
///
/// Unlike other parts of the system,
/// this is heap-allocated,
/// but should be very cache-friendly.
/// This reason for heap allocation is that this is explicitly
/// _unbounded_—systems like code generators ought to be able to output
/// expressions in a tacit style without worrying about arbitrary limits.
/// It is worth noting that the other parts of the system using
/// stack-allocated data structures is less about performance and more
/// about the simplicity afforded by keeping allocators out of the picture.
/// We'll address performance issues if they appear during profiling.
///
/// Another benefit of using [`Vec`] here is that Rust is able to properly
/// optimize away `memcpy`s for it,
/// rather than having to utilize the parser's mutable context.
/// Further,
/// the ASG is heap-allocated,
/// so we're not avoiding the heap anyway.
///
/// The interface is modeled after [Haskell's `Stack`][haskell-stack],
/// with a slight variation for [`Self::pop`] so that we can avoid
/// reallocation after a stack is used up,
/// which is frequent.
///
/// [haskell-stack]: https://hackage.haskell.org/package/Stack/docs/Data-Stack.html
#[derive(Debug, PartialEq, Eq)]
pub struct ExprStack(Vec<ObjectIndex<Expr>>);
/// Building an expression that is yet not reachable from any other
/// object.
impl ExprStack {
fn push(self, item: ObjectIndex<Expr>) -> Self {
let Self(mut stack) = self;
stack.push(item);
Self(stack)
}
/// Attempt to remove an item from the stack,
/// returning a new stack and the item,
/// if any.
///
/// This returns a new [`Self`] even if it is empty so that it can be
/// reused without having to reallocate.
fn pop(self) -> (Self, Option<ObjectIndex<Expr>>) {
let Self(mut stack) = self;
let oi = stack.pop();
(Self(stack), oi)
}
/// Whether the current expression being parsed is the root expression.
///
/// This simply means that the stack is empty.
fn is_at_root(&self) -> bool {
matches!(self, Self(stack) if stack.is_empty())
}
}
impl Default for ExprStack {
fn default() -> Self {
// TODO: 16 is a generous guess that is very unlikely to be exceeded
// in practice at the time of writing,
// even with template expansion,
// but let's develop an informed heuristic.
// Note that this is very unlikely to make a difference;
// I just don't like using numbers without data to back them up.
Self(Vec::with_capacity(16))
}
}
/// AIR parser state.
#[derive(Debug, PartialEq, Eq)]
pub enum AirAggregate {
/// Parser is not currently performing any work.
///
/// This state is accepting iff the inner [`ExprStack`] is empty.
Empty(ExprStack),
/// Building an expression whose root 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(ObjectIndex<Expr>),
DanglingExpr(ExprStack, ObjectIndex<Expr>),
/// Building an expression that is reachable from another object.
///
/// See also [`Self::DanglingExpr`].
ReachableExpr(ObjectIndex<Expr>),
ReachableExpr(ExprStack, ObjectIndex<Expr>),
}
impl Default for AirAggregate {
fn default() -> Self {
Self::Empty(ExprStack::default())
}
}
impl Display for AirAggregate {
@ -228,13 +312,24 @@ impl Display for AirAggregate {
use AirAggregate::*;
match self {
Empty => write!(f, "awaiting AIR input for ASG"),
DanglingExpr(_) => write!(f, "building dangling expression"),
ReachableExpr(_) => write!(f, "building reachable expression"),
Empty(es) => write!(f, "awaiting AIR input for ASG with {es}"),
DanglingExpr(es, _) => {
write!(f, "building dangling expression with {es}")
}
ReachableExpr(es, _) => {
write!(f, "building reachable expression with {es}")
}
}
}
}
impl Display for ExprStack {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let Self(stack) = self;
write!(f, "expression stack length of {}", stack.len())
}
}
impl ParseState for AirAggregate {
type Token = Air;
type Object = ();
@ -254,67 +349,104 @@ impl ParseState for AirAggregate {
use AirAggregate::*;
match (self, tok) {
(_, Todo) => Transition(Empty).incomplete(),
(st, Todo) => Transition(st).incomplete(),
(Empty, OpenExpr(op, span)) => {
(Empty(es), OpenExpr(op, span)) => {
let oi = asg.create(Expr::new(op, span));
Transition(DanglingExpr(oi)).incomplete()
Transition(DanglingExpr(es, oi)).incomplete()
}
(Empty, CloseExpr(_)) => todo!("no matching expr to end"),
(DanglingExpr(es, poi), OpenExpr(op, span)) => {
let oi = asg.create(Expr::new(op, span));
Transition(DanglingExpr(es.push(poi), oi)).incomplete()
}
(DanglingExpr(oi), CloseExpr(end)) => {
(ReachableExpr(es, poi), OpenExpr(op, span)) => {
let oi = asg.create(Expr::new(op, span));
Transition(ReachableExpr(es.push(poi), oi)).incomplete()
}
(Empty(_), CloseExpr(_)) => todo!("no matching expr to end"),
(DanglingExpr(es, oi), CloseExpr(end)) if es.is_at_root() => {
let start: Span = oi.into();
Transition(Empty).err(AsgError::DanglingExpr(
start.merge(end).unwrap_or(start),
))
match es.pop() {
(es, Some(poi)) => Transition(DanglingExpr(es, poi)),
(es, None) => Transition(Empty(es)),
}
.err(AsgError::DanglingExpr(start.merge(end).unwrap_or(start)))
}
(ReachableExpr(oi), CloseExpr(end)) => {
(DanglingExpr(es, oi), CloseExpr(end)) => {
let _ = asg.mut_map_obj::<Expr>(oi, |expr| {
expr.map(|span| span.merge(end).unwrap_or(span))
});
Transition(Empty).incomplete()
match es.pop() {
(es, Some(poi)) => Transition(DanglingExpr(es, poi)),
(es, None) => Transition(Empty(es)),
}
.incomplete()
}
(Empty, IdentExpr(_)) => todo!("cannot bind ident to nothing"),
(ReachableExpr(es, oi), CloseExpr(end)) => {
let _ = asg.mut_map_obj::<Expr>(oi, |expr| {
expr.map(|span| span.merge(end).unwrap_or(span))
});
(DanglingExpr(oi), IdentExpr(id)) => {
match es.pop() {
(es, Some(poi)) => Transition(ReachableExpr(es, poi)),
(es, None) => Transition(Empty(es)),
}
.incomplete()
}
(Empty(_), IdentExpr(_)) => todo!("cannot bind ident to nothing"),
(DanglingExpr(es, oi), IdentExpr(id)) => {
// TODO: error on existing ident
let identi = asg.lookup_or_missing(id);
asg.add_dep(identi, oi);
Transition(ReachableExpr(oi)).incomplete()
if es.is_at_root() {
Transition(ReachableExpr(es, oi)).incomplete()
} else {
Transition(DanglingExpr(es, oi)).incomplete()
}
}
(st, tok @ (OpenExpr(..) | IdentExpr(..))) => {
todo!("{st:?} -> {tok:?}")
(ReachableExpr(es, oi), IdentExpr(id)) => {
// TODO: error on existing ident
let identi = asg.lookup_or_missing(id);
asg.add_dep(identi, oi);
Transition(ReachableExpr(es, oi)).incomplete()
}
(Empty, IdentDecl(name, kind, src)) => {
asg.declare(name, kind, src).map(|_| ()).transition(Empty)
(st @ Empty(_), IdentDecl(name, kind, src)) => {
asg.declare(name, kind, src).map(|_| ()).transition(st)
}
(Empty, IdentExternDecl(name, kind, src)) => asg
(st @ Empty(_), IdentExternDecl(name, kind, src)) => asg
.declare_extern(name, kind, src)
.map(|_| ())
.transition(Empty),
.transition(st),
(Empty, IdentDep(sym, dep)) => {
(st @ Empty(_), IdentDep(sym, dep)) => {
asg.add_dep_lookup(sym, dep);
Transition(Empty).incomplete()
Transition(st).incomplete()
}
(Empty, IdentFragment(sym, text)) => {
asg.set_fragment(sym, text).map(|_| ()).transition(Empty)
(st @ Empty(_), IdentFragment(sym, text)) => {
asg.set_fragment(sym, text).map(|_| ()).transition(st)
}
(Empty, IdentRoot(sym)) => {
(st @ Empty(_), IdentRoot(sym)) => {
let obj = asg.lookup_or_missing(sym);
asg.add_root(obj);
Transition(Empty).incomplete()
Transition(st).incomplete()
}
(
@ -326,7 +458,7 @@ impl ParseState for AirAggregate {
}
fn is_accepting(&self, _: &Self::Context) -> bool {
*self == Self::Empty
matches!(self, Self::Empty(es) if es.is_at_root())
}
}

View File

@ -266,14 +266,70 @@ fn expr_empty() {
let mut sut = Sut::parse(toks.into_iter());
assert!(sut.all(|x| x.is_ok()));
let mut asg = sut.finalize().unwrap().into_context();
let 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
});
let expr = asg.expect_ident_obj::<Expr>(id);
assert_eq!(expr.span(), S1.merge(S3).unwrap());
}
#[test]
fn expr_non_empty() {
let id_a = SPair("foo".into(), S2);
let id_b = SPair("bar".into(), S2);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// Identifier while still empty...
Air::IdentExpr(id_a),
Air::OpenExpr(ExprOp::Sum, S3),
// (note that the inner expression _does not_ have an ident binding)
Air::CloseExpr(S4),
// ...and an identifier non-empty.
Air::IdentExpr(id_b),
Air::CloseExpr(S6),
];
let mut sut = Sut::parse(toks.into_iter());
assert!(sut.all(|x| x.is_ok()));
let asg = sut.finalize().unwrap().into_context();
let expr_a = asg.expect_ident_obj::<Expr>(id_a);
assert_eq!(expr_a.span(), S1.merge(S6).unwrap());
// Identifiers should reference the same expression.
let expr_b = asg.expect_ident_obj::<Expr>(id_b);
assert_eq!(expr_a, expr_b);
}
// Binding an identifier after a child expression means that the parser is
// creating an expression that is a child of a dangling expression,
// which only becomes reachable at the end.
#[test]
fn expr_non_empty_bind_only_after() {
let id = SPair("foo".into(), S2);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::OpenExpr(ExprOp::Sum, S2),
Air::CloseExpr(S3),
// We only bind an identifier _after_ we've created the expression,
// which should cause the still-dangling root to become
// reachable.
Air::IdentExpr(id),
Air::CloseExpr(S5),
];
let mut sut = Sut::parse(toks.into_iter());
assert!(sut.all(|x| x.is_ok()));
let asg = sut.finalize().unwrap().into_context();
let expr = asg.expect_ident_obj::<Expr>(id);
assert_eq!(expr.span(), S1.merge(S5).unwrap());
}
// Danging expressions are unreachable and therefore not useful
@ -281,7 +337,7 @@ fn expr_empty() {
// Prohibit them,
// since they're either mistakes or misconceptions.
#[test]
fn expr_dangling() {
fn expr_dangling_no_subexpr() {
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// No `IdentExpr`,
@ -290,7 +346,6 @@ fn expr_dangling() {
];
// 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!(
@ -300,7 +355,144 @@ fn expr_dangling() {
],
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
}
#[test]
fn expr_dangling_with_subexpr() {
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::OpenExpr(ExprOp::Sum, S2),
Air::CloseExpr(S3),
// Still no ident binding,
// so root should still be dangling.
Air::CloseExpr(S4),
];
let full_span = S1.merge(S4).unwrap();
assert_eq!(
vec![
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Err(ParseError::StateError(AsgError::DanglingExpr(full_span)))
],
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
);
}
#[test]
fn expr_dangling_with_subexpr_ident() {
let id = SPair("foo".into(), S3);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::OpenExpr(ExprOp::Sum, S2),
// The _inner_ expression receives an identifier,
// but that should have no impact on the dangling status of the
// root,
// especially given that subexpressions are always reachable
// anyway.
Air::IdentExpr(id),
Air::CloseExpr(S4),
// But the root still has no ident binding,
// and so should still be dangling.
Air::CloseExpr(S5),
];
let full_span = S1.merge(S5).unwrap();
assert_eq!(
vec![
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Err(ParseError::StateError(AsgError::DanglingExpr(full_span)))
],
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
);
}
// Ensure that the parser correctly recognizes dangling expressions after
// having encountered a reachable expression.
// Ideally the parser will have been written to make this impossible,
// but this also protects against potential future breakages.
#[test]
fn expr_reachable_subsequent_dangling() {
let id = SPair("foo".into(), S2);
let toks = vec![
// Reachable
Air::OpenExpr(ExprOp::Sum, S1),
Air::IdentExpr(id),
Air::CloseExpr(S3),
// Dangling
Air::OpenExpr(ExprOp::Sum, S4),
Air::CloseExpr(S5),
];
// The error span should encompass the entire expression.
// TODO: ...let's actually have something inside this expression.
let second_span = S4.merge(S5).unwrap();
assert_eq!(
vec![
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Err(ParseError::StateError(AsgError::DanglingExpr(second_span)))
],
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
);
}
// Recovery from dangling expression.
#[test]
fn recovery_expr_reachable_after_dangling() {
let id = SPair("foo".into(), S4);
let toks = vec![
// Dangling
Air::OpenExpr(ExprOp::Sum, S1),
Air::CloseExpr(S2),
// Reachable, after error from dangling.
Air::OpenExpr(ExprOp::Sum, S3),
Air::IdentExpr(id),
Air::CloseExpr(S5),
];
// The error span should encompass the entire expression.
let err_span = S1.merge(S2).unwrap();
let mut sut = Sut::parse(toks.into_iter());
assert_eq!(
vec![
Ok(Parsed::Incomplete),
Err(ParseError::StateError(AsgError::DanglingExpr(err_span))),
// Recovery allows us to continue at this point with the next
// expression.
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
Ok(Parsed::Incomplete),
],
sut.by_ref().collect::<Vec<_>>(),
);
let asg = sut.finalize().unwrap().into_context();
// Let's make sure that we _actually_ added it to the graph,
// despite the previous error.
let expr = asg.expect_ident_obj::<Expr>(id);
assert_eq!(expr.span(), S3.merge(S5).unwrap());
// The dangling expression may or may not be on the graph,
// but it doesn't matter;
// we cannot reference it
// (unless we break abstraction and walk the underlying graph).
// Let's leave this undefined so that we have flexibility in what we
// decide to do in the future.
// So we end here.
}

View File

@ -24,7 +24,7 @@ use super::{
Source, TransitionResult,
};
use crate::diagnose::panic::DiagnosticPanic;
use crate::diagnose::Annotate;
use crate::diagnose::{Annotate, AnnotatedSpan};
use crate::f::Functor;
use crate::global;
use crate::parse::util::SPair;
@ -436,19 +436,7 @@ impl Asg {
) -> ObjectIndex<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 ObjectIndex 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."),
],
diagnostic_node_missing_desc(index),
"invalid ObjectIndex: data are missing from the ASG",
);
@ -458,22 +446,7 @@ impl Asg {
// 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.",
),
],
diagnostic_borrowed_node_desc(index),
"inaccessible ObjectIndex: object has not been returned to the ASG",
);
@ -487,42 +460,26 @@ impl Asg {
index.overwrite(new_span)
}
/// Map over an inner object that is referenced by an identifier.
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
/// if any.
///
/// This uses [`Self::mut_map_obj`];
/// see that method for more information,
/// especially as it relates to panics.
/// The type parameter `O` indicates the _expected_ [`ObjectKind`] to be
/// bound to the returned [`ObjectIndex`],
/// which will be used for narrowing (downcasting) the object after
/// lookup.
/// An incorrect kind will not cause any failures until such a lookup
/// occurs.
///
/// _This method is intended to be used when the system expects the
/// identifier must exist on the graph and be associated with an
/// object._
/// This panic-happy method is dangerous if used sloppily,
/// so it is currently available only for ASG tests.
/// If production code is to make use of this concept,
/// it should either first ensure the identifier exists and retrieve
/// it by [`ObjectIndex`],
/// or this method should be modified to be able to return lookup
/// errors.
///
/// Panics
/// ======
/// In addition to the reasons listed in [`Self::mut_map_obj`],
/// this will also panic if:
///
/// 1. The identifier does not exist on the graph;
/// 2. The identifier is opaque (has no edge to any object on the
/// graph).
/// This will return [`None`] if the identifier is either opaque or does
/// not exist.
#[cfg(test)]
pub(super) fn mut_map_obj_by_ident<O: ObjectKind>(
&mut self,
fn get_ident_obj_oi<O: ObjectKind>(
&self,
ident: SPair,
f: impl FnOnce(O) -> O,
) {
use crate::fmt::{DisplayWrapper, TtQuote};
) -> Option<ObjectIndex<O>> {
use petgraph::Direction;
let oi = self
.lookup(ident.symbol())
self.lookup(ident.symbol())
.and_then(|identi| {
self.graph
.neighbors_directed(identi.into(), Direction::Outgoing)
@ -533,30 +490,49 @@ impl Asg {
// the type will be verified during narrowing but will panic
// if this expectation is not met.
.map(|ni| ObjectIndex::<O>::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),
),
}
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
/// panicing if the identifier is opaque or does not exist.
///
/// See [`Self::get_ident_obj_oi`] for more information;
/// this simply panics if its result is [`None`].
///
/// Panics
/// ======
/// 1. The identifier does not exist on the graph;
/// 2. The identifier is opaque (has no edge to any object on the
/// graph).
#[cfg(test)]
fn expect_ident_obj_oi<O: ObjectKind>(
&self,
ident: SPair,
) -> ObjectIndex<O> {
use crate::fmt::{DisplayWrapper, TtQuote};
self.get_ident_obj_oi(ident).diagnostic_expect(
diagnostic_opaque_ident_desc(ident),
&format!(
"opaque identifier: {} has no object binding",
TtQuote::wrap(ident),
),
)
}
#[cfg(test)]
pub(super) fn expect_ident_obj<O: ObjectKind>(&self, ident: SPair) -> &O {
let oi = self.expect_ident_obj_oi::<O>(ident);
let obj_container =
self.graph.node_weight(oi.into()).diagnostic_expect(
diagnostic_node_missing_desc(oi),
"invalid ObjectIndex: data are missing from the ASG",
);
// We do not care about the updated ObjectIndex from `mut_map_obj`
// since it was ephemeral for this operation.
let _ = self.mut_map_obj(oi, f);
obj_container.as_ref().diagnostic_expect(
diagnostic_borrowed_node_desc(oi),
"inaccessible ObjectIndex: object has not been returned to the ASG",
).into()
}
/// Retrieve an identifier from the graph by [`ObjectIndex`].
@ -640,6 +616,48 @@ impl Asg {
}
}
fn diagnostic_node_missing_desc<O: ObjectKind>(
index: ObjectIndex<O>,
) -> Vec<AnnotatedSpan<'static>> {
vec![
index.internal_error("this object is missing from the ASG"),
index.help("this means that either an ObjectIndex 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."),
]
}
fn diagnostic_borrowed_node_desc<O: ObjectKind>(
index: ObjectIndex<O>,
) -> Vec<AnnotatedSpan<'static>> {
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.",
),
]
}
#[cfg(test)]
fn diagnostic_opaque_ident_desc(ident: SPair) -> Vec<AnnotatedSpan<'static>> {
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."),
]
}
#[cfg(test)]
mod test {
use super::super::error::AsgError;