tamer: asg: Integrate package CanonicalName

This change requires every package to have a canonical name, and performs
namespec canonicalization on imports.

Since all package names are canonicalized, this opens the door to being able
to index package names at import, allowing the object to be shared on the
graph and properly reference a package after it has been resolved.

Note that the system tests' canonicalization is relative to the hard-coded
`/TODO` presently; that will change in the near future once `tamec`
generates names from the provided path.

DEV-13162
main
Mike Gerwitz 2023-05-04 15:03:14 -04:00
parent a9d0f43684
commit 48bcb0cdab
10 changed files with 96 additions and 120 deletions

View File

@ -44,7 +44,7 @@ use crate::{
diagnostic_todo,
parse::{prelude::*, StateStack},
span::{Span, UNKNOWN_SPAN},
sym::{st::raw::WS_EMPTY, SymbolId},
sym::SymbolId,
};
use std::fmt::{Debug, Display};
@ -175,7 +175,7 @@ impl ParseState for AirAggregate {
// TODO: Remove; transitionary (no package name required)
(UnnamedPkg(span), tok) => {
match ctx.begin_pkg(span, SPair(WS_EMPTY, span)) {
match ctx.begin_pkg(span, SPair("/TODO".into(), span)) {
Ok(oi_pkg) => Transition(Toplevel(oi_pkg))
.incomplete()
.with_lookahead(tok),
@ -187,10 +187,7 @@ impl ParseState for AirAggregate {
// TODO: `unwrap_or` is just until canonical name is
// unconditionally available just to avoid the possibility
// of a panic.
let name = oi_pkg
.resolve(ctx.asg_mut())
.canonical_name()
.unwrap_or(rename);
let name = oi_pkg.resolve(ctx.asg_mut()).canonical_name();
Transition(Toplevel(oi_pkg))
.err(AsgError::PkgRename(name, rename))
@ -220,10 +217,10 @@ impl ParseState for AirAggregate {
}
// Package import
(Toplevel(oi_pkg), AirBind(RefIdent(pathspec))) => {
oi_pkg.import(ctx.asg_mut(), pathspec);
Transition(Toplevel(oi_pkg)).incomplete()
}
(Toplevel(oi_pkg), AirBind(RefIdent(pathspec))) => oi_pkg
.import(ctx.asg_mut(), pathspec)
.map(|_| ())
.transition(Toplevel(oi_pkg)),
// Note: We unfortunately can't match on `AirExpr | AirBind | ...`
// and delegate in the same block

View File

@ -411,7 +411,7 @@ fn nested_open_pkg() {
#[rustfmt::skip]
let toks = vec![
PkgStart(S1),
BindIdent(SPair("foo".into(), S2)),
BindIdent(SPair("/foo".into(), S2)),
// Cannot nest package
PkgStart(S3),
@ -434,7 +434,7 @@ fn nested_open_pkg() {
#[test]
fn pkg_canonical_name() {
let name = SPair("foo/bar".into(), S2);
let name = SPair("/foo/bar".into(), S2);
#[rustfmt::skip]
let toks = vec![
@ -454,7 +454,7 @@ fn pkg_canonical_name() {
.next()
.expect("cannot find package from root");
assert_eq!(Some(name), oi_pkg.resolve(&asg).canonical_name());
assert_eq!(name, oi_pkg.resolve(&asg).canonical_name());
// We should be able to find the same package by its index.
let oi_pkg_indexed = asg.lookup(oi_root, name);
@ -470,9 +470,9 @@ fn pkg_canonical_name() {
// filenames.
#[test]
fn pkg_cannot_redeclare() {
let name = SPair("foo/bar".into(), S2);
let name2 = SPair("foo/bar".into(), S5);
let namefix = SPair("foo/fix".into(), S6);
let name = SPair("/foo/bar".into(), S2);
let name2 = SPair("/foo/bar".into(), S5);
let namefix = SPair("/foo/fix".into(), S6);
#[rustfmt::skip]
let toks = vec![
@ -521,8 +521,8 @@ fn pkg_cannot_redeclare() {
#[test]
fn pkg_cannot_rename() {
let name = SPair("foo/bar".into(), S2);
let name2 = SPair("bad/rename".into(), S3);
let name = SPair("/foo/bar".into(), S2);
let name2 = SPair("/bad/rename".into(), S3);
#[rustfmt::skip]
let toks = vec![
@ -558,13 +558,15 @@ fn pkg_cannot_rename() {
}
#[test]
fn pkg_import() {
let pathspec = SPair("foo/bar".into(), S2);
fn pkg_import_canonicalized_against_current_pkg() {
let pkg_name = SPair("/foo/bar".into(), S2);
let pkg_rel = SPair("baz/quux".into(), S3);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1),
RefIdent(pathspec),
BindIdent(pkg_name),
RefIdent(pkg_rel),
PkgEnd(S3),
];
@ -583,7 +585,8 @@ fn pkg_import() {
.expect("cannot find imported package")
.resolve(&asg);
assert_eq!(Some(pathspec), import.import_path());
// TODO
assert_eq!(SPair("/foo/baz/quux".into(), S3), import.canonical_name());
}
// Documentation can be mixed in with objects in a literate style.

View File

@ -31,7 +31,9 @@ use crate::{
span::Span,
};
use super::{visit::Cycle, TransitionError};
use super::{
graph::object::pkg::CanonicalNameError, visit::Cycle, TransitionError,
};
/// An error from an ASG operation.
///
@ -61,6 +63,9 @@ pub enum AsgError {
/// whereas _declaring_ an identifier provides metadata about it.
IdentRedefine(SPair, Span),
/// Error while processing the canonical name for a package.
PkgCanonicalName(CanonicalNameError),
/// A package of this same name has already been defined.
///
/// The [`SPair`]s represent the original and redefinition names
@ -157,10 +162,11 @@ impl Display for AsgError {
use AsgError::*;
match self {
IdentTransition(err) => Display::fmt(&err, f),
IdentTransition(e) => Display::fmt(&e, f),
IdentRedefine(spair, _) => {
write!(f, "cannot redefine {}", TtQuote::wrap(spair))
}
PkgCanonicalName(e) => Display::fmt(&e, f),
PkgRedeclare(orig, _) => write!(
f,
"attempted to redeclare or redefine package {}",
@ -213,8 +219,14 @@ impl Display for AsgError {
impl Error for AsgError {}
impl From<TransitionError> for AsgError {
fn from(err: TransitionError) -> Self {
Self::IdentTransition(err)
fn from(e: TransitionError) -> Self {
Self::IdentTransition(e)
}
}
impl From<CanonicalNameError> for AsgError {
fn from(e: CanonicalNameError) -> Self {
Self::PkgCanonicalName(e)
}
}
@ -247,6 +259,8 @@ impl Diagnostic for AsgError {
.help(" defined and its definition cannot be changed."),
],
PkgCanonicalName(e) => e.describe(),
PkgRedeclare(orig, redef) => vec![
orig.note("package originally declared here"),
redef.error("attempting to redeclare or redefine package here"),

View File

@ -19,7 +19,7 @@
//! Package object on the ASG.
use super::{prelude::*, Doc, Ident, NameableMissingObject, Tpl};
use super::{prelude::*, Doc, Ident, Tpl};
use crate::{
f::Functor,
fmt::{DisplayWrapper, TtQuote},
@ -33,21 +33,34 @@ use super::ObjectKind;
mod name;
pub use name::{CanonicalName, CanonicalNameError};
#[derive(Debug, PartialEq, Eq)]
pub struct Pkg(Span, PathSpec);
pub struct Pkg(Span, CanonicalName);
impl Pkg {
/// Create a new package with a canonicalized name.
///
/// A canonical package name is a path relative to the project root.
pub(super) fn new_canonical<S: Into<Span>>(start: S, name: SPair) -> Self {
Self(start.into(), PathSpec::Canonical(name))
pub(super) fn new_canonical<S: Into<Span>>(
start: S,
name: SPair,
) -> Result<Self, AsgError> {
Ok(Self(start.into(), CanonicalName::from_canonical(name)?))
}
/// Represent a package imported according to the provided
/// [`PathSpec`].
pub fn new_imported(pathspec: SPair) -> Self {
Self(pathspec.span(), PathSpec::TodoImport(pathspec))
/// Import a package with a namespec canonicalized against a reference
/// parent package.
///
/// The parent package should be the package containing the namespec.
pub fn new_imported(
Pkg(_, parent_name): &Pkg,
namespec: SPair,
) -> Result<Self, AsgError> {
Ok(Self(
namespec.span(),
CanonicalName::resolve_namespec_rel(parent_name, namespec)?,
))
}
pub fn span(&self) -> Span {
@ -59,31 +72,18 @@ impl Pkg {
/// The canonical name for this package assigned by
/// [`Self::new_canonical`],
/// if any.
pub fn canonical_name(&self) -> Option<SPair> {
pub fn canonical_name(&self) -> SPair {
match self {
Self(_, pathspec) => pathspec.canonical_name(),
}
}
/// The import path to this package as provided by
/// [`Self::new_imported`],
/// if any.
pub fn import_path(&self) -> Option<SPair> {
match self {
Self(_, pathspec) => pathspec.import_path(),
Self(_, name) => (*name).into(),
}
}
}
impl Display for Pkg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "package")
}
}
impl NameableMissingObject for Pkg {
fn missing(pathspec: SPair) -> Self {
Self::new_imported(pathspec)
match self {
Self(_, name) => write!(f, "package {}", TtQuote::wrap(name)),
}
}
}
@ -95,62 +95,6 @@ impl Functor<Span> for Pkg {
}
}
#[derive(Debug, PartialEq, Eq)]
enum PathSpec {
/// Canonical package name.
///
/// This is the name of the package relative to the project root.
/// This is like the module name after `crate::` in Rust,
/// but with `/` package separators in place of `::`.
Canonical(SPair),
/// Import path relative to the current package
/// (which is likely the compilation unit).
///
/// TODO: This will be replaced with [`Self::Canonical`] once that is
/// working and relative paths can be resolved against the active
/// package.
TodoImport(SPair),
}
impl PathSpec {
fn canonical_name(&self) -> Option<SPair> {
use PathSpec::*;
match self {
Canonical(spair) => Some(*spair),
TodoImport(_) => None,
}
}
fn import_path(&self) -> Option<SPair> {
use PathSpec::*;
match self {
// TODO: Let's wait to see if we actually need this,
// since we'll need to allocate and intern a `/`-prefixed
// symbol.
Canonical(_) => None,
TodoImport(spair) => Some(*spair),
}
}
}
impl Display for PathSpec {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use PathSpec::*;
match self {
Canonical(spair) => write!(f, "package {}", TtQuote::wrap(spair)),
TodoImport(spair) => write!(
f,
"package import {} relative to compilation unit",
TtQuote::wrap(spair)
),
}
}
}
object_rel! {
/// Packages serve as a root for all identifiers defined therein,
/// and so an edge to [`Ident`] will never be a cross edge.
@ -182,9 +126,15 @@ impl ObjectIndex<Pkg> {
///
/// This simply adds the import to the graph;
/// package loading must be performed by another subsystem.
pub fn import(self, asg: &mut Asg, pathspec: SPair) -> Self {
let oi_import = asg.create(Pkg::new_imported(pathspec));
self.add_edge_to(asg, oi_import, Some(pathspec.span()))
pub fn import(
self,
asg: &mut Asg,
namespec: SPair,
) -> Result<Self, AsgError> {
let parent = self.resolve(asg);
let oi_import = asg.create(Pkg::new_imported(parent, namespec)?);
Ok(self.add_edge_to(asg, oi_import, Some(namespec.span())))
}
/// Arbitrary text serving as documentation in a literate style.

View File

@ -325,6 +325,14 @@ fn validate_components(
.map(|_| name)
}
impl Display for CanonicalName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self(name) => Display::fmt(name, f),
}
}
}
impl From<CanonicalName> for SPair {
fn from(value: CanonicalName) -> Self {
match value {

View File

@ -109,7 +109,7 @@ impl ObjectIndex<Root> {
start: Span,
name: SPair,
) -> Result<ObjectIndex<Pkg>, AsgError> {
let oi_pkg = asg.create(Pkg::new_canonical(start, name));
let oi_pkg = asg.create(Pkg::new_canonical(start, name)?);
asg.try_index(self, name, oi_pkg).map_err(|oi_prev| {
let prev = oi_prev.resolve(asg);
@ -118,7 +118,7 @@ impl ObjectIndex<Root> {
// have been thrown,
// but this will at least not blow up if something really
// odd happens.
AsgError::PkgRedeclare(prev.canonical_name().unwrap_or(name), name)
AsgError::PkgRedeclare(prev.canonical_name(), name)
})?;
Ok(oi_pkg.root(asg))

View File

@ -301,8 +301,8 @@ fn omits_unreachable() {
// roots since that is the entry point for this API.
#[test]
fn sorts_objects_given_multiple_roots() {
let pkg_a_name = SPair("pkg/a".into(), S2);
let pkg_b_name = SPair("pkg/b".into(), S8);
let pkg_a_name = SPair("/pkg/a".into(), S2);
let pkg_b_name = SPair("/pkg/b".into(), S8);
let id_a = SPair("expr_a".into(), S4);
let id_b = SPair("expr_b".into(), S10);

View File

@ -235,8 +235,10 @@ impl<'a> TreeContext<'a> {
}
/// Emit a package import statement.
///
/// The import will have its path canonicalized.
fn emit_import(&mut self, pkg: &Pkg, depth: Depth) -> Option<Xirf> {
let ps = pkg.import_path()?;
let ps = pkg.canonical_name();
self.push(Xirf::attr(QN_PACKAGE, ps.symbol(), (ps.span(), ps.span())));
Some(Xirf::open(

View File

@ -5,8 +5,8 @@
<import package="first" />
<import package="second" />
<import package="third" />
<import package="/first/pkg" />
<import package="/second" />
<import package="/third" />
</package>

View File

@ -5,7 +5,9 @@
Packages aren't actually imported yet,
but they do need to be represented on the graph for `xmli` derivation.
<import package="first" />
Import paths will be canonicalized.
<import package="first/pkg" />
<import package="second" />
<import package="third" />
</package>