tamer: asg: Bind transparent ident

This provides the initial implementation allowing an identifier to be
defined (bound to an object and made transparent).

I'm not yet entirely sure whether I'll stick with the "transparent" and
"opaque" terminology when there's also "declare" and "define", but a
`Missing` state is a type of declaration and so the distinction does still
seem to be important.

There is still work to be done on `ObjectIndex::<Ident>::bind_definition`,
which will follow.  I'm going to be balancing work to provide type-level
guarantees, since I don't have the time to go as far as I'd like.

DEV-13597
main
Mike Gerwitz 2023-01-17 16:31:13 -05:00
parent 378fe3db66
commit 4e3a81d7f5
7 changed files with 324 additions and 32 deletions

View File

@ -276,7 +276,9 @@ impl Display for StackEdge {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Dangling => write!(f, "dangling"),
Self::Reachable(ident) => write!(f, "reachable (by {ident})"),
Self::Reachable(ident) => {
write!(f, "reachable (by {})", TtQuote::wrap(ident))
}
}
}
}
@ -454,7 +456,7 @@ impl ParseState for AirAggregate {
(BuildingExpr(es, oi), CloseExpr(end)) => {
let start: Span = oi.into();
let _ = asg.mut_map_obj::<Expr>(oi, |expr| {
let _ = oi.map_obj(asg, |expr| {
expr.map(|span| span.merge(end).unwrap_or(span))
});
@ -482,11 +484,15 @@ impl ParseState for AirAggregate {
}
(BuildingExpr(es, oi), IdentExpr(id)) => {
// TODO: error on existing ident
let identi = asg.lookup_or_missing(id);
asg.add_dep(identi, oi);
Transition(BuildingExpr(es.reachable_by(id), oi)).incomplete()
// It is important that we do not mark this expression as
// reachable unless we successfully bind the identifier.
match identi.bind_definition(asg, oi) {
Ok(_) => Transition(BuildingExpr(es.reachable_by(id), oi))
.incomplete(),
Err(e) => Transition(BuildingExpr(es, oi)).err(e),
}
}
(st @ Empty(_), IdentDecl(name, kind, src)) => {

View File

@ -657,6 +657,130 @@ fn nested_subexprs_related_to_relative_parent() {
assert_eq!(subexpr_2.span(), S3.merge(S4).unwrap());
}
#[test]
fn expr_redefine_ident() {
// Same identifier but with different spans
// (which would be the case in the real world).
let id_first = SPair("foo".into(), S2);
let id_dup = SPair("foo".into(), S3);
let toks = vec![
Air::OpenExpr(ExprOp::Sum, S1),
Air::IdentExpr(id_first),
Air::OpenExpr(ExprOp::Sum, S3),
Air::IdentExpr(id_dup),
Air::CloseExpr(S4),
Air::CloseExpr(S5),
];
let mut sut = Sut::parse(toks.into_iter());
assert_eq!(
vec![
Ok(Parsed::Incomplete), // OpenExpr
Ok(Parsed::Incomplete), // IdentExpr (first)
Ok(Parsed::Incomplete), // OpenExpr
Err(ParseError::StateError(AsgError::IdentRedefine(
id_first,
id_dup.span(),
))),
// RECOVERY: Ignore the attempt to redefine and continue.
Ok(Parsed::Incomplete), // CloseExpr
Ok(Parsed::Incomplete), // CloseExpr
],
sut.by_ref().collect::<Vec<_>>(),
);
let asg = sut.finalize().unwrap().into_context();
// The identifier should continue to reference the first expression.
let expr = asg.expect_ident_obj::<Expr>(id_first);
assert_eq!(expr.span(), S1.merge(S5).unwrap());
}
// Similar to the above test,
// but with two entirely separate expressions,
// such that a failure to identify an expression ought to leave it in an
// unreachable state.
#[test]
fn expr_still_dangling_on_redefine() {
// Same identifier but with different spans
// (which would be the case in the real world).
let id_first = SPair("foo".into(), S2);
let id_dup = SPair("foo".into(), S5);
let id_dup2 = SPair("foo".into(), S8);
let id_second = SPair("bar".into(), S9);
let toks = vec![
// First expr (OK)
Air::OpenExpr(ExprOp::Sum, S1),
Air::IdentExpr(id_first),
Air::CloseExpr(S3),
// Second expr should still dangle due to use of duplicate
// identifier
Air::OpenExpr(ExprOp::Sum, S4),
Air::IdentExpr(id_dup),
Air::CloseExpr(S6),
// Third expr will error on redefine but then be successful.
// This probably won't happen in practice with TAME's original
// source language,
// but could happen at e.g. a REPL.
Air::OpenExpr(ExprOp::Sum, S7),
Air::IdentExpr(id_dup2), // fail
Air::IdentExpr(id_second), // succeed
Air::CloseExpr(S10),
];
let mut sut = Sut::parse(toks.into_iter());
assert_eq!(
vec![
Ok(Parsed::Incomplete), // OpenExpr
Ok(Parsed::Incomplete), // IdentExpr (first)
Ok(Parsed::Incomplete), // CloseExpr
// Beginning of second expression
Ok(Parsed::Incomplete), // OpenExpr
Err(ParseError::StateError(AsgError::IdentRedefine(
id_first,
id_dup.span(),
))),
// RECOVERY: Ignore the attempt to redefine and continue.
// ...but then immediately fail _again_ because we've closed a
// dangling expression.
Err(ParseError::StateError(AsgError::DanglingExpr(
S4.merge(S6).unwrap()
))),
// RECOVERY: But we'll continue onto one final expression,
// which we will fail to define but then subsequently define
// successfully.
Ok(Parsed::Incomplete), // OpenExpr
Err(ParseError::StateError(AsgError::IdentRedefine(
id_first,
id_dup2.span(),
))),
// RECOVERY: Despite the initial failure,
// we can now re-attempt to bind with a unique id.
Ok(Parsed::Incomplete), // IdentExpr (second)
Ok(Parsed::Incomplete), // CloseExpr
],
sut.by_ref().collect::<Vec<_>>(),
);
let asg = sut.finalize().unwrap().into_context();
// The identifier should continue to reference the first expression.
let expr = asg.expect_ident_obj::<Expr>(id_first);
assert_eq!(expr.span(), S1.merge(S3).unwrap());
// There's nothing we can do using the ASG's public API at the time of
// writing to try to reference the dangling expression.
// The second identifier should have been successfully bound despite the
// failed initial attempt.
let expr = asg.expect_ident_obj::<Expr>(id_second);
assert_eq!(expr.span(), S7.merge(S10).unwrap());
}
fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
where
I::IntoIter: Debug,

View File

@ -26,6 +26,7 @@ use std::{
use crate::{
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
fmt::{DisplayWrapper, TtQuote},
parse::util::SPair,
span::Span,
};
@ -38,6 +39,18 @@ pub enum AsgError {
/// An object could not change state in the manner requested.
IdentTransition(TransitionError),
/// An identifier was already bound to some object,
/// and an attempt was made to bind it to a different one.
///
/// This includes an [`SPair`] representing the _original_ definition
/// that was already accepted by the system and a [`Span`]
/// representing the _duplicate_ definition that triggered this error.
///
/// Note that this is different than a _redeclaration_;
/// _defining_ an identifier associates it with an object,
/// whereas _declaring_ an identifier provides metadata about it.
IdentRedefine(SPair, Span),
/// An expresion is not reachable by any other expression or
/// identifier.
///
@ -78,6 +91,9 @@ impl Display for AsgError {
match self {
IdentTransition(err) => Display::fmt(&err, f),
IdentRedefine(spair, _) => {
write!(f, "cannot redefine {}", TtQuote::wrap(spair))
}
DanglingExpr(_) => write!(f, "dangling expression"),
UnbalancedExpr(_) => write!(f, "unbalanced expression"),
InvalidExprBindContext(_) => {
@ -93,9 +109,10 @@ impl Error for AsgError {
match self {
IdentTransition(err) => err.source(),
DanglingExpr(_) | UnbalancedExpr(_) | InvalidExprBindContext(_) => {
None
}
IdentRedefine(_, _)
| DanglingExpr(_)
| UnbalancedExpr(_)
| InvalidExprBindContext(_) => None,
}
}
}
@ -118,6 +135,23 @@ impl Diagnostic for AsgError {
// TODO: need spans
IdentTransition(_) => vec![],
IdentRedefine(first, span_redecl) => vec![
first.note(format!(
"first definition of {} is here",
TtQuote::wrap(first)
)),
span_redecl.error(format!(
"attempted to redefine {} here",
TtQuote::wrap(first),
)),
span_redecl.help(format!(
"variables in TAME are immutable; {} was previously",
TtQuote::wrap(first),
)),
span_redecl
.help(" defined and its definition cannot be changed."),
],
DanglingExpr(span) => vec![
span.error("expression has no parent or identifier"),
span.help("an expression must either be the child of another "),

View File

@ -186,6 +186,12 @@ impl Asg {
/// The provided span is necessary to seed the missing identifier with
/// some sort of context to aid in debugging why a missing identifier
/// was introduced to the graph.
/// The provided span will be used even if an identifier exists on the
/// graph,
/// which can be used for retaining information on the location that
/// requested the identifier.
/// To retrieve the span of a previously declared identifier,
/// you must resolve the [`Ident`] object and inspect it.
///
/// See [`Ident::declare`] for more information.
pub(super) fn lookup_or_missing(
@ -403,7 +409,8 @@ impl Asg {
.map(ObjectContainer::get)
}
/// Map over an inner [`Object`] referenced by [`ObjectIndex`].
/// Attempt to map over an inner [`Object`] referenced by
/// [`ObjectIndex`].
///
/// The type `O` is the expected type of the [`Object`],
/// which should be known to the caller based on the provied
@ -413,11 +420,6 @@ impl Asg {
/// 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
@ -433,20 +435,20 @@ impl Asg {
/// representing a type mismatch between what the caller thinks
/// this object represents and what the object actually is.
#[must_use = "returned ObjectIndex has a possibly-updated and more relevant span"]
pub fn mut_map_obj<O: ObjectKind>(
pub(super) fn try_map_obj<O: ObjectKind, E>(
&mut self,
index: ObjectIndex<O>,
f: impl FnOnce(O) -> O,
) -> ObjectIndex<O> {
f: impl FnOnce(O) -> Result<O, (O, E)>,
) -> Result<ObjectIndex<O>, E> {
let obj_container =
self.graph.node_weight_mut(index.into()).diagnostic_expect(
|| diagnostic_node_missing_desc(index),
"invalid ObjectIndex: data are missing from the ASG",
);
obj_container.replace_with(f);
index.overwrite(obj_container.get::<Object>().span())
obj_container
.try_replace_with(f)
.map(|()| index.overwrite(obj_container.get::<Object>().span()))
}
/// Create an iterator over the [`ObjectIndex`]es of the outgoing edges
@ -704,7 +706,7 @@ mod test {
use super::super::error::AsgError;
use super::*;
use crate::{num::Dim, span::dummy::*, sym::GlobalSymbolIntern};
use std::assert_matches::assert_matches;
use std::{assert_matches::assert_matches, convert::Infallible};
type Sut = Asg;
@ -1049,7 +1051,7 @@ mod test {
}
#[test]
fn mut_map_narrows_and_modifies() {
fn try_map_narrows_and_modifies() {
let mut sut = Sut::new();
let id_a = SPair("foo".into(), S1);
@ -1060,12 +1062,14 @@ mod test {
// This is the method under test.
// It should narrow to an `Ident` because `oi` was `create`'d with
// an `Ident`.
let oi_new = sut.mut_map_obj(oi, |ident| {
assert_eq!(ident, Ident::Missing(id_a));
let oi_new = sut
.try_map_obj(oi, |ident| {
assert_eq!(ident, Ident::Missing(id_a));
// Replace the identifier
Ident::Missing(id_b)
});
// Replace the identifier
Ok::<_, (_, Infallible)>(Ident::Missing(id_b))
})
.unwrap();
// These would not typically be checked by the caller;
// they are intended for debugging.
@ -1077,6 +1081,29 @@ mod test {
// Ensure that the graph was updated with the new object from the
// above method.
assert_eq!(&Ident::Missing(id_b), sut.get(oi).unwrap(),);
assert_eq!(&Ident::Missing(id_b), sut.get(oi).unwrap());
}
#[test]
fn try_map_failure_restores_original_object() {
let mut sut = Sut::new();
let id_a = SPair("foo".into(), S1);
let err = "uh oh";
let oi = sut.create(Ident::Missing(id_a));
// This will fail to modify the object.
let oi_new = sut.try_map_obj(oi, |ident| {
assert_eq!(ident, Ident::Missing(id_a));
Err((ident, err))
});
assert_eq!(Err(err), oi_new);
// Ensure that the original object was retained.
assert_eq!(&Ident::Missing(id_a), sut.get(oi).unwrap());
}
}

View File

@ -19,8 +19,7 @@
//! Identifiers (a type of [object][super::object]).
use std::fmt::Display;
use super::{object::ObjectRelTo, Asg, AsgError, ObjectIndex, ObjectKind};
use crate::{
diagnose::{Annotate, Diagnostic},
f::Functor,
@ -30,14 +29,23 @@ use crate::{
span::Span,
sym::{st, GlobalSymbolResolve, SymbolId},
};
use std::fmt::Display;
use Ident::*;
pub type TransitionResult<T> = Result<T, (T, TransitionError)>;
/// Identifier.
/// An identifier for some object on the [`Asg`].
///
/// These types represent object states:
/// An identifier can be either _opaque_ declaration,
/// meaning that it stands in _place_ of a definition,
/// or _transparent_,
/// meaning that references to the identifier should "see through" it
/// and directly reference the object to which it is bound.
///
/// Invariants
/// ==========
/// The [`Ident`] variants represent object states:
///
/// ```text
/// ,--> ((Transparent))
@ -47,6 +55,23 @@ pub type TransitionResult<T> = Result<T, (T, TransitionError)>;
/// \ / \ /
/// `--------------------` `------------'
/// ```
///
/// The following invariants must hold with respect to [`Asg`]:
/// 1. A [`Transparent`] identifier must have _one_ edge to the object
/// that it describes
/// (upheld by [`ObjectIndex::<Ident>::bind_definition`]).
/// 2. All other identifiers must have _zero_ outgoing edges,
/// since they describe nothing available on the graph.
/// Since edges can only be added by
/// [`ObjectIndex::<Ident>::bind_definition`],
/// and since an [`Ident`] cannot transition away from
/// [`Transparent`],
/// this invariant is upheld.
/// 3. There must be _zero_ incoming edges to [`Transparent`].
/// When an identifier transitions to [`Transparent`],
/// incoming edges must be rewritten to the object to which the
/// identifier has become bound.
/// This is handled by [`ObjectIndex::<Ident>::bind_definition`].
#[derive(Debug, PartialEq, Clone)]
pub enum Ident {
/// An identifier is expected to be declared or defined but is not yet
@ -942,5 +967,42 @@ pub struct Source {
pub override_: bool,
}
impl ObjectIndex<Ident> {
/// Bind an identifier to a `definition`,
/// making it [`Transparent`].
///
/// If an identifier is successfully bound,
/// then an edge will be added to `definition`.
/// An edge will _not_ be added if there is an error in this operation.
///
/// If an identifier is already [`Transparent`],
/// then it is already defined and this operation will result in an
/// [`AsgError::IdentRedefine`] error.
pub fn bind_definition<O: ObjectKind>(
self,
asg: &mut Asg,
definition: ObjectIndex<O>,
) -> Result<ObjectIndex<Ident>, AsgError>
where
Ident: ObjectRelTo<O>,
{
let my_span = self.into();
// TODO: Move all incoming edges to `definition`
self.try_map_obj(asg, |ident| match ident {
Transparent(id) => {
Err((ident, AsgError::IdentRedefine(id, my_span)))
}
Opaque(id, ..) | IdentFragment(id, ..) => todo!("opaque {id}"),
Extern(id, ..) => todo!("extern {id}"),
// We are okay to proceed to add an edge to the `definition`.
Missing(id) => Ok(Transparent(id)),
})
.map(|ident_oi| ident_oi.add_edge_to(asg, definition))
}
}
#[cfg(test)]
mod test;

View File

@ -338,6 +338,44 @@ impl<O: ObjectKind> ObjectIndex<O> {
pub fn resolve(self, asg: &Asg) -> &O {
asg.expect_obj(self)
}
/// Resolve the identifier and map over the resulting [`Object`]
/// narrowed to [`ObjectKind`] `O`,
/// replacing the object on the given [`Asg`].
///
/// While the provided map may be pure,
/// this does mutate the provided [`Asg`].
///
/// If the operation fails,
/// `f` is expected to provide an object
/// (such as the original)
/// to return to the graph.
///
/// If this operation is [`Infallible`],
/// see [`Self::map_obj`].
pub fn try_map_obj<E>(
self,
asg: &mut Asg,
f: impl FnOnce(O) -> Result<O, (O, E)>,
) -> Result<Self, E> {
asg.try_map_obj(self, f)
}
/// Resolve the identifier and infallibly map over the resulting
/// [`Object`] narrowed to [`ObjectKind`] `O`,
/// replacing the object on the given [`Asg`].
///
/// If this operation is _not_ [`Infallible`],
/// see [`Self::try_map_obj`].
pub fn map_obj(self, asg: &mut Asg, f: impl FnOnce(O) -> O) -> Self {
// This verbose notation (in place of e.g. `unwrap`) is intentional
// to emphasize why it's unreachable and to verify our assumptions
// at every point.
match self.try_map_obj::<Infallible>(asg, |o| Ok(f(o))) {
Ok(oi) => oi,
Err::<_, Infallible>(_) => unreachable!(),
}
}
}
impl ObjectIndex<Object> {

View File

@ -715,6 +715,7 @@ pub mod dummy {
pub const S7: Span = S0.offset_add(7).unwrap();
pub const S8: Span = S0.offset_add(8).unwrap();
pub const S9: Span = S0.offset_add(9).unwrap();
pub const S10: Span = S0.offset_add(10).unwrap();
}
#[cfg(test)]