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))) => {
|
(BuildingExpr(es, poi), AirExpr(ExprStart(op, span))) => {
|
||||||
match poi.create_subexpr(ctx.asg_mut(), Expr::new(op, span)) {
|
let oi_child = ctx.asg_mut().create(Expr::new(op, span));
|
||||||
Ok(oi) => {
|
Transition(BuildingExpr(es.push(poi), oi_child)).incomplete()
|
||||||
Transition(BuildingExpr(es.push(poi), oi)).incomplete()
|
|
||||||
}
|
|
||||||
Err(e) => Transition(BuildingExpr(es, poi)).err(e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(BuildingExpr(es, oi), AirExpr(ExprEnd(end))) => {
|
(BuildingExpr(es, oi), AirExpr(ExprEnd(end))) => {
|
||||||
|
@ -107,13 +103,19 @@ impl ParseState for AirExprAggregate {
|
||||||
let oi_root = ctx.dangling_expr_oi();
|
let oi_root = ctx.dangling_expr_oi();
|
||||||
|
|
||||||
match (es.pop(), dangling) {
|
match (es.pop(), dangling) {
|
||||||
((es, Some(poi)), _) => {
|
// This was a child expression
|
||||||
Transition(BuildingExpr(es, poi)).incomplete()
|
((es, Some(poi)), _) => poi
|
||||||
}
|
.add_completed_subexpr(ctx.asg_mut(), oi)
|
||||||
|
.map(|_| ())
|
||||||
|
.transition(BuildingExpr(es, poi)),
|
||||||
|
|
||||||
|
// Topmost expression, dangling.
|
||||||
((es, None), true) => {
|
((es, None), true) => {
|
||||||
Self::hold_dangling(ctx.asg_mut(), oi_root, oi)
|
Self::hold_dangling(ctx.asg_mut(), oi_root, oi)
|
||||||
.transition(Ready(es.done()))
|
.transition(Ready(es.done()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Topmost expression, reachable.
|
||||||
((es, None), false) => {
|
((es, None), false) => {
|
||||||
Transition(Ready(es.done())).incomplete()
|
Transition(Ready(es.done())).incomplete()
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,10 +47,15 @@ pub fn collect_subexprs(
|
||||||
asg: &Asg,
|
asg: &Asg,
|
||||||
oi: ObjectIndex<Expr>,
|
oi: ObjectIndex<Expr>,
|
||||||
) -> Vec<(ObjectIndex<Expr>, &Expr)> {
|
) -> Vec<(ObjectIndex<Expr>, &Expr)> {
|
||||||
oi.edges(&asg)
|
let mut exprs = oi
|
||||||
|
.edges(&asg)
|
||||||
.filter_map(|rel| rel.narrow::<Expr>())
|
.filter_map(|rel| rel.narrow::<Expr>())
|
||||||
.map(|oi| (oi, oi.resolve(&asg)))
|
.map(|oi| (oi, oi.resolve(&asg)))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Edge order is reversed.
|
||||||
|
exprs.reverse();
|
||||||
|
exprs
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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
|
// but throw in a known reference to show that it doesn't override the
|
||||||
// missing one.
|
// missing one.
|
||||||
#[test]
|
#[test]
|
||||||
fn expr_referencing_missing_with_concrete_without_abstract_is_unknown() {
|
fn expr_referencing_missing_with_concrete_without_abstract_is_maybe_concrete() {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
let toks = [
|
let toks = [
|
||||||
ExprStart(ExprOp::Sum, S1),
|
ExprStart(ExprOp::Sum, S1),
|
||||||
|
@ -959,3 +964,73 @@ fn expr_referencing_missing_with_concrete_without_abstract_is_unknown() {
|
||||||
// so only one is missing.
|
// so only one is missing.
|
||||||
assert_eq!(expr.meta_state(), MetaState::MaybeConcrete(1.unwrap_into()));
|
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)
|
collect_subexprs(&asg, oi_expr)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, expr)| expr.span())
|
.map(|(_, expr)| expr.span())
|
||||||
.rev()
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,50 @@
|
||||||
//! so expressions both naturally compose and are able to be replaced with
|
//! so expressions both naturally compose and are able to be replaced with
|
||||||
//! the value that they represent without affecting the meaning of the
|
//! the value that they represent without affecting the meaning of the
|
||||||
//! program.
|
//! 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::{
|
use super::{
|
||||||
ident::IdentDefinition, prelude::*, Doc, Ident, ObjectIndexToTree, Tpl,
|
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
|
/// The [`Span`] of an expression should be expanded to encompass not only
|
||||||
/// all child expressions,
|
/// all child expressions,
|
||||||
/// but also any applicable closing span.
|
/// but also any applicable closing span.
|
||||||
|
///
|
||||||
|
/// See the [parent module](self) for more information.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Expr {
|
pub struct Expr {
|
||||||
op: ExprOp,
|
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 {
|
impl Display for MetaState {
|
||||||
|
@ -384,10 +444,27 @@ object_rel! {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Non-meta identifiers are just references.
|
// This is a _reference_ to another expression tree.
|
||||||
// We don't care what they are as long as they're not
|
// Only tree edges influence our abstract status.
|
||||||
// metavariables.
|
Some(IdentDefinition::Expr(_)) => (),
|
||||||
Some(IdentDefinition::Expr(_) | IdentDefinition::Tpl(_)) => (),
|
|
||||||
|
// 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 => {
|
None => {
|
||||||
rel.from_oi.map_obj_inner(asg, |meta: MetaState| {
|
rel.from_oi.map_obj_inner(asg, |meta: MetaState| {
|
||||||
|
@ -404,10 +481,28 @@ object_rel! {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
tree Expr,
|
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);
|
||||||
|
|
||||||
|
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,
|
tree Doc,
|
||||||
|
|
||||||
// Template application
|
// Deferred template application
|
||||||
tree Tpl,
|
tree Tpl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -419,18 +514,20 @@ impl ObjectIndex<Expr> {
|
||||||
self.map_obj_inner(asg, |span: Span| span.merge(end).unwrap_or(span))
|
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
|
/// Add a completed subexpression as a child of a parent expression.
|
||||||
/// return the [`ObjectIndex`] of the new subexpression.
|
///
|
||||||
|
/// 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
|
/// Sub-expressions maintain relative order to accommodate
|
||||||
/// non-associative and non-commutative expressions.
|
/// non-associative and non-commutative expressions.
|
||||||
pub fn create_subexpr(
|
pub fn add_completed_subexpr(
|
||||||
self,
|
self,
|
||||||
asg: &mut Asg,
|
asg: &mut Asg,
|
||||||
expr: Expr,
|
oi_sub: ObjectIndex<Expr>,
|
||||||
) -> Result<ObjectIndex<Expr>, AsgError> {
|
) -> Result<ObjectIndex<Expr>, AsgError> {
|
||||||
let oi_subexpr = asg.create(expr);
|
self.add_tree_edge_to(asg, oi_sub)
|
||||||
oi_subexpr.add_tree_edge_from(asg, self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reference the value of the expression identified by `oi_ident` as if
|
/// Reference the value of the expression identified by `oi_ident` as if
|
||||||
|
@ -451,8 +548,8 @@ impl ObjectIndex<Expr> {
|
||||||
/// If this is not true,
|
/// If this is not true,
|
||||||
/// consider using:
|
/// consider using:
|
||||||
///
|
///
|
||||||
/// 1. [`Self::create_subexpr`] to create and assign ownership of
|
/// 1. [`Self::add_completed_subexpr`] to create and assign ownership
|
||||||
/// expressions contained within other expressions; or
|
/// of expressions contained within other expressions; or
|
||||||
/// 2. [`ObjectIndex<Ident>::bind_definition`] if this expression is to
|
/// 2. [`ObjectIndex<Ident>::bind_definition`] if this expression is to
|
||||||
/// be assigned to an identifier.
|
/// be assigned to an identifier.
|
||||||
pub fn held_by(
|
pub fn held_by(
|
||||||
|
|
Loading…
Reference in New Issue