// 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 <http://www.gnu.org/licenses/>.

//! Intermediate representation for construction of the
//!   [abstract semantic graph (ASG)](super) (AIR).
//!
//! AIR serves as an abstraction layer between higher-level parsers and the
//!   aggregate ASG.
//! It allows parsers to operate as a raw stream of data without having to
//!   worry about ownership of or references to the ASG,
//!     and allows for multiple such parsers to be joined.

use super::{
    graph::object::{
        Object, ObjectIndexRelTo, ObjectIndexTo, ObjectIndexToTree,
        ObjectRelTy, ObjectRelatable, Pkg, Root, Tpl,
    },
    Asg, AsgError, Expr, Ident, ObjectIndex,
};
use crate::{
    diagnose::Annotate,
    diagnostic_unreachable,
    f::Functor,
    parse::{prelude::*, StateStack},
    span::Span,
    sym::SymbolId,
};
use std::{
    collections::hash_map::Entry,
    fmt::{Debug, Display},
};

#[macro_use]
mod ir;
use fxhash::FxHashMap;
pub use ir::Air;

mod expr;
mod meta;
mod opaque;
mod pkg;
mod tpl;
use expr::AirExprAggregate;
use meta::AirMetaAggregate;
use opaque::AirOpaqueAggregate;
use pkg::AirPkgAggregate;
use tpl::AirTplAggregate;

pub type IdentSym = SymbolId;
pub type DepSym = SymbolId;

/// AIR parser state.
#[derive(Debug, PartialEq, Default)]
pub enum AirAggregate {
    /// Parser has not yet been initialized.
    #[default]
    Uninit,

    /// Parser is in the root context.
    ///
    /// As a parser,
    ///   this does nothing but await work.
    /// Its presence in the [`AirStack`] is used for the global environment.
    Root(ObjectIndex<Root>),

    /// Parsing a package.
    Pkg(AirPkgAggregate),

    /// Parsing an expression.
    ///
    /// This expects to inherit an [`AirExprAggregate`] from the prior state
    ///   so that we are not continuously re-allocating its stack for each
    ///   new expression root.
    PkgExpr(AirExprAggregate),

    /// Parser is in template parsing mode.
    ///
    /// All objects encountered until the closing [`Air::TplEnd`] will be
    ///   parented to this template rather than the parent [`Pkg`].
    /// See [`Air::TplStart`] for more information.
    PkgTpl(AirTplAggregate),

    /// Parsing metavariables.
    PkgMeta(AirMetaAggregate),

    /// Parsing opaque objects.
    ///
    /// This parser is intended for loading declarations from object files
    ///   without loading their corresponding definitions.
    PkgOpaque(AirOpaqueAggregate),
}

impl Display for AirAggregate {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use AirAggregate::*;

        match self {
            Uninit => write!(f, "awaiting AIR input"),
            Root(_) => write!(f, "awaiting input at root"),
            Pkg(pkg) => {
                write!(f, "defining a package: {pkg}")
            }
            PkgExpr(expr) => {
                write!(f, "defining a package expression: {expr}")
            }
            PkgTpl(tpl) => {
                write!(f, "building a template: {tpl}")
            }
            PkgMeta(meta) => {
                write!(f, "building metavariable: {meta}")
            }
            PkgOpaque(opaque) => {
                write!(f, "loading opaque objects: {opaque}")
            }
        }
    }
}

impl From<AirPkgAggregate> for AirAggregate {
    fn from(st: AirPkgAggregate) -> Self {
        Self::Pkg(st)
    }
}

impl From<AirExprAggregate> for AirAggregate {
    fn from(st: AirExprAggregate) -> Self {
        Self::PkgExpr(st)
    }
}

impl From<AirTplAggregate> for AirAggregate {
    fn from(st: AirTplAggregate) -> Self {
        Self::PkgTpl(st)
    }
}

impl From<AirMetaAggregate> for AirAggregate {
    fn from(st: AirMetaAggregate) -> Self {
        Self::PkgMeta(st)
    }
}

impl From<AirOpaqueAggregate> for AirAggregate {
    fn from(st: AirOpaqueAggregate) -> Self {
        Self::PkgOpaque(st)
    }
}

impl ParseState for AirAggregate {
    type Token = Air;
    type Object = ();
    type Error = AsgError;
    type Context = AirAggregateCtx;

    fn parse_token(
        self,
        tok: Self::Token,
        ctx: &mut Self::Context,
    ) -> crate::parse::TransitionResult<Self> {
        use ir::{AirSubsets::*, AirTodo::*};
        use AirAggregate::*;

        match (self, tok.into()) {
            // Initialize the parser with the graph root,
            //   or continue with a previous context that has already been
            //   initialized.
            // See `asg::air::test::resume_previous_parsing_context` for an
            //   explanation of why this is important.
            (Uninit, tok) => {
                let oi_root = ctx.asg_ref().root(tok.span());
                ctx.stack().continue_or_init(|| Root(oi_root), tok)
            }

            (st, AirTodo(Todo(_))) => Transition(st).incomplete(),

            // Package
            //
            // Note that `ret_or_transfer` will return from the active frame
            //   if it is in an accepting state,
            //     and so encountering a properly nested `PkgClose` will pop
            //     frames off of the stack until reaching the still-active
            //     parent package frame.
            (
                st @ (Root(..) | PkgExpr(..) | PkgTpl(..) | PkgMeta(..)
                | PkgOpaque(..)),
                tok @ AirPkg(..),
            ) => ctx.ret_or_transfer(st, tok, AirPkgAggregate::new()),
            (Pkg(pkg), AirPkg(etok)) => ctx.proxy(pkg, etok),
            (Pkg(pkg), AirBind(etok)) => ctx.proxy(pkg, etok),
            (Pkg(pkg), AirDoc(etok)) => ctx.proxy(pkg, etok),

            // Expression
            (st @ (Pkg(_) | PkgTpl(_)), tok @ AirExpr(..)) => {
                ctx.ret_or_transfer(st, tok, AirExprAggregate::new())
            }
            (PkgExpr(expr), AirExpr(etok)) => ctx.proxy(expr, etok),
            (PkgExpr(expr), AirBind(etok)) => ctx.proxy(expr, etok),
            (PkgExpr(expr), AirDoc(etok)) => ctx.proxy(expr, etok),

            // Template
            (st @ (Pkg(_) | PkgExpr(_)), tok @ AirTpl(..)) => {
                ctx.ret_or_transfer(st, tok, AirTplAggregate::new())
            }
            (PkgTpl(tplst), AirTpl(ttok)) => ctx.proxy(tplst, ttok),
            (PkgTpl(tplst), AirBind(ttok)) => ctx.proxy(tplst, ttok),
            (PkgTpl(tplst), AirDoc(ttok)) => ctx.proxy(tplst, ttok),

            // Metasyntactic variables (metavariables)
            (st @ PkgTpl(_), tok @ AirMeta(..)) => {
                ctx.ret_or_transfer(st, tok, AirMetaAggregate::new())
            }
            (PkgMeta(meta), AirMeta(mtok)) => ctx.proxy(meta, mtok),
            (PkgMeta(meta), AirBind(mtok)) => ctx.proxy(meta, mtok),
            (PkgMeta(meta), tok @ (AirExpr(..) | AirTpl(..) | AirDoc(..))) => {
                ctx.try_ret_with_lookahead(meta, tok)
            }

            // Opaque
            //
            // By having opaque object loading be its _own_ child parser,
            //   we ensure that the active package frame becomes held on the
            //   stack before loading e.g. opaque identifiers.
            // Since scope is determined by stack frames,
            //   this has the effect of ensuring that the package `st`
            //   becomes included in the identifier's scope.
            (st @ Pkg(_), tok @ AirIdent(..)) => {
                ctx.ret_or_transfer(st, tok, AirOpaqueAggregate::new())
            }
            (PkgOpaque(opaque), AirIdent(otok)) => ctx.proxy(opaque, otok),
            (
                PkgOpaque(_),
                tok @ (AirExpr(..) | AirBind(..) | AirTpl(..) | AirDoc(..)),
            ) => {
                // This is simply not expected at the time of writing,
                //   since this is used for importing object files.
                crate::diagnostic_panic!(
                    vec![
                        tok.span()
                            .internal_error("this is not an opaque identifier"),
                        tok.span().help(
                            "this may represent a problem with an object file"
                        )
                    ],
                    "expected opaque identifier, found {tok}",
                );
            }

            (
                st @ Root(_),
                tok @ (AirExpr(..) | AirBind(..) | AirTpl(..) | AirMeta(..)
                | AirDoc(..)),
            ) => Transition(st).err(AsgError::PkgExpected(tok.span())),

            // TODO: We will need to be more intelligent about this,
            //   since desugaring will produce metavariables in nested contexts,
            //     e.g. within an expression within a template.
            (st @ (Pkg(..) | PkgExpr(..) | PkgOpaque(..)), AirMeta(tok)) => {
                Transition(st).err(AsgError::UnexpectedMeta(tok.span()))
            }

            (
                st @ (Root(..) | PkgExpr(..) | PkgTpl(..) | PkgMeta(..)),
                AirIdent(tok),
            ) => {
                Transition(st).err(AsgError::UnexpectedOpaqueIdent(tok.name()))
            }
        }
    }

    fn is_accepting(&self, ctx: &Self::Context) -> bool {
        ctx.stack_ref().iter().all(|st| st.active_is_accepting(ctx))
            && self.active_is_accepting(ctx)
    }
}

impl AirAggregate {
    /// Whether the active parser is completed with active parsing.
    ///
    /// This method is used to determine whether control ought to be
    ///   transferred to a new child parser.
    ///
    /// If a child parser is active,
    ///   then its [`ParseState::is_accepting`] will be consulted.
    fn active_is_complete(&self, ctx: &<Self as ParseState>::Context) -> bool {
        use AirAggregate::*;

        match self {
            Uninit => false,

            // We can't be done with something we're not doing.
            // This is necessary to start the first child parser.
            Root(_) => false,

            Pkg(st) => st.is_accepting(ctx),
            PkgExpr(st) => st.is_accepting(ctx),
            PkgTpl(st) => st.is_accepting(ctx),
            PkgMeta(st) => st.is_accepting(ctx),
            PkgOpaque(st) => st.is_accepting(ctx),
        }
    }

    // Whether the parser is in an accepting state.
    fn active_is_accepting(&self, ctx: &<Self as ParseState>::Context) -> bool {
        use AirAggregate::*;

        match self {
            Uninit => false,

            // This must not recurse on `AirAggregate::is_accepting`,
            //   otherwise it'll be mutually recursive.
            Root(_) => true,

            Pkg(st) => st.is_accepting(ctx),
            PkgExpr(st) => st.is_accepting(ctx),
            PkgTpl(st) => st.is_accepting(ctx),
            PkgMeta(st) => st.is_accepting(ctx),
            PkgOpaque(st) => st.is_accepting(ctx),
        }
    }

    /// The rooting context for [`Ident`]s for the active parser.
    ///
    /// A value of [`None`] indicates that the current parser does not
    ///   support direct bindings,
    ///     but a parent context may
    ///       (see [`AirAggregateCtx::rooting_oi`]).
    fn active_rooting_oi(&self) -> Option<ObjectIndexToTree<Ident>> {
        use AirAggregate::*;

        match self {
            Uninit => None,

            // Root will serve as a pool of identifiers,
            //   but it can never _contain_ their definitions.
            // See `active_env_oi`.
            Root(_) => None,

            // Packages always serve as roots for identifiers
            //   (that is their entire purpose).
            Pkg(pkgst) => pkgst.active_pkg_oi().map(Into::into),

            // Expressions never serve as roots for identifiers;
            //   this will always fall through to the parent context.
            // Since the parent context is a package or a template,
            //   the next frame should succeed.
            PkgExpr(_) => None,

            // Identifiers bound while within a template definition context
            //   must bind to the eventual _expansion_ site,
            //     as if the body were pasted there.
            // Templates must therefore serve as containers for identifiers
            //   bound therein.
            PkgTpl(tplst) => tplst.active_tpl_oi().map(Into::into),

            // Identifiers cannot be rooted within metavariables since they
            //   contain only lexical information.
            PkgMeta(_) => None,

            // Loading of opaque objects happens within the context of the
            //   parent frame.
            // At the time of writing,
            //   that is only a package.
            PkgOpaque(_) => None,
        }
    }

    /// Active environment for identifier lookups.
    ///
    /// An environment is a superset of a container,
    ///   which is described by [`Self::active_rooting_oi`].
    /// For example,
    ///   [`Self::Root`] cannot own any identifiers,
    ///     but it can serve as a pool of references to them.
    fn active_env_oi(&self) -> Option<ObjectIndexTo<Ident>> {
        use AirAggregate::*;

        match self {
            Root(oi_root) => Some((*oi_root).into()),
            _ => self.active_rooting_oi().map(Into::into),
        }
    }

    /// The boundary associated with the active environment.
    ///
    /// If the active parser does not introduce its own scope
    ///   (if its environment is that of an ancestor frame)
    ///   then the boundary will be [`EnvBoundary::Transparent`].
    fn active_env_boundary(&self) -> EnvBoundary {
        use AirAggregate::*;
        use EnvBoundary::*;

        match self {
            // These are just parsing states,
            //   not environments.
            Uninit => Transparent,
            PkgOpaque(_) => Transparent,

            // Expressions and metadata are not containers,
            //   and do not introduce scope.
            PkgExpr(_) => Transparent,
            PkgMeta(_) => Transparent,

            // Packages and templates act as containers and so restrict
            //   identifier scope.
            Pkg(_) => Opaque,
            PkgTpl(_) => Opaque,

            // Root will absorb any identifiers that are Visibile
            //   immediately below it
            //     (which is `Pkg` in practice).
            Root(_) => Filter,
        }
    }
}

/// Behavior of an environment boundary when crossing environment upward
///   into a parent (less nested) environment.
///
/// An identifier's [`EnvScopeKind`] is a function of its current
///   [`EnvScopeKind`] and the boundary that it is crossing upward toward.
///
/// This system focuses heavily on identifier "shadows".
/// This boundary is therefore described in terms of material properties
///   with respect to light,
///     where [`EnvScopeKind`] represents a light source
///       (or absence thereof in the case of a shadow).
///
/// TAME does not explicitly track identifier scope at environments more
///   deeply nested than its definition site,
///     since the scope is always implicitly [`EnvScopeKind::Visible`] at
///     those levels.
///
/// Boundary rules are applied to an [`EnvScopeKind`] via
///   [`Self::cross_low_to_high`].
///
/// TODO: Diagrams are still only in test cases.
enum EnvBoundary {
    /// Identifier scope is unaffected by this boundary.
    ///
    /// From a boundary crossing function perspective,
    ///   this represents the identity function.
    Transparent,

    /// Identifier scope will end at this boundary,
    ///   casting a shadow from that point forward.
    ///
    /// Opaque boundaries are visualized as `|` in TAME's scope diagrams.
    Opaque,

    /// Visible identifiers will remain visible after crossing this
    ///   boundary,
    ///     but shadows will not cross.
    ///
    /// Filtering boundaries are visualized as `:` in TAME's scope
    ///   diagrams.
    Filter,
}

impl EnvBoundary {
    /// Apply boundary rules to the given `kind` when crossing the boundary
    ///   from a lower (more nested) environment toward a higher (less
    ///   nested) one.
    ///
    /// The definition (source code) of this function clearly states the
    ///   transformation rules and how they map to the scope diagram
    ///   representation.
    /// See [`Self`] for a summary.
    fn cross_low_to_high<T>(self, kind: EnvScopeKind<T>) -> EnvScopeKind<T> {
        use EnvBoundary::*;
        use EnvScopeKind::*;

        match (kind, self) {
            // x . x
            (kind, Transparent) => kind,

            // v | s
            (Visible(x), Opaque) => Shadow(x),
            // s | s
            (Shadow(x), Opaque) => Shadow(x),
            // _ | _
            (Hidden(x), Opaque) => Hidden(x),

            // v : v
            (Visible(x), Filter) => Visible(x),
            // s : _
            (Shadow(x), Filter) => Hidden(x),
            // _ : _
            (Hidden(x), Filter) => Hidden(x),
        }
    }
}

/// Additional parser context,
///   including the ASG and parser stack frames.
#[derive(Default)]
pub struct AirAggregateCtx {
    /// The ASG under construction by this parser.
    ///
    /// The ASG must be under exclusive ownership of this parser to ensure
    ///   that graph metadata
    ///     (e.g. indexes)
    ///     are accurate.
    asg: Asg,

    /// Held parser frames.
    stack: AirStack,

    /// The package currently being parsed,
    ///   if any.
    ///
    /// This is not necessarily the current compilation unit,
    ///   as the parser may be parsing imports.
    ooi_pkg: Option<ObjectIndex<Pkg>>,

    /// Cache of object scope at a given environment.
    ///
    // TODO: FxHashMap is used for other parts of TAMER,
    //   but here it's really used primarily for bucketing,
    //   so it may be worth profiling compared to alternatives.
    index: ScopeIndex,
}

impl Debug for AirAggregateCtx {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // The index provides far too much information to be useful,
        //   and slows down Parser trace generation.
        f.debug_struct("AirAggregateCtx")
            .field("asg", &self.asg)
            .field("stack", &self.stack)
            .field("ooi_pkg", &self.ooi_pkg)
            .field("index.len()", &self.index.len())
            .finish_non_exhaustive()
    }
}

/// Object scope index at a given environment.
type ScopeIndex = FxHashMap<
    (ObjectRelTy, SymbolId, ObjectIndex<Object>),
    EnvScopeKind<ObjectIndex<Object>>,
>;

/// Limit of the maximum number of held parser frames.
///
/// Note that this is the number of [`ParseState`]s held,
///   _not_ the depth of the graph at a given point.
/// The intent of this is to limit runaway recursion in the event of some
///   bug in the system;
///     while the input stream is certainly finite,
///       lookahead tokens cause recursion that does not provably
///       terminate.
///
/// This limit is arbitrarily large,
///   but hopefully such that no legitimate case will ever hit it.
const MAX_AIR_STACK_DEPTH: usize = 1024;

/// Held parser stack frames.
///
/// See [`AirAggregateCtx`] for more information.
pub type AirStack = StateStack<AirAggregate, MAX_AIR_STACK_DEPTH>;

impl AirAggregateCtx {
    fn asg_mut(&mut self) -> &mut Asg {
        self.as_mut()
    }

    fn asg_ref(&self) -> &Asg {
        self.as_ref()
    }

    fn stack(&mut self) -> &mut AirStack {
        &mut self.stack
    }

    fn stack_ref(&self) -> &AirStack {
        &self.stack
    }

    /// Return control to the parser atop of the stack if `st` is an
    ///   accepting state,
    ///     otherwise transfer control to a new parser `to`.
    ///
    /// This serves as a balance with the behavior of [`Self::proxy`].
    /// Rather than checking for an accepting state after each proxy,
    ///   or having the child parsers return to the top stack frame once
    ///     they have completed,
    ///   we leave the child parser in place to potentially handle more
    ///     tokens of the same type.
    /// For example,
    ///   adjacent expressions can re-use the same parser rather than having
    ///   to pop and push for each sibling.
    ///
    /// Consequently,
    ///   this means that a parser may be complete when we need to push and
    ///   transfer control to another parser.
    /// Before pushing,
    ///   we first check to see if the parser atop of the stack is in an
    ///   accepting state.
    /// If so,
    ///   then we are a sibling,
    ///   and so instead of proceeding with instantiating a new parser,
    ///   we return to the one atop of the stack and delegate to it.
    ///
    /// If `st` is _not_ in an accepting state,
    ///   that means that we are a _child_;
    ///     we then set aside the state `st` on the stack and transfer
    ///     control to the child `to`.
    ///
    /// See also [`Self::proxy`].
    fn ret_or_transfer<S: Into<AirAggregate>, SB: Into<AirAggregate>>(
        &mut self,
        st: S,
        tok: impl Token + Into<Air>,
        to: SB,
    ) -> TransitionResult<AirAggregate> {
        let st_super = st.into();

        if st_super.active_is_complete(self) {
            // TODO: error (this should never happen, so maybe panic instead?)
            self.stack().ret_or_dead(AirAggregate::Uninit, tok)
        } else {
            self.stack().transfer_with_ret(
                Transition(st_super),
                Transition(to.into()).incomplete().with_lookahead(tok),
            )
        }
    }

    /// Attempt to return to the previous stack frame,
    ///   using the provided token as a token of lookahead.
    ///
    /// If the provided `st` is in an accepting state,
    ///   then control will return to the frame atop of the stack.
    /// Otherwise,
    ///   an unexpected token was found,
    ///   and a dead state transition will be yielded,
    ///     leaving `st` unchanged.
    fn try_ret_with_lookahead<S: Into<AirAggregate>>(
        &mut self,
        st: S,
        tok: impl Token + Into<Air>,
    ) -> TransitionResult<AirAggregate> {
        let st_super = st.into();

        if st_super.active_is_complete(self) {
            // TODO: error (this should never happen, so maybe panic instead?)
            self.stack().ret_or_dead(AirAggregate::Uninit, tok)
        } else {
            Transition(st_super).dead(tok)
        }
    }

    /// Proxy `tok` to `st`,
    ///   returning to the state atop of the stack if parsing reaches a dead
    ///   state.
    ///
    /// See also [`Self::ret_or_transfer`].
    fn proxy<S: ParseState<Super = AirAggregate, Context = Self>>(
        &mut self,
        st: S,
        tok: impl Token + Into<S::Token>,
    ) -> TransitionResult<AirAggregate> {
        st.delegate_child(tok.into(), self, |_deadst, tok, ctx| {
            // TODO: error (this should never happen, so maybe panic instead?)
            ctx.stack().ret_or_dead(AirAggregate::Uninit, tok)
        })
    }

    /// Index the provided symbol `name` as representing the
    ///   [`ObjectIndex`] in the immediate environment `imm_env`.
    ///
    /// Intersecting shadows are permitted,
    ///   but the existing index will be kept in tact.
    /// This behavior may change in the future to permit diagnostic messages
    ///   that list _all_ identifiers in the event of a shadowing error.
    ///
    /// If indexing fails,
    ///   the _existing_ [`ObjectIndex`] will be returned,
    ///     leaving the caller to determine how to react.
    fn try_index<
        O: ObjectRelatable,
        OS: ObjectIndexRelTo<O>,
        S: Into<SymbolId>,
    >(
        index: &mut ScopeIndex,
        imm_env: OS,
        name: S,
        eoi: EnvScopeKind<ObjectIndex<O>>,
    ) -> Result<(), ObjectIndex<O>> {
        let sym = name.into();
        let ient = index.entry((O::rel_ty(), sym, imm_env.widen()));

        use Entry::*;
        use EnvScopeKind::*;

        match (ient, eoi) {
            (Vacant(_), Hidden(_)) => Ok(()),

            (Vacant(ent), Shadow(_) | Visible(_)) => {
                ent.insert(eoi.map(ObjectIndex::widen));
                Ok(())
            }

            (Occupied(ent), eoi) => match (ent.get(), eoi) {
                // This ought to be omitted from the index,
                //   as shown above.
                (Hidden(oi), _) => diagnostic_unreachable!(
                    vec![oi.internal_error(
                        "this should not have been indexed as Hidden"
                    )],
                    "unexpected Hidden scope index entry",
                ),

                (Visible(_), Hidden(_)) => Ok(()),

                (Shadow(_), Hidden(_) | Shadow(_)) => Ok(()),

                (Shadow(oi), Visible(_))
                | (Visible(oi), Shadow(_) | Visible(_)) => {
                    Err(oi.must_narrow_into::<O>())
                }
            },
        }
    }

    /// Index the provided symbol `name` as representing the
    ///   [`ObjectIndex`] in the immediate environment `imm_env`.
    ///
    /// An index does not require the existence of an edge,
    ///   but an index may only be created if an edge `imm_env->oi` _could_
    ///   be constructed.
    ///
    /// This index permits `O(1)` object lookups.
    /// The term "immediate environment" is meant to convey that this index
    ///   applies only to the provided `imm_env` node and does not
    ///   propagate to any other objects that share this environment.
    ///
    /// After an object is indexed it is not expected to be re-indexed
    ///   to another node.
    /// Debug builds contain an assertion that will panic in this instance.
    fn index<O: ObjectRelatable, OS: ObjectIndexRelTo<O>, S: Into<SymbolId>>(
        index: &mut ScopeIndex,
        imm_env: OS,
        name: S,
        eoi: EnvScopeKind<ObjectIndex<O>>,
    ) {
        let sym = name.into();
        let prev = Self::try_index(index, imm_env, sym, eoi);

        // We should never overwrite indexes
        #[allow(unused_variables)] // used only for debug
        #[allow(unused_imports)]
        if let Err(prev_oi) = prev {
            use crate::fmt::{DisplayWrapper, TtQuote};
            crate::debug_diagnostic_panic!(
                vec![
                    imm_env.widen().note("at this scope boundary"),
                    prev_oi.note("previously indexed identifier was here"),
                    eoi.internal_error(
                        "this identifier has already been indexed at the above scope boundary"
                    ),
                    eoi.help(
                        "this is a bug in the system responsible for analyzing \
                            identifier scope;"
                    ),
                    eoi.help(
                        "  you can try to work around it by duplicating the definition of "
                    ),
                    eoi.help(
                        format!(
                            "  {} as a _new_ identifier with a different name.",
                            TtQuote::wrap(sym),
                        )
                    ),
                ],
                "re-indexing of identifier at scope boundary",
            );
        }
    }

    /// Create a new rooted package of the given canonical name and record
    ///   it as the active package.
    ///
    /// A canonical package name is a path relative to the project root.
    ///
    /// This operation will fail if a package of the same name has already
    ///   been declared.
    fn pkg_begin(
        &mut self,
        start: Span,
        name: SPair,
    ) -> Result<ObjectIndex<Pkg>, AsgError> {
        let oi_root = self.asg.root(start);
        let oi_pkg = self.asg.create(Pkg::new_canonical(start, name)?);
        let eoi_pkg = EnvScopeKind::Visible(oi_pkg);

        Self::try_index(&mut self.index, oi_root, name, eoi_pkg).map_err(
            |oi_prev| {
                let prev = oi_prev.resolve(&self.asg);

                // unwrap note: a canonical name must exist for this error to
                //   have been thrown,
                //     but this will at least not blow up if something really
                //     odd happens.
                AsgError::PkgRedeclare(prev.canonical_name(), name)
            },
        )?;

        oi_pkg.root(&mut self.asg);
        self.ooi_pkg.replace(oi_pkg);

        Ok(oi_pkg)
    }

    /// Indicate that there is no longer any active package.
    fn pkg_clear(&mut self) {
        self.ooi_pkg.take();
    }

    /// The active package if any.
    fn pkg_oi(&self) -> Option<ObjectIndex<Pkg>> {
        self.ooi_pkg
    }

    /// The active container (rooting context) for [`Ident`]s.
    ///
    /// The integer value returned represents the stack offset at which the
    ///   rooting index was found,
    ///     with `0` representing the package.
    ///
    /// A value of [`None`] indicates that no bindings are permitted in the
    ///   current context.
    fn rooting_oi(&self) -> Option<ObjectIndexToTree<Ident>> {
        self.stack
            .iter()
            .rev()
            .find_map(|st| st.active_rooting_oi())
    }

    /// The active dangling expression context for [`Expr`]s.
    ///
    /// A value of [`None`] indicates that expressions are not permitted to
    ///   dangle in the current context
    ///     (and so must be identified).
    fn dangling_expr_oi(&self) -> Option<ObjectIndexTo<Expr>> {
        use AirAggregate::*;

        self.stack.iter().rev().find_map(|st| match st {
            Uninit => None,

            // It should never be possible to define expressions directly in
            //   Root.
            Root(_) => None,

            // A dangling expression in a package context would be
            //   unreachable.
            // There should be no parent frame and so this will fail to find
            //   a value.
            Pkg(_) => None,

            // Expressions may always contain other expressions,
            //   and so this method should not be consulted in such a
            //   context.
            // Nonetheless,
            //   fall through to the parent frame and give a correct answer.
            PkgExpr(_) => None,

            // Templates serve as containers for dangling expressions,
            //   since they may expand into an context where they are not
            //   considered to be dangling.
            PkgTpl(tplst) => tplst.active_tpl_oi().map(Into::into),

            // No definitions can be made within metavariables.
            PkgMeta(_) => None,

            // Expressions are transparent definitions,
            //   not opaque,
            //   and so not permitted in this context.
            PkgOpaque(_) => None,
        })
    }

    /// The active expansion target (splicing context) for [`Tpl`]s.
    ///
    /// A value of [`None`] indicates that template expansion is not
    ///   permitted in this current context.
    fn expansion_oi(&self) -> Option<ObjectIndexTo<Tpl>> {
        use AirAggregate::*;

        self.stack.iter().rev().find_map(|st| match st {
            Uninit => None,
            Root(_) => None,
            Pkg(pkg_st) => pkg_st.active_pkg_oi().map(Into::into),
            PkgExpr(exprst) => exprst.active_expr_oi().map(Into::into),
            PkgTpl(tplst) => tplst.active_tpl_oi().map(Into::into),

            // Metavariables do not historically support template
            //   applications within their body.
            // Support for such a feature can be evaluated in the future.
            PkgMeta(_) => None,

            // Templates _could_ conceptually expand into opaque objects,
            //   but the source language of TAME provides no mechanism to do
            //   such a thing,
            //     and so it'd be best to leave this alone unless it's
            //     actually needed.
            PkgOpaque(_) => None,
        })
    }

    /// Root an identifier using the [`Self::rooting_oi`] atop of the stack.
    fn defines(&mut self, name: SPair) -> Result<ObjectIndex<Ident>, AsgError> {
        let oi_root = self
            .rooting_oi()
            .ok_or(AsgError::InvalidBindContext(name))?;

        Ok(self.lookup_lexical_or_missing(name).add_edge_from(
            self.asg_mut(),
            oi_root,
            None,
        ))
    }

    /// Attempt to retrieve an identifier and its scope information from the
    ///   graph by name relative to the environment `env`.
    ///
    /// See [`Self::lookup`] for more information.
    #[cfg(test)]
    fn env_scope_lookup_raw<O: ObjectRelatable>(
        &self,
        env: impl ObjectIndexRelTo<O>,
        name: SPair,
    ) -> Option<EnvScopeKind<ObjectIndex<O>>> {
        self.lookup_raw(env, name)
    }

    /// Resolve an identifier at the scope of the provided environment.
    ///
    /// If the identifier is not in scope at `env`,
    ///   [`None`] will be returned.
    #[cfg(test)]
    fn env_scope_lookup<O: ObjectRelatable>(
        &self,
        env: impl ObjectIndexRelTo<O>,
        name: SPair,
    ) -> Option<ObjectIndex<O>> {
        self.env_scope_lookup_raw(env, name)
            .and_then(EnvScopeKind::in_scope)
            .map(EnvScopeKind::into_inner)
    }

    /// Attempt to retrieve an identifier from the graph by name relative to
    ///   the immediate environment `imm_env`.
    ///
    /// Since only identifiers carry a name,
    ///   this method cannot be used to retrieve all possible objects on the
    ///   graph---for
    ///     that, see [`Asg::get`].
    #[inline]
    fn lookup<O: ObjectRelatable>(
        &self,
        imm_env: impl ObjectIndexRelTo<O>,
        id: SPair,
    ) -> Option<ObjectIndex<O>> {
        self.lookup_raw(imm_env, id)
            .and_then(EnvScopeKind::in_scope)
            .map(EnvScopeKind::into_inner)
    }

    /// Attempt to retrieve an identifier and its scope information from the
    ///   graph by name relative to the immediate environment `imm_env`.
    ///
    /// See [`Self::lookup`] for more information.
    #[inline]
    fn lookup_raw<O: ObjectRelatable>(
        &self,
        imm_env: impl ObjectIndexRelTo<O>,
        id: SPair,
    ) -> Option<EnvScopeKind<ObjectIndex<O>>> {
        // The type `O` is encoded into the index on [`Self::index`] and so
        //   should always be able to be narrowed into the expected type.
        // If this invariant somehow does not hold,
        //   then the system will panic when the object is resolved.
        // Maybe future Rust will have dependent types that allow for better
        //   static assurances.
        self.index
            .get(&(O::rel_ty(), id.symbol(), imm_env.widen()))
            .map(|&eoi| {
                eoi.map(|oi| oi.overwrite(id.span()).must_narrow_into::<O>())
            })
    }

    /// Attempt to locate a lexically scoped identifier in the current stack,
    ///   or create a new one if missing.
    ///
    /// Since shadowing is not permitted
    ///   (but local identifiers are),
    ///   we can reduce the cost of lookups for the majority of identifiers
    ///     by beginning at the root and continuing down into the narrowest
    ///     lexical scope until we find what we're looking for.
    ///
    /// Note that the global environment,
    ///   represented by the root,
    ///   is a pool of identifiers from all packages;
    ///     it does not form a hierarchy and local identifiers will not be
    ///     indexed outside of their package hierarchy,
    ///       so we'll have to continue searching for those.
    ///
    /// The provided name's span is used to seed the missing object with
    ///   some sort of context to aid in debugging why a missing object
    ///   was introduced to the graph.
    /// The provided span will be used by the returned [`ObjectIndex`] even
    ///   if an object exists on the graph,
    ///     which can be used for retaining information on the location that
    ///     requested the object.
    /// To retrieve the span of a previously declared object,
    ///   you must resolve the [`ObjectIndex`] and inspect it.
    fn lookup_lexical_or_missing(&mut self, name: SPair) -> ObjectIndex<Ident> {
        self.stack
            .iter()
            .filter_map(|st| st.active_env_oi())
            .find_map(|oi| self.lookup(oi, name))
            .unwrap_or_else(|| self.create_env_indexed_ident(name))
    }

    /// Index an identifier within its environment.
    ///
    /// TODO: More information as this is formalized.
    fn create_env_indexed_ident(&mut self, name: SPair) -> ObjectIndex<Ident> {
        let oi_ident = self.asg.create(Ident::declare(name));

        self.stack
            .iter()
            .rev()
            .filter_map(|frame| frame.active_env_oi().map(|oi| (oi, frame)))
            .fold(None, |oeoi, (imm_oi, frame)| {
                let eoi_next = oeoi
                    .map(|eoi| {
                        frame.active_env_boundary().cross_low_to_high(eoi)
                    })
                    .unwrap_or(EnvScopeKind::Visible(oi_ident));

                // TODO: Let's find this a better home.
                match eoi_next {
                    // There is no use in indexing something that will be
                    //   filtered out on retrieval.
                    EnvScopeKind::Hidden(_) => (),
                    _ => Self::index(&mut self.index, imm_oi, name, eoi_next),
                }

                Some(eoi_next)
            });

        oi_ident
    }

    /// Consume the context and yield the inner [`Asg`].
    ///
    /// This indicates that all package parsing has been completed and that
    ///   the ASG contains complete information about the program sources
    ///   for the requested compilation unit.
    pub fn finish(self) -> Asg {
        self.asg
    }
}

/// 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.
///
/// Root and Global Environment
/// ===========================
/// Identifiers are pooled without any defined hierarchy at the root.
///
/// 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.
///
/// 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.
#[derive(Debug, PartialEq, Copy, Clone)]
pub(super) enum EnvScopeKind<T = ObjectIndex<Object>> {
    /// This environment owns the identifier,
    ///   is descended from an environment that does,
    ///   or is a global pool of identifiers.
    Visible(T),

    /// 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(T),

    /// The identifier is not in scope.
    Hidden(T),
}

impl<T> EnvScopeKind<T> {
    pub fn into_inner(self) -> T {
        use EnvScopeKind::*;

        match self {
            Shadow(x) | Visible(x) | Hidden(x) => x,
        }
    }

    /// Whether this represents an identifier that is in scope.
    pub fn in_scope(self) -> Option<Self> {
        use EnvScopeKind::*;

        match self {
            Visible(_) => Some(self),
            Shadow(_) | Hidden(_) => None,
        }
    }
}

impl<T> AsRef<T> for EnvScopeKind<T> {
    fn as_ref(&self) -> &T {
        use EnvScopeKind::*;

        match self {
            Shadow(x) | Visible(x) | Hidden(x) => x,
        }
    }
}

impl<T, U> Functor<T, U> for EnvScopeKind<T> {
    type Target = EnvScopeKind<U>;

    fn map(self, f: impl FnOnce(T) -> U) -> Self::Target {
        use EnvScopeKind::*;

        match self {
            Shadow(x) => Shadow(f(x)),
            Visible(x) => Visible(f(x)),
            Hidden(x) => Hidden(f(x)),
        }
    }
}

impl<T> From<EnvScopeKind<T>> for Span
where
    T: Into<Span>,
{
    fn from(kind: EnvScopeKind<T>) -> Self {
        kind.into_inner().into()
    }
}

impl AsMut<AirAggregateCtx> for AirAggregateCtx {
    fn as_mut(&mut self) -> &mut AirAggregateCtx {
        self
    }
}

impl AsRef<Asg> for AirAggregateCtx {
    fn as_ref(&self) -> &Asg {
        &self.asg
    }
}

impl AsMut<Asg> for AirAggregateCtx {
    fn as_mut(&mut self) -> &mut Asg {
        &mut self.asg
    }
}

impl AsMut<AirStack> for AirAggregateCtx {
    fn as_mut(&mut self) -> &mut AirStack {
        &mut self.stack
    }
}

impl From<Asg> for AirAggregateCtx {
    fn from(asg: Asg) -> Self {
        Self {
            asg,
            ..Default::default()
        }
    }
}

#[cfg(test)]
pub mod test;
