tamer: asg::air::expr: Infer concrete/abstract state from child expressions
See included documentation for more information. This completes `MetaState` inference, for now, until we are able to be notified when missing identifiers acquire bindings. DEV-13163main
parent
ad9b6d1582
commit
7001b50543
|
@ -92,12 +92,8 @@ impl ParseState for AirExprAggregate {
|
|||
}
|
||||
|
||||
(BuildingExpr(es, poi), AirExpr(ExprStart(op, span))) => {
|
||||
match poi.create_subexpr(ctx.asg_mut(), Expr::new(op, span)) {
|
||||
Ok(oi) => {
|
||||
Transition(BuildingExpr(es.push(poi), oi)).incomplete()
|
||||
}
|
||||
Err(e) => Transition(BuildingExpr(es, poi)).err(e),
|
||||
}
|
||||
let oi_child = ctx.asg_mut().create(Expr::new(op, span));
|
||||
Transition(BuildingExpr(es.push(poi), oi_child)).incomplete()
|
||||
}
|
||||
|
||||
(BuildingExpr(es, oi), AirExpr(ExprEnd(end))) => {
|
||||
|
@ -107,13 +103,19 @@ impl ParseState for AirExprAggregate {
|
|||
let oi_root = ctx.dangling_expr_oi();
|
||||
|
||||
match (es.pop(), dangling) {
|
||||
((es, Some(poi)), _) => {
|
||||
Transition(BuildingExpr(es, poi)).incomplete()
|
||||
}
|
||||
// This was a child expression
|
||||
((es, Some(poi)), _) => poi
|
||||
.add_completed_subexpr(ctx.asg_mut(), oi)
|
||||
.map(|_| ())
|
||||
.transition(BuildingExpr(es, poi)),
|
||||
|
||||
// Topmost expression, dangling.
|
||||
((es, None), true) => {
|
||||
Self::hold_dangling(ctx.asg_mut(), oi_root, oi)
|
||||
.transition(Ready(es.done()))
|
||||
}
|
||||
|
||||
// Topmost expression, reachable.
|
||||
((es, None), false) => {
|
||||
Transition(Ready(es.done())).incomplete()
|
||||
}
|
||||
|
|
|
@ -47,10 +47,15 @@ pub fn collect_subexprs(
|
|||
asg: &Asg,
|
||||
oi: ObjectIndex<Expr>,
|
||||
) -> Vec<(ObjectIndex<Expr>, &Expr)> {
|
||||
oi.edges(&asg)
|
||||
let mut exprs = oi
|
||||
.edges(&asg)
|
||||
.filter_map(|rel| rel.narrow::<Expr>())
|
||||
.map(|oi| (oi, oi.resolve(&asg)))
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Edge order is reversed.
|
||||
exprs.reverse();
|
||||
exprs
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -931,7 +936,7 @@ fn expr_referencing_missing_without_abstract_is_unknown() {
|
|||
// but throw in a known reference to show that it doesn't override the
|
||||
// missing one.
|
||||
#[test]
|
||||
fn expr_referencing_missing_with_concrete_without_abstract_is_unknown() {
|
||||
fn expr_referencing_missing_with_concrete_without_abstract_is_maybe_concrete() {
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
ExprStart(ExprOp::Sum, S1),
|
||||
|
@ -959,3 +964,73 @@ fn expr_referencing_missing_with_concrete_without_abstract_is_unknown() {
|
|||
// so only one is missing.
|
||||
assert_eq!(expr.meta_state(), MetaState::MaybeConcrete(1.unwrap_into()));
|
||||
}
|
||||
|
||||
// If any child expression is not known to be concrete,
|
||||
// then we cannot reasonably call the parent expression concrete either,
|
||||
// because we must decend into the parent's tree in order to expand
|
||||
// children.
|
||||
#[test]
|
||||
fn expr_is_maybe_concrete_if_any_child_is_maybe_concrete() {
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
ExprStart(ExprOp::Sum, S1), // A = 2 // ----------------.
|
||||
BindIdent(spair("expr", S2)), // |
|
||||
// |
|
||||
// This should be MaybeConcrete because // |
|
||||
// there is an inner missing reference. // |
|
||||
ExprStart(ExprOp::Sum, S3), // B = 1 // ---------. <-: A1
|
||||
ExprStart(ExprOp::Sum, S4), // C = 2 // --. <-' B1 |
|
||||
// A couple children deep to make sure // | |
|
||||
// this property is inherited by all // | |
|
||||
// ancestors (B, A). // | |
|
||||
RefIdent(spair("missing", S5)), // <-: C1 |
|
||||
// | |
|
||||
// A second missing reference. // | |
|
||||
// Even though _this_ expression has two, // | |
|
||||
// we want the parent B to see only a // | |
|
||||
// single missing edge to C. // | |
|
||||
RefIdent(spair("missing", S6)), // <-' C2 |
|
||||
ExprEnd(S6), // |
|
||||
ExprEnd(S7), // |
|
||||
// |
|
||||
// Concrete, // |
|
||||
// to make sure our count isn't affected. // |
|
||||
ExprStart(ExprOp::Sum, S8), // |
|
||||
ExprEnd(S9), // |
|
||||
// |
|
||||
// A second abstract child for A. // |
|
||||
ExprStart(ExprOp::Sum, S10), // D = 1 // --. <--------' A2
|
||||
RefIdent(spair("missing", S11)), // <-' D1
|
||||
ExprEnd(S12),
|
||||
ExprEnd(S13),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_a = pkg_expect_ident_oi::<Expr>(&ctx, spair("expr", S20));
|
||||
let a = oi_a.resolve(&asg);
|
||||
|
||||
let ae = collect_subexprs(asg, oi_a);
|
||||
assert_eq!(ae.len(), 3); // abstract, concrete, abstract
|
||||
let (oi_b, b) = ae[0];
|
||||
let (_, d) = ae[2];
|
||||
|
||||
// Sanity check to make sure we have the exprs that we're expecting.
|
||||
assert_eq!(S3.merge(S7).unwrap(), b.span());
|
||||
assert_eq!(S10.merge(S12).unwrap(), d.span());
|
||||
|
||||
let be = collect_subexprs(asg, oi_b);
|
||||
assert_eq!(be.len(), 1);
|
||||
let (_, c) = be[0];
|
||||
|
||||
// We'll assert according to the visualization above,
|
||||
// from inside out.
|
||||
// Asserting individually helps to guide debugging with a narrow focus,
|
||||
// and we want to fail on inner exprs first since effects compose
|
||||
// outward.
|
||||
assert_eq!(d.meta_state(), MetaState::MaybeConcrete(1.unwrap_into()));
|
||||
assert_eq!(c.meta_state(), MetaState::MaybeConcrete(2.unwrap_into()));
|
||||
assert_eq!(b.meta_state(), MetaState::MaybeConcrete(1.unwrap_into()));
|
||||
assert_eq!(a.meta_state(), MetaState::MaybeConcrete(2.unwrap_into()));
|
||||
}
|
||||
|
|
|
@ -161,7 +161,6 @@ fn tpl_within_expr() {
|
|||
collect_subexprs(&asg, oi_expr)
|
||||
.iter()
|
||||
.map(|(_, expr)| expr.span())
|
||||
.rev()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,50 @@
|
|||
//! so expressions both naturally compose and are able to be replaced with
|
||||
//! the value that they represent without affecting the meaning of the
|
||||
//! program.
|
||||
//!
|
||||
//! An expression is [_concrete_](`MetaState::Concrete`) if it requires no
|
||||
//! expansion by the template system.
|
||||
//! If an expression or any of its children reference any
|
||||
//! [metavariables](super::Meta)
|
||||
//! (template parameters),
|
||||
//! then the expression will be [_abstract_](MetaState::Abstract).
|
||||
//! Expressions' static bindings together with their referential
|
||||
//! transparency means that a concrete expression is able to be moved and
|
||||
//! copied to any other point in the program without changing its
|
||||
//! meaning;
|
||||
//! this includes the act of copying via template expansion.
|
||||
//!
|
||||
//! A _reference_ to another expression does not have any influence over
|
||||
//! whether an expression is abstract or not.
|
||||
//! In graph terms:
|
||||
//! tree edges influence an expression's [`MetaState`],
|
||||
//! but not cross edges.
|
||||
//! Consider the following expression in XML notation to help with intuition
|
||||
//! on this.
|
||||
//! Assume that this is the body of some template with a single template
|
||||
//! parameter identified as `@foo@`:
|
||||
//!
|
||||
//! ```xml
|
||||
//! <!-- there are no metavariable references, so this is concrete -->
|
||||
//! <c:sum id="conc">
|
||||
//! <c:value-of name="#5" />
|
||||
//! </c:sum>
|
||||
//!
|
||||
//! <!-- this is abstract because it requires expansion -->
|
||||
//! <c:sum id="abstract">
|
||||
//! <c:value-of name="@foo@" />
|
||||
//! </c:sum>
|
||||
//!
|
||||
//! <!-- this is concrete... -->
|
||||
//! <c:sum id="combine">
|
||||
//! <c:value-of name="conc" />
|
||||
//! <!-- ...even though `abstract` is abstract, because moving or
|
||||
//! copying `combine` would have no effect on the meaning of the
|
||||
//! the expression, and there is nothing to expand via the template
|
||||
//! system -->
|
||||
//! <c:value-of name="abstract" />
|
||||
//! </c:sum>
|
||||
//! ```
|
||||
|
||||
use super::{
|
||||
ident::IdentDefinition, prelude::*, Doc, Ident, ObjectIndexToTree, Tpl,
|
||||
|
@ -44,6 +88,8 @@ use super::ObjectKind;
|
|||
/// The [`Span`] of an expression should be expanded to encompass not only
|
||||
/// all child expressions,
|
||||
/// but also any applicable closing span.
|
||||
///
|
||||
/// See the [parent module](self) for more information.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Expr {
|
||||
op: ExprOp,
|
||||
|
@ -348,6 +394,20 @@ impl MetaState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine how a child expression should impact whether this
|
||||
/// expression is abstract.
|
||||
fn observe_child(self, state: MetaState, span: Span) -> Self {
|
||||
match state {
|
||||
Self::Concrete => self,
|
||||
Self::Abstract => self.found_abstract(),
|
||||
|
||||
// Since we track the number of missing edges to direct children,
|
||||
// we treat the child subgraph as if it were a single node on
|
||||
// the graph.
|
||||
Self::MaybeConcrete(_) => self.found_missing(span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MetaState {
|
||||
|
@ -384,10 +444,27 @@ object_rel! {
|
|||
);
|
||||
},
|
||||
|
||||
// Non-meta identifiers are just references.
|
||||
// We don't care what they are as long as they're not
|
||||
// metavariables.
|
||||
Some(IdentDefinition::Expr(_) | IdentDefinition::Tpl(_)) => (),
|
||||
// This is a _reference_ to another expression tree.
|
||||
// Only tree edges influence our abstract status.
|
||||
Some(IdentDefinition::Expr(_)) => (),
|
||||
|
||||
// Unlike the XSLT-based TAME,
|
||||
// this reference can act as a template application,
|
||||
// just as the `tree Tpl` edge below.
|
||||
// TODO: We can expand closed expr templates here,
|
||||
// since it's no different than referencing the inner
|
||||
// expression.
|
||||
Some(IdentDefinition::Tpl(_)) => diagnostic_todo!(
|
||||
vec![
|
||||
rel.to_oi.error("this references a template"),
|
||||
rel.to_oi.help(
|
||||
"only closed expression templates will be \
|
||||
supported in this context"
|
||||
)
|
||||
],
|
||||
"template references in an expression context are
|
||||
not yet supported"
|
||||
),
|
||||
|
||||
None => {
|
||||
rel.from_oi.map_obj_inner(asg, |meta: MetaState| {
|
||||
|
@ -404,10 +481,28 @@ object_rel! {
|
|||
}
|
||||
},
|
||||
|
||||
tree Expr,
|
||||
tree Doc,
|
||||
tree Expr {
|
||||
fn pre_add_edge(
|
||||
asg: &mut Asg,
|
||||
rel: ProposedRel<Self, Self>,
|
||||
commit: impl FnOnce(&mut Asg),
|
||||
) -> Result<(), AsgError> {
|
||||
let to = rel.to_oi.resolve(asg);
|
||||
|
||||
// Template application
|
||||
let child_state = to.meta_state();
|
||||
let span = to.span();
|
||||
|
||||
rel.from_oi.map_obj_inner(asg, |state: MetaState| {
|
||||
state.observe_child(child_state, span)
|
||||
});
|
||||
|
||||
Ok(commit(asg))
|
||||
}
|
||||
},
|
||||
|
||||
tree Doc,
|
||||
|
||||
// Deferred template application
|
||||
tree Tpl,
|
||||
}
|
||||
}
|
||||
|
@ -419,18 +514,20 @@ impl ObjectIndex<Expr> {
|
|||
self.map_obj_inner(asg, |span: Span| span.merge(end).unwrap_or(span))
|
||||
}
|
||||
|
||||
/// Create a new subexpression as the next child of this expression and
|
||||
/// return the [`ObjectIndex`] of the new subexpression.
|
||||
/// Add a completed subexpression as a child of a parent expression.
|
||||
///
|
||||
/// It is important that the subexpression has _completed parsing_ so
|
||||
/// that edge hooks are able to conduct inference on the entirety of
|
||||
/// the subexpression.
|
||||
///
|
||||
/// Sub-expressions maintain relative order to accommodate
|
||||
/// non-associative and non-commutative expressions.
|
||||
pub fn create_subexpr(
|
||||
pub fn add_completed_subexpr(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
expr: Expr,
|
||||
oi_sub: ObjectIndex<Expr>,
|
||||
) -> Result<ObjectIndex<Expr>, AsgError> {
|
||||
let oi_subexpr = asg.create(expr);
|
||||
oi_subexpr.add_tree_edge_from(asg, self)
|
||||
self.add_tree_edge_to(asg, oi_sub)
|
||||
}
|
||||
|
||||
/// Reference the value of the expression identified by `oi_ident` as if
|
||||
|
@ -451,8 +548,8 @@ impl ObjectIndex<Expr> {
|
|||
/// If this is not true,
|
||||
/// consider using:
|
||||
///
|
||||
/// 1. [`Self::create_subexpr`] to create and assign ownership of
|
||||
/// expressions contained within other expressions; or
|
||||
/// 1. [`Self::add_completed_subexpr`] to create and assign ownership
|
||||
/// of expressions contained within other expressions; or
|
||||
/// 2. [`ObjectIndex<Ident>::bind_definition`] if this expression is to
|
||||
/// be assigned to an identifier.
|
||||
pub fn held_by(
|
||||
|
|
Loading…
Reference in New Issue