diff --git a/tamer/src/asg/air.rs b/tamer/src/asg/air.rs index 3496fdf2..d38ff21a 100644 --- a/tamer/src/asg/air.rs +++ b/tamer/src/asg/air.rs @@ -523,6 +523,60 @@ impl AirAggregateCtx { } } +/// Property of identifier scope within a given environment. +/// +/// An _environment_ is the collection of identifiers associated with a +/// container object. +/// Environments stack, +/// such that an environment inherits the identifiers of its parent. +/// +/// The _scope_ of an identifier is defined by what environments can "see" +/// that identifier. +/// For the purposes of TAME's analysis, +/// we care only about the global environment and shadowing. +/// +/// The relationship between identifier scope and environment can be +/// visualized as a two-dimensional table with the environments forming +/// layers along the x-axes, +/// and scopes slicing those layers along the y-axies. +/// +/// TODO: Example visualization. +#[cfg(test)] +#[derive(Debug, PartialEq)] +enum EnvScopeKind { + /// Identifiers are pooled without any defined hierarchy. + /// + /// An identifier that is part of a pool must be unique. + /// Since there is no hierarchy, + /// the system should not suggest that shadowing is not permitted and + /// should insteam emphasize that such an identifier must be unique + /// globally. + /// + /// This should be used only at the root. + /// An identifier's scope can be further refined to provide more useful + /// diagnostic messages by descending into the package in which it is + /// defined and evaluating scope relative to the package. + _Pool, + + /// Identifier in this environment is a shadow of a deeper environment. + /// + /// An identifier is said to cast a shadow on environments higher in its + /// hierarchy. + /// Since shadowing is not permitted in TAME, + /// this can be used to present useful diagnostic information to the + /// user. + /// + /// A shadow can be used to check for identifier conflicts, + /// but it cannot be used for lookup; + /// this environment should be filtered out of this identifier's + /// scope. + _Shadow, + + /// This environment owns the identifier or is an environment descended + /// from one that does. + Visible, +} + impl AsMut for AirAggregateCtx { fn as_mut(&mut self) -> &mut AirAggregateCtx { self diff --git a/tamer/src/asg/air/test.rs b/tamer/src/asg/air/test.rs index 131a628b..07f0abc2 100644 --- a/tamer/src/asg/air/test.rs +++ b/tamer/src/asg/air/test.rs @@ -35,6 +35,8 @@ type Sut = AirAggregate; use Air::*; use Parsed::Incomplete; +mod scope; + #[test] fn ident_decl() { let id = SPair("foo".into(), S2); @@ -642,6 +644,16 @@ where sut.finalize().unwrap().into_context() } +/// [`asg_from_toks`] without creating a package automatically. +pub fn asg_from_toks_raw>(toks: I) -> Asg +where + I::IntoIter: Debug, +{ + let mut sut = Sut::parse(toks.into_iter()); + assert!(sut.all(|x| x.is_ok())); + sut.finalize().unwrap().into_context() +} + fn root_lookup(asg: &Asg, name: SPair) -> Option> { asg.lookup(asg.root(S1), name) } diff --git a/tamer/src/asg/air/test/scope.rs b/tamer/src/asg/air/test/scope.rs new file mode 100644 index 00000000..30de5436 --- /dev/null +++ b/tamer/src/asg/air/test/scope.rs @@ -0,0 +1,296 @@ +// Scope tests for ASG IR +// +// Copyright (C) 2014-2023 Ryan Specialty, 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 . + +//! Scoping tests. +//! +//! These tests verify that identifiers are scoped to the proper +//! environments. +//! These may duplicate portions of other tests, +//! but having concrete examples all in one place helps to develop, debug, +//! and understand a system that can be quite confusing in the abstract. +//! +//! These tests _do not_ assert that identifiers are properly assigned to +//! their corresponding definitions; +//! those tests exist elsewhere. +//! +//! The core abstraction for these tests is [`assert_scope`], +//! which allows for declarative assertions of identifier scope against +//! the graph; +//! it is key to creating tests that can be both easily created and +//! easily understood. +//! +//! If You Are Here Due To A Test Failure +//! ===================================== +//! If there are failing tests in parent or sibling modules, +//! check those first; +//! these tests are potentially fragile given that they test only a +//! subset of behavior without first asserting against other system +//! invariants, +//! as described above. + +use super::*; +use crate::{ + asg::{ + graph::object::{self, ObjectTy}, + visit::{tree_reconstruction, TreeWalkRel}, + ExprOp, + }, + fmt::{DisplayWrapper, TtQuote}, + span::UNKNOWN_SPAN, +}; +use std::iter::once; + +use EnvScopeKind::*; +use ObjectTy::*; + +const S0: Span = UNKNOWN_SPAN; + +fn m(a: Span, b: Span) -> Span { + a.merge(b).unwrap() +} + +#[test] +fn pkg_child_definition() { + let pkg_name = SPair("/pkg".into(), S1); + let name = SPair("foo".into(), S3); + + #[rustfmt::skip] + let toks = vec![ + // ENV: 0 global + PkgStart(S1, pkg_name), + // ENV: 1 pkg + ExprStart(ExprOp::Sum, S2), + // ENV: 1 pkg + BindIdent(name), + ExprEnd(S4), + PkgEnd(S5), + ]; + + let asg = asg_from_toks_raw(toks); + + #[rustfmt::skip] + assert_scope(&asg, name, [ + // The identifier is not local, + // and so its scope should extend into the global environment. + // TODO: (Root, S0, Pool), + + // Expr does not introduce a new environment, + // and so the innermost environment in which we should be able to + // find the identifier is the Pkg. + (Pkg, m(S1, S5), Visible) + ]); +} + +#[test] +fn pkg_nested_expr_definition() { + let pkg_name = SPair("/pkg".into(), S1); + let outer = SPair("outer".into(), S3); + let inner = SPair("inner".into(), S5); + + #[rustfmt::skip] + let toks = vec![ + // ENV: 0 global + PkgStart(S1, pkg_name), + // ENV: 1 pkg + ExprStart(ExprOp::Sum, S2), + // ENV: 1 pkg + BindIdent(outer), + + ExprStart(ExprOp::Sum, S4), + // ENV: 1 pkg + BindIdent(inner), + ExprEnd(S6), + ExprEnd(S7), + PkgEnd(S8), + ]; + + let asg = asg_from_toks_raw(toks); + + #[rustfmt::skip] + assert_scope(&asg, inner, [ + // The identifier is not local, + // and so its scope should extend into the global environment. + // TODO: (Root, S0, Pool), + + // Expr does not introduce a new environment, + // and so just as the outer expression, + // the inner is scoped to a package environment. + (Pkg, m(S1, S8), Visible) + ]); +} + +#[test] +fn pkg_tpl_definition() { + let pkg_name = SPair("/pkg".into(), S1); + + let tpl_outer = SPair("_tpl-outer_".into(), S3); + let meta_outer = SPair("@param_outer@".into(), S5); + let expr_outer = SPair("exprOuter".into(), S8); + + let tpl_inner = SPair("_tpl-inner_".into(), S11); + let meta_inner = SPair("@param_inner@".into(), S13); + let expr_inner = SPair("exprInner".into(), S16); + + #[rustfmt::skip] + let toks = vec![ + // ENV: 0 global + PkgStart(S1, pkg_name), + // ENV: 1 pkg + TplStart(S2), + // ENV: 2 tpl + BindIdent(tpl_outer), + + TplMetaStart(S4), + BindIdent(meta_outer), + TplMetaEnd(S6), + + ExprStart(ExprOp::Sum, S7), + BindIdent(expr_outer), + ExprEnd(S9), + + TplStart(S10), + // ENV: 3 tpl + BindIdent(tpl_inner), + + TplMetaStart(S12), + BindIdent(meta_inner), + TplMetaEnd(S14), + + ExprStart(ExprOp::Sum, S15), + BindIdent(expr_inner), + ExprEnd(S17), + TplEnd(S18), + TplEnd(S19), + PkgEnd(S20), + ]; + + let asg = asg_from_toks_raw(toks); + + #[rustfmt::skip] + assert_scope(&asg, tpl_outer, [ + // TODO: (Root, S0, Pool), + (Pkg, m(S1, S20), Visible) + ]); + #[rustfmt::skip] + assert_scope(&asg, meta_outer, [ + // TODO: (Tpl, m(S2, S19), Visible) + ]); + #[rustfmt::skip] + assert_scope(&asg, expr_outer, [ + (Tpl, m(S2, S19), Visible) + ]); + + #[rustfmt::skip] + assert_scope(&asg, tpl_inner, [ + (Tpl, m(S2, S19), Visible) + ]); + #[rustfmt::skip] + assert_scope(&asg, meta_inner, [ + // TODO: (Tpl, m(S10, S18), Visible) + ]); + #[rustfmt::skip] + assert_scope(&asg, expr_inner, [ + (Tpl, m(S10, S18), Visible) + ]); +} + +///// Tests end above this line, plumbing begins below ///// + +/// Assert that the scope of the identifier named `name` is that of the +/// provided environment list `expected`. +/// +/// This will search the graph for all environments in which `name` has been +/// indexed, +/// gather information about those environments, +/// and compare them against `expected`. +/// The environment listing is expected to be in ontological order, +/// as by [`tree_reconstruction`]. +/// +/// This function is essential to providing easily understood, +/// declarative scope test definitions, +/// which make it easy to form and prove hypotheses about the behavior of +/// TAMER's scoping system. +fn assert_scope( + asg: &Asg, + name: SPair, + expected: impl IntoIterator, +) { + // We are interested only in identifiers for scoping, + // not the objects that they point to. + // We will use the span of the identifier that we locate via index + // lookups to determine whether we've found the right one. + // This relies on the tests using unique spans for each object on the + // graph, + // which is standard convention for TAMER's tests. + let expected_span = name.span(); + + // We use what was most convenient at the time of writing to gather + // environments representing the scope of `name`. + // This is not the most efficient, + // but our test graphs are quite small, + // and so that won't matter. + // + // The reason that this works is because the traversal will visit + // objects following the graph's ontology, + // which will produce a tree in the expected order. + // We filter on index lookup, + // which discards the portions of the tree + // (the graph) + // that we are not interested in. + // + // This also means that a failure to extend the scope of `name` to a + // particular environment will cause it to be omitted from this + // iterator, + // but that is okay; + // it'll result in a test failure that should be easy enough to + // understand. + let given_without_root = + tree_reconstruction(asg).filter_map(|TreeWalkRel(dynrel, _)| { + dynrel.target_oi_rel_to_dyn::().map(|oi_to| { + ( + dynrel.target_ty(), + dynrel.target().resolve(asg).span(), + asg.lookup(oi_to, name), + ) + }) + }); + + // `tree_reconstruction` omits root, + // so we'll have to add it ourselves. + let oi_root = asg.root(name); + let given = once((Root, S0, asg.lookup(oi_root, name))) + .chain(given_without_root) + .filter_map(|(ty, span, ooi)| ooi.map(|oi| (ty, span, oi.resolve(asg)))) + .inspect(|(ty, span, ident)| assert_eq!( + expected_span, + ident.span(), + "expected {wname} span {expected_span} at {ty}:{span}, but found {given}", + wname = TtQuote::wrap(name), + given = ident.span(), + )) + // TODO + .map(|(ty, span, _)| (ty, span, EnvScopeKind::Visible)); + + // Collection allows us to see the entire expected and given lists on + // assertion failure. + assert_eq!( + expected.into_iter().collect::>(), + given.collect::>(), + ) +} diff --git a/tamer/src/asg/graph.rs b/tamer/src/asg/graph.rs index 5765cb67..bb34f349 100644 --- a/tamer/src/asg/graph.rs +++ b/tamer/src/asg/graph.rs @@ -90,7 +90,7 @@ pub struct Asg { /// Directed graph on which objects are stored. graph: DiGraph, - /// Edge cache of [`SymbolId`][crate::sym::SymbolId] to + /// Environment cache of [`SymbolId`][crate::sym::SymbolId] to /// [`ObjectIndex`]es. /// /// This maps a `(SymbolId, NodeIndex)` pair to a node on the graph for diff --git a/tamer/src/asg/graph/object.rs b/tamer/src/asg/graph/object.rs index 2ec78418..6d9164ec 100644 --- a/tamer/src/asg/graph/object.rs +++ b/tamer/src/asg/graph/object.rs @@ -210,6 +210,31 @@ macro_rules! object_gen { $($kind,)+ } + impl ObjectTy { + /// Assume that the provided [`ObjectIndex`] is of the type + /// associated with `self`, + /// and then determine whether that object can be related to + /// another object of a given type `OB`. + /// + /// This method should be kept private; + /// it is memory safe, + /// but incorrect assumptions will violate graph object + /// invariants and cause panics when the [`ObjectIndexTo`] + /// is later resolved. + fn assuming_oi_maybe_rel_to_dyn( + &self, + oi: ObjectIndex, + ) -> Option> { + match self { + $( + Self::$kind => { + $kind::oi_rel_to_dyn(oi.must_narrow_into()) + } + )+ + } + } + } + impl From<&Object> for ObjectTy { fn from(obj: &Object) -> Self { match obj { diff --git a/tamer/src/asg/graph/object/rel.rs b/tamer/src/asg/graph/object/rel.rs index 803c5f47..91246992 100644 --- a/tamer/src/asg/graph/object/rel.rs +++ b/tamer/src/asg/graph/object/rel.rs @@ -131,6 +131,23 @@ macro_rules! object_rel { _ => None, } } + + fn oi_rel_to_dyn( + #[allow(unused_variables)] // for empty Rel + oi: ObjectIndex, + ) -> Option<$crate::asg::graph::object::ObjectIndexTo> { + #[allow(unused_imports)] + use $crate::asg::graph::object::ObjectIndexTo; + + match OB::rel_ty() { + $( + ObjectRelTy::$kind => { + ObjectIndexTo::<$kind>::from(oi).reflexivity() + }, + )* + _ => None, + } + } } $( @@ -291,6 +308,37 @@ impl DynObjectRel> { self.map(|(soi, toi)| (soi, toi.resolve(asg).pair_oi(toi))) } + /// Retrieve the target [`ObjectIndex`] as an [`ObjectIndexTo`](ObjectIndexTo), + /// if the object can be related to objects of type `OB`. + /// + /// This method may be confusing in that it represents another + /// _possible_ relation on top of the relation represented by + /// [`Self`]. + /// That is: + /// + /// ```text + /// OA -> OB [ -> OC] + /// '______' + /// Self + /// ``` + /// + /// If this method returns [`Some`], + /// that means that the target of this relation `OB` is an object + /// _that is capable of being related to_ an object of type `OC`. + pub fn target_oi_rel_to_dyn( + &self, + ) -> Option> { + // TODO: A newtype ought to couple these in a way that we don't have + // to trust this assumption! + // This requires assuming that the target is of the target type, + // which _should_ certainly be the case if originating from the graph, + // but if it's not, + // then later resolving the `ObjectIndex` with a mismatched type + // will result in a panic. + self.target_ty() + .assuming_oi_maybe_rel_to_dyn(*self.target()) + } + /// Dynamically determine whether this edge represents a cross edge. /// /// This function is intended for _dynamic_ edge types, @@ -461,7 +509,7 @@ pub trait ObjectRelatable: ObjectKind { /// Represent a relation to another [`ObjectKind`] that cannot be /// statically known and must be handled at runtime. /// - /// A value of [`None`] means that the provided [`DynObjectRel`] is not + /// A value of [`None`] means that the provided [`ObjectRelTy`] is not /// valid for [`Self`]. /// If the caller is utilizing edge data that is already present on the graph, /// then this means that the system is not properly upholding edge @@ -476,6 +524,21 @@ pub trait ObjectRelatable: ObjectKind { ty: ObjectRelTy, oi: ObjectIndex, ) -> Option; + + /// Cast the provided [`ObjectIndex`] into an [`ObjectIndexTo`] if it is + /// able to be related to the provided `OB`. + /// + /// This is intended to be used in a dynamic context, + /// where the caller is not aware statically of the [`ObjectKind`]s + /// involved. + /// If it is _required_ that an object be relatable, + /// use [`ObjectRelTo`] to statically verify that assertion. + /// + /// If the type `OB` is not a valid target of a relation from this type, + /// [`None`] will be returned. + fn oi_rel_to_dyn( + oi: ObjectIndex, + ) -> Option>; } impl ObjectIndex { @@ -961,6 +1024,32 @@ mod private { pub fn span(&self) -> Span { (*self).into() } + + /// Assert a reflexive relationship between `OB` and `OC`. + /// + /// The types `OB` and `OC` are equivalent (and therefore reflexive) + /// iff they have matching `ObjectRelTy`s. + /// + /// The sole purpose of this method is to satisfy Rust's type system + /// in dynamic situations where the type system is not able to + /// understand what we're doing, + /// where the type `OC` is more general than the type `OB`. + /// This method is always safe; + /// it will return [`None`] if the two types differ in runtime + /// value. + /// + /// While the term "reflexive" is a binary relation in mathematics, + /// the term "reflexivity" originates from the Coq tactic. + /// + /// For an example of where this is needed, + /// see [`ObjectRelatable::oi_rel_to_dyn`]. + pub fn reflexivity( + self, + ) -> Option> { + let Self(parts, _) = self; + (OB::rel_ty() == OC::rel_ty()) + .then_some(ObjectIndexTo(parts, PhantomData::default())) + } } // Ignore metadata that should always be consistent with the underlying