Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions crates/graphcal-compiler/src/hir/expr_lower/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::resolved_name::{ResolvedDeclName, ResolvedIndexVariant};

use crate::builtin::{BuiltinConst, BuiltinFn};
use crate::desugar::desugared_ast as ast;
use crate::resolve::SourcePathBoundary;
use crate::resolve::category::{DeclSymbolKind, SymbolTable};
use crate::resolve::error::{ExpectedDeclKind, ModuleResolveError, NameCategory};
use crate::resolve::namespace::Namespace;
Expand Down Expand Up @@ -56,6 +57,7 @@ struct SourceDeclTarget<'r> {
symbol: Option<DeclSymbol<'r>>,
/// Whether the path was bound by the owner's lexical binding table.
bound: bool,
boundary: GraphRefBoundary,
}

/// The scope boundary a graph reference crosses. It decides which
Expand All @@ -74,6 +76,13 @@ pub(super) enum GraphRefBoundary {
}

impl GraphRefBoundary {
const fn from_source(boundary: SourcePathBoundary) -> Self {
match boundary {
SourcePathBoundary::Local => Self::Local,
SourcePathBoundary::ThroughAlias(role) => Self::through_alias(role),
}
}

/// The boundary of a reference qualified by a module alias of `role`.
pub(super) const fn through_alias(role: ModuleAliasRole) -> Self {
match role {
Expand Down Expand Up @@ -360,8 +369,9 @@ impl<'a> ExprLowerer<'a> {
resolved,
symbol: None,
bound: true,
boundary: GraphRefBoundary::IncludedInstance,
};
self.check_graph_ref_boundary(target, GraphRefBoundary::IncludedInstance, *span)
self.check_graph_ref_boundary(target, *span)
}
}
}
Expand All @@ -375,6 +385,16 @@ impl<'a> ExprLowerer<'a> {
let span = reference.span;
let path = reference.value.to_name_path();
let target = self.resolve_source_decl(&path, span)?;
self.check_graph_ref_boundary(target, span)
}

/// Only elaborated lexical bindings absent from the resolver need their
/// source boundary classified separately.
fn bound_source_graph_boundary(
&self,
path: &NamePath,
span: Span,
) -> Result<GraphRefBoundary, ExprLowerError> {
let boundary = match path.qualifier().first() {
None => GraphRefBoundary::Local,
Some(head) => {
Expand All @@ -398,13 +418,12 @@ impl<'a> ExprLowerer<'a> {
}
}
};
self.check_graph_ref_boundary(target, boundary, span)
Ok(boundary)
}

fn check_graph_ref_boundary(
&self,
target: SourceDeclTarget<'a>,
boundary: GraphRefBoundary,
span: Span,
) -> Result<Spanned<ResolvedDeclName>, ExprLowerError> {
let actual = match target
Expand All @@ -417,7 +436,8 @@ impl<'a> ExprLowerer<'a> {
None if target.bound => return Ok(Spanned::new(target.resolved, span)),
None => return Err(unknown_decl(&target.resolved, span)),
};
boundary
target
.boundary
.check(actual)
.map_err(|expected| ExprLowerError::ModuleResolve {
source: ModuleResolveError::UnexpectedDeclKind {
Expand All @@ -443,20 +463,32 @@ impl<'a> ExprLowerer<'a> {
.ctx
.scope
.resolver
.resolve_decl_path(self.ctx.scope.owner, path);
.resolve_source_decl_path(self.ctx.scope.owner, path);
if let Some(resolved) = self.bound_decl(&name) {
let symbol = self.check_bound_source_access(path, lookup, &resolved, span)?;
let (symbol, boundary) = match lookup {
Ok(resolution) => (
Some(resolution.symbol),
GraphRefBoundary::from_source(resolution.boundary),
),
Err(source) => {
let symbol =
self.check_bound_source_access(path, Err(source), &resolved, span)?;
(symbol, self.bound_source_graph_boundary(path, span)?)
}
};
return Ok(SourceDeclTarget {
resolved,
symbol,
bound: true,
boundary,
});
}
match lookup {
Ok(symbol) => Ok(SourceDeclTarget {
resolved: symbol.into_resolved(),
symbol: Some(symbol),
Ok(resolution) => Ok(SourceDeclTarget {
resolved: resolution.symbol.into_resolved(),
symbol: Some(resolution.symbol),
bound: false,
boundary: GraphRefBoundary::from_source(resolution.boundary),
}),
Err(ModuleResolveError::UnknownName { .. }) => {
Err(ExprLowerError::UnknownGraphRef { name, span })
Expand Down
66 changes: 66 additions & 0 deletions crates/graphcal-compiler/src/hir/expr_lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,72 @@ fn include_output_ref_resolves_only_through_instance_bindings() {
));
}

#[test]
fn source_graph_refs_keep_alias_rules_with_and_without_overlay_bindings() {
use crate::resolve::error::ExpectedDeclKind;

let lib_id = DagId::root_in_package("test", "lib");
let main_id = DagId::root_in_package("test", "main");
let lib = desugared_source("pub node p: Dimensionless = 1.0;");
let main = desugared_source(
"import lib as imported;
include lib() as included;
node local: Dimensionless = 0.0;
node imported_value: Dimensionless = @imported::p;
node included_value: Dimensionless = @included::p;
node imported_overlay: Dimensionless = @imported::generated;
node included_overlay: Dimensionless = @included::generated;",
);
let resolver = resolver_with_import(&lib_id, &main_id, &lib, &main);
let scope = GenericScope::new();
let time_zones = TimeZoneRegistry::bundled();
let module = ModuleScope::new(&main_id, &resolver, &scope);
let no_units = HashMap::new();
let no_templates = HashMap::new();
let target = ResolvedDeclName::for_test(main_id.clone(), DeclName::expect_valid("local"));
let bindings = HashMap::from_iter(["imported", "included"].map(|alias| {
(
ScopedName::in_scope(
crate::syntax::module_name::ModuleAliasName::expect_valid(alias),
DeclName::expect_valid("generated"),
),
target.clone(),
)
}));

for (name, allowed) in [
("imported_value", false),
("included_value", true),
("imported_overlay", false),
("included_overlay", true),
] {
let result = lower_expr(
node_value(&main, name),
ExprLoweringContext::with_overlay(
module,
&time_zones,
BindingOverlay::Frozen(FrozenBindings {
unit_bindings: &no_units,
decl_bindings: &bindings,
instance_templates: &no_templates,
}),
),
);
match (allowed, result) {
(true, Ok(body)) => assert!(matches!(body.kind(), ExprKind::GraphRef(_))),
(false, Err(ExprLowerError::ModuleResolve { source, .. })) => assert!(matches!(
source,
ModuleResolveError::UnexpectedDeclKind {
expected: ExpectedDeclKind::InstanceIndependentConst,
actual: DeclSymbolKind::Node,
..
}
)),
(_, result) => panic!("unexpected result for {name}: {result:?}"),
}
}
}

#[test]
fn qualified_source_graph_ref_requires_a_module_alias() {
let owner = DagId::root_in_package("test", "main");
Expand Down
36 changes: 32 additions & 4 deletions crates/graphcal-compiler/src/resolve/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use super::category::{DeclSymbolKind, SurfaceNameKind};
use super::error::{ExpectedDeclKind, ModuleResolveError, NameCategory};
use super::exports::{ExportedBinding, ExportedBindingTarget, ExportedImportItem};
use super::namespace::Namespace;
use super::scope::Access;
use super::scope::{Access, ModuleAliasRole};
use super::source_path::{SourcePathBoundary, SourcePathResolution};
use super::symbols::{ConstructorSignature, GenericParamSignature, Symbol, SymbolRef};
use super::tables::NamespaceTables;
use super::{ModuleEntry, ModuleResolver};
Expand All @@ -27,6 +28,7 @@ use super::{ModuleEntry, ModuleResolver};
struct ResolvedModuleQualifier {
owner: DagId,
access: Access,
role: ModuleAliasRole,
}

impl super::ModuleRef<'_> {
Expand Down Expand Up @@ -177,6 +179,19 @@ impl ModuleResolver {
self.resolve_symbol_path::<DeclNameNamespace>(owner, path)
}

/// Resolve a source declaration path without discarding its alias role.
///
/// The role comes from the same alias lookup that enforces visibility;
/// consumers must not reconstruct it from the symbol's canonical owner.
pub(crate) fn resolve_source_decl_path(
&self,
owner: &DagId,
path: &NamePath,
) -> Result<SourcePathResolution<'_>, ModuleResolveError> {
self.resolve_symbol_path_with_boundary::<DeclNameNamespace>(owner, path)
.map(|(symbol, boundary)| SourcePathResolution { symbol, boundary })
}

/// Resolve a declaration path and require that it names a const declaration.
pub(crate) fn resolve_const_decl_path(
&self,
Expand Down Expand Up @@ -523,14 +538,23 @@ impl ModuleResolver {
owner: &DagId,
path: &NamePath,
) -> Result<SymbolRef<'_, Ns, Ns::Declared>, ModuleResolveError> {
self.resolve_symbol_path_with_boundary::<Ns>(owner, path)
.map(|(symbol, _)| symbol)
}

fn resolve_symbol_path_with_boundary<Ns: NamespaceTables>(
&self,
owner: &DagId,
path: &NamePath,
) -> Result<(SymbolRef<'_, Ns, Ns::Declared>, SourcePathBoundary), ModuleResolveError> {
let Some((qualifier, leaf)) = path.qualifier_and_leaf() else {
let atom = path.leaf();
let name = NameDef::<Ns>::classify(atom.clone());
if let Some(symbol) = Ns::declared(self.module_symbols(owner)?).get(&name) {
return Ok(SymbolRef::new(symbol));
return Ok((SymbolRef::new(symbol), SourcePathBoundary::Local));
}
if let Some(imported) = Ns::selected(self.module_scope(owner)?).get(&name) {
return Ok(SymbolRef::new(imported));
return Ok((SymbolRef::new(imported), SourcePathBoundary::Local));
}
if let Some(actual) = self.visible_surface_kind(owner, Ns::NAMESPACE, atom, false)? {
return Err(ModuleResolveError::WrongUniverseName {
Expand Down Expand Up @@ -565,7 +589,10 @@ impl ModuleResolver {
name: leaf.clone(),
});
}
return Ok(SymbolRef::new(symbol));
return Ok((
SymbolRef::new(symbol),
SourcePathBoundary::ThroughAlias(target_ref.role),
));
}

if let Some(actual) =
Expand Down Expand Up @@ -660,6 +687,7 @@ impl ModuleResolver {
Ok(ResolvedModuleQualifier {
owner: target,
access: alias_target.access,
role: alias_target.role(),
})
} else {
Err(ModuleResolveError::UnknownModule { owner: target })
Expand Down
2 changes: 2 additions & 0 deletions crates/graphcal-compiler/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod prelude;
mod projection;
pub mod reserved_name;
pub mod scope;
mod source_path;
pub mod symbols;
pub mod tables;
#[cfg(test)]
Expand All @@ -58,6 +59,7 @@ use self::error::ModuleResolveError;
pub use self::module_table::ModuleHandle;
use self::module_table::ModuleTable;
use self::scope::{ModuleAliasRole, ModuleAliasTarget, ModuleScope, PluginAliasTarget};
pub(crate) use self::source_path::SourcePathBoundary;
use self::symbols::{ModuleSymbols, SymbolRef};
use self::tables::NamespaceTables;

Expand Down
20 changes: 20 additions & 0 deletions crates/graphcal-compiler/src/resolve/source_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! Facts preserved by a source declaration-path lookup.

use crate::syntax::decl_name::DeclNameNamespace;

use super::category::DeclSymbolKind;
use super::scope::ModuleAliasRole;
use super::symbols::SymbolRef;

/// The source boundary used to reach a symbol, independent of its canonical owner.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourcePathBoundary {
Local,
ThroughAlias(ModuleAliasRole),
}

/// A declaration lookup together with the boundary traversed by its source path.
pub struct SourcePathResolution<'r> {
pub symbol: SymbolRef<'r, DeclNameNamespace, DeclSymbolKind>,
pub boundary: SourcePathBoundary,
}
54 changes: 54 additions & 0 deletions crates/graphcal-compiler/src/resolve/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,60 @@ fn first_dag(file: &ast::File) -> &ast::DagDecl {
.expect("source should contain a dag")
}

#[test]
fn source_decl_resolution_preserves_local_and_alias_boundaries() {
use super::SourcePathBoundary;
use super::scope::ModuleAliasRole;

let lib = desugared_source(
"pub const node C: Dimensionless = 1.0;
pub dag nested { pub node value: Dimensionless = 2.0; }",
);
let main = desugared_source(
"import lib as imported;
import lib::{ C };
include lib() as included;
node local: Dimensionless = 0.0;",
);
let lib_id = DagId::root_in_package("test", "lib");
let main_id = DagId::root_in_package("test", "main");
let mut modules = TestModules::default();
modules.add_file(&lib_id, &lib.declarations);
modules.add_file(&main_id, &main.declarations);
modules.edge(&main_id, &module_path(&["lib"]), &lib_id);
let resolver = modules.build().unwrap();

for (segments, boundary) in [
(vec!["local"], SourcePathBoundary::Local),
// A selective import is local even though its canonical owner is lib.
(vec!["C"], SourcePathBoundary::Local),
(
vec!["imported", "C"],
SourcePathBoundary::ThroughAlias(ModuleAliasRole::ImportedDag),
),
(
vec!["imported", "nested", "value"],
SourcePathBoundary::ThroughAlias(ModuleAliasRole::ImportedDag),
),
(
vec!["included", "C"],
SourcePathBoundary::ThroughAlias(ModuleAliasRole::IncludedInstance),
),
] {
let path = path(&segments);
let resolution = resolver.resolve_source_decl_path(&main_id, &path).unwrap();
assert_eq!(resolution.boundary, boundary, "{path}");
assert_eq!(
resolution.symbol.resolved(),
resolver
.resolve_decl_path(&main_id, &path)
.unwrap()
.resolved(),
"{path}"
);
}
}

#[test]
fn local_type_index_name_collision_is_rejected() {
let owner = DagId::root_in_package("test", "main");
Expand Down
Loading
Loading