Resolve fully qualified names (#4056)

Added a separate pass, `FullyQualifiedNames`, that partially resolves fully qualified names. The pass only resolves the library part of the name and replaces it with a reference to the `Main` module.

There are 2 scenarios that could be potentially:
1) the code uses a fully qualified name to a component that has been
parsed/compiled
2) the code uses a fully qualified name to a component that has **not** be
imported

For the former case, it is sufficient to just check `PackageRepository` for the presence of the library name.
In the latter we have to ensure that the library has been already parsed and all its imports are resolved. That would require the reference to `Compiler` in the `FullyQualifiedNames` pass, which could then trigger a full compilation for missing library. Since it has some undesired consequences (tracking of dependencies becomes rather complex) we decided to exclude that scenario until it is really needed.

# Important Notes
With this change, one can use a fully qualified name directly.
e.g.
```
import Standard.Base
main =
Standard.Base.IO.println "Hello world!"
```
This commit is contained in:
Hubert Plociniczak 2023-01-18 21:19:36 +01:00 committed by GitHub
parent ed859c2682
commit d463a43633
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 512 additions and 57 deletions

View File

@ -517,6 +517,7 @@
- [Introducing Meta.atom_with_hole][4023]
- [Report failures in name resolution in type signatures][4030]
- [Attach visualizations to sub-expressions][4048]
- [Resolve Fully Qualified Names][4056]
[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
@ -602,6 +603,7 @@
[4023]: https://github.com/enso-org/enso/pull/4023
[4030]: https://github.com/enso-org/enso/pull/4030
[4048]: https://github.com/enso-org/enso/pull/4048
[4056]: https://github.com/enso-org/enso/pull/4056
# Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -242,11 +242,12 @@ public final class EnsoLanguage extends TruffleLanguage<EnsoContext> {
);
var inlineContext = new InlineContext(
module,
redirectConfigWithStrictErrors,
scala.Some.apply(localScope),
scala.Some.apply(false),
scala.Option.empty(),
scala.Option.empty(),
redirectConfigWithStrictErrors
scala.Option.empty()
);
Compiler silentCompiler = context.getCompiler().duplicateWithConfig(redirectConfigWithStrictErrors);
scala.Option<ExpressionNode> exprNode;

View File

@ -322,7 +322,8 @@ class Compiler(
val moduleContext = ModuleContext(
module = module,
freshNameSupply = Some(freshNameSupply),
compilerConfig = config
compilerConfig = config,
pkgRepo = Some(packageRepository)
)
val compilerOutput = runMethodBodyPasses(module.getIr, moduleContext)
module.unsafeSetIr(compilerOutput)

View File

@ -53,6 +53,9 @@ trait PackageRepository {
libraryName: LibraryName
): Either[PackageRepository.Error, Unit]
/** Checks if the library has already been loaded */
def isPackageLoaded(libraryName: LibraryName): Boolean
/** Get a sequence of currently loaded packages. */
def getLoadedPackages: Seq[Package[TruffleFile]]
@ -107,6 +110,9 @@ trait PackageRepository {
/** Modifies package and module names to reflect the project name change. */
def renameProject(namespace: String, oldName: String, newName: String): Unit
/** Checks if any library with a given namespace has been registered */
def isNamespaceRegistered(namespace: String): Boolean
}
object PackageRepository {
@ -526,6 +532,11 @@ object PackageRepository {
}
}
/** @inheritdoc */
def isPackageLoaded(libraryName: LibraryName): Boolean = {
loadedPackages.keySet.contains(libraryName)
}
/** @inheritdoc */
override def getLoadedModules: Seq[Module] =
loadedModules.values.toSeq
@ -627,6 +638,9 @@ object PackageRepository {
loadedModules.put(module.getName.toString, module)
}
}
override def isNamespaceRegistered(namespace: String): Boolean =
loadedPackages.keySet.exists(_.namespace == namespace)
}
/** Creates a [[PackageRepository]] for the run.

View File

@ -60,6 +60,7 @@ class Passes(
List(
ExpressionAnnotations,
AliasAnalysis,
FullyQualifiedNames,
GlobalNames,
TypeNames,
MethodCalls,

View File

@ -1,5 +1,6 @@
package org.enso.compiler.context
import org.enso.compiler.PackageRepository
import org.enso.compiler.data.CompilerConfig
import org.enso.compiler.pass.PassConfiguration
import org.enso.interpreter.node.BaseNode.TailStatus
@ -10,20 +11,22 @@ import org.enso.interpreter.runtime.scope.{LocalScope, ModuleScope}
* expression.
*
* @param module the module in which the expression is being executed
* @param compilerConfig the compiler configuration
* @param localScope the local scope in which the expression is being executed
* @param isInTailPosition whether or not the inline expression occurs in tail
* position ([[None]] indicates no information)
* @param freshNameSupply the compiler's supply of fresh names
* @param passConfiguration the pass configuration
* @param compilerConfig the compiler configuration
* @param pkgRepo the compiler's package repository
*/
case class InlineContext(
module: Module,
compilerConfig: CompilerConfig,
localScope: Option[LocalScope] = None,
isInTailPosition: Option[Boolean] = None,
freshNameSupply: Option[FreshNameSupply] = None,
passConfiguration: Option[PassConfiguration] = None,
compilerConfig: CompilerConfig
pkgRepo: Option[PackageRepository] = None
)
object InlineContext {
@ -63,7 +66,8 @@ object InlineContext {
isInTailPosition = None,
freshNameSupply = moduleContext.freshNameSupply,
passConfiguration = moduleContext.passConfiguration,
compilerConfig = moduleContext.compilerConfig
compilerConfig = moduleContext.compilerConfig,
pkgRepo = moduleContext.pkgRepo
)
}
}

View File

@ -1,5 +1,6 @@
package org.enso.compiler.context
import org.enso.compiler.PackageRepository
import org.enso.compiler.data.CompilerConfig
import org.enso.compiler.pass.PassConfiguration
import org.enso.interpreter.runtime.Module
@ -10,11 +11,14 @@ import org.enso.interpreter.runtime.Module
* @param freshNameSupply the compiler's supply of fresh names
* @param passConfiguration the pass configuration
* @param compilerConfig the compiler configuration
* @param isGeneratingDocs if true, should generate docs for IR
* @param pkgRepo the compiler's package repository
*/
case class ModuleContext(
module: Module,
compilerConfig: CompilerConfig,
freshNameSupply: Option[FreshNameSupply] = None,
passConfiguration: Option[PassConfiguration] = None,
isGeneratingDocs: Boolean = false,
compilerConfig: CompilerConfig
pkgRepo: Option[PackageRepository] = None
)

View File

@ -7395,6 +7395,13 @@ object IR {
}
}
case class MissingLibraryImportInFQNError(namespace: String)
extends Reason {
override def explain(originalName: IR.Name): String =
s"Fully qualified name references a library $namespace.${originalName.name} but an import statement for it is missing."
}
}
/** A representation of an error resulting from wrong pattern matches.

View File

@ -22,7 +22,7 @@ case object Imports extends IRPass {
/** The passes that are invalidated by running this pass. */
override val invalidatedPasses: Seq[IRPass] = Seq()
private val mainModuleName =
val mainModuleName =
IR.Name.Literal(
"Main",
isMethod = false,

View File

@ -0,0 +1,367 @@
package org.enso.compiler.pass.resolve
import org.enso.compiler.{Compiler, PackageRepository}
import org.enso.compiler.context.{FreshNameSupply, InlineContext, ModuleContext}
import org.enso.compiler.core.IR
import org.enso.compiler.core.IR.Error.Resolution.MissingLibraryImportInFQNError
import org.enso.compiler.core.ir.MetadataStorage.ToPair
import org.enso.compiler.data.BindingsMap
import org.enso.compiler.data.BindingsMap.{ModuleReference, Resolution}
import org.enso.compiler.exception.CompilerError
import org.enso.compiler.pass.IRPass
import org.enso.compiler.pass.analyse.{AliasAnalysis, BindingAnalysis}
import org.enso.compiler.pass.desugar.Imports
import org.enso.editions.LibraryName
/** Partially resolves fully qualified names corresponding to the library names
*
* 1. Identifies potential library names e.g., `Standard.Base`
* 2. If the component has not be compiled yet, compilation is triggered
* 3. Replaces the library name with a fresh name and a resolved Main module
*/
case object FullyQualifiedNames extends IRPass {
/** The type of the metadata object that the pass writes to the IR. */
override type Metadata = FullyQualifiedNames.FQNResolution
/** The type of configuration for the pass. */
override type Config = IRPass.Configuration.Default
/** The passes that this pass depends _directly_ on to run. */
override val precursorPasses: Seq[IRPass] =
Seq(AliasAnalysis, BindingAnalysis)
/** The passes that are invalidated by running this pass. */
override val invalidatedPasses: Seq[IRPass] = Nil
/** Executes the pass on the provided `ir`, and returns a possibly transformed
* or annotated version of `ir`.
*
* @param ir the Enso IR to process
* @param moduleContext a context object that contains the information needed
* to process a module
* @return `ir`, possibly having made transformations or annotations to that
* IR.
*/
override def runModule(
ir: IR.Module,
moduleContext: ModuleContext
): IR.Module = {
val scopeMap = ir.unsafeGetMetadata(
BindingAnalysis,
"No binding analysis on the module"
)
val freshNameSupply = moduleContext.freshNameSupply.getOrElse(
throw new CompilerError(
"No fresh name supply passed to UppercaseNames resolver."
)
)
val new_bindings =
ir.bindings.map(
processModuleDefinition(
_,
scopeMap,
freshNameSupply,
moduleContext.pkgRepo
)
)
ir.copy(bindings = new_bindings)
}
/** Executes the pass on the provided `ir`, and returns a possibly transformed
* or annotated version of `ir` in an inline context.
*
* @param ir the Enso IR to process
* @param inlineContext a context object that contains the information needed
* for inline evaluation
* @return `ir`, possibly having made transformations or annotations to that
* IR.
*/
override def runExpression(
ir: IR.Expression,
inlineContext: InlineContext
): IR.Expression = {
val scopeMap = inlineContext.module.getIr.unsafeGetMetadata(
BindingAnalysis,
"No binding analysis on the module"
)
val freshNameSupply = inlineContext.freshNameSupply.getOrElse(
throw new CompilerError(
"No fresh name supply passed to UppercaseNames resolver."
)
)
processExpression(
ir,
scopeMap,
freshNameSupply,
None,
inlineContext.pkgRepo
)
}
private def processModuleDefinition(
definition: IR.Module.Scope.Definition,
bindings: BindingsMap,
freshNameSupply: FreshNameSupply,
pkgRepo: Option[PackageRepository]
): IR.Module.Scope.Definition = {
definition match {
case asc: IR.Type.Ascription => asc
case method: IR.Module.Scope.Definition.Method =>
val resolution = method.methodReference.typePointer.flatMap(
_.getMetadata(MethodDefinitions)
)
method.mapExpressions(
processExpression(_, bindings, freshNameSupply, resolution, pkgRepo)
)
case tp: IR.Module.Scope.Definition.Type =>
tp.copy(members =
tp.members.map(
_.mapExpressions(
processExpression(
_,
bindings,
freshNameSupply,
bindings.resolveName(tp.name.name).toOption.map(Resolution),
pkgRepo
)
)
)
)
case a =>
a.mapExpressions(
processExpression(_, bindings, freshNameSupply, None, pkgRepo)
)
}
}
private def processExpression(
ir: IR.Expression,
bindings: BindingsMap,
freshNameSupply: FreshNameSupply,
selfTypeResolution: Option[Resolution],
pkgRepo: Option[PackageRepository]
): IR.Expression =
ir.transformExpressions {
case lit: IR.Name.Literal =>
if (!lit.isMethod && !isLocalVar(lit)) {
val resolution = bindings.resolveName(lit.name)
resolution match {
case Left(_) =>
if (
pkgRepo
.map(_.isNamespaceRegistered(lit.name))
.getOrElse(false)
) {
lit.updateMetadata(
this -->> FQNResolution(ResolvedLibrary(lit.name))
)
} else {
lit
}
case Right(_) =>
lit
}
} else {
lit
}
case app @ IR.Application.Prefix(_, List(_), _, _, _, _) =>
app.function match {
case lit: IR.Name.Literal =>
if (lit.isMethod)
resolveLocalApplication(
app,
bindings,
freshNameSupply,
pkgRepo,
selfTypeResolution
)
else
app.mapExpressions(
processExpression(
_,
bindings,
freshNameSupply,
selfTypeResolution,
pkgRepo
)
)
case _ =>
app.mapExpressions(
processExpression(
_,
bindings,
freshNameSupply,
selfTypeResolution,
pkgRepo
)
)
}
}
private def resolveLocalApplication(
app: IR.Application.Prefix,
bindings: BindingsMap,
freshNameSupply: FreshNameSupply,
pkgRepo: Option[PackageRepository],
selfTypeResolution: Option[Resolution]
): IR.Expression = {
val processedFun =
processExpression(
app.function,
bindings,
freshNameSupply,
selfTypeResolution,
pkgRepo
)
val processedArgs =
app.arguments.map(
_.mapExpressions(
processExpression(
_,
bindings,
freshNameSupply,
selfTypeResolution,
pkgRepo
)
)
)
val processedApp = processedArgs match {
case List(thisArg) =>
(thisArg.value.getMetadata(this).map(_.target), processedFun) match {
case (Some(resolved @ ResolvedLibrary(_)), name: IR.Name.Literal) =>
resolveQualName(resolved, name, pkgRepo).fold(
err => Some(err),
_.map(resolvedMod =>
freshNameSupply
.newName()
.updateMetadata(this -->> resolvedMod)
.setLocation(name.location)
)
)
case _ =>
None
}
case _ =>
None
}
processedApp.getOrElse(
app.copy(function = processedFun, arguments = processedArgs)
)
}
private def resolveQualName(
thisResolution: ResolvedLibrary,
consName: IR.Name.Literal,
optPkgRepo: Option[PackageRepository]
): Either[IR.Expression, Option[FQNResolution]] = {
optPkgRepo
.flatMap { pkgRepo =>
val libName = LibraryName(thisResolution.namespace, consName.name)
if (pkgRepo.isPackageLoaded(libName)) {
pkgRepo
.getLoadedModule(
s"${libName.toString}.${Imports.mainModuleName.name}"
)
.map { m =>
if (m.getIr == null) {
// Limitation of Fully Qualified Names:
// If the library has not been imported explicitly, then we won't have
// IR for it. Triggering a full compilation at this stage may have
// undesired consequences and is therefore prohibited on purpose.
Left(
IR.Error.Resolution(
consName,
MissingLibraryImportInFQNError(thisResolution.namespace)
)
)
} else {
Right(
Some(
FQNResolution(ResolvedModule(ModuleReference.Concrete(m)))
)
)
}
}
} else {
Some(
Left(
IR.Error.Resolution(
consName,
MissingLibraryImportInFQNError(thisResolution.namespace)
)
)
)
}
}
.getOrElse(Right(None))
}
/** Updates the metadata in a copy of the IR when updating that metadata
* requires global state.
*
* This is usually the case in the presence of structures that are shared
* throughout the IR, and need to maintain that sharing for correctness. This
* must be called with `copyOfIr` as the result of an `ir.duplicate` call.
*
* Additionally this method _must not_ alter the structure of the IR. It
* should only update its metadata.
*
* @param sourceIr the IR being copied
* @param copyOfIr a duplicate of `sourceIr`
* @tparam T the concrete [[IR]] type
* @return the result of updating metadata in `copyOfIr` globally using
* information from `sourceIr`
*/
override def updateMetadataInDuplicate[T <: IR](sourceIr: T, copyOfIr: T): T =
copyOfIr
private def isLocalVar(name: IR.Name.Literal): Boolean = {
val aliasInfo = name
.unsafeGetMetadata(
AliasAnalysis,
"no alias analysis info on a name"
)
.unsafeAs[AliasAnalysis.Info.Occurrence]
val defLink = aliasInfo.graph.defLinkFor(aliasInfo.id)
defLink.isDefined
}
/** The FQN resolution metadata for a node.
*
* @param target the partially resolved name
*/
sealed case class FQNResolution(target: PartiallyResolvedFQN)
extends IRPass.Metadata {
override val metadataName: String =
"FullyQualifiedNames.Resolution"
/** @inheritdoc */
override def prepareForSerialization(compiler: Compiler): FQNResolution =
this
/** @inheritdoc */
override def restoreFromSerialization(
compiler: Compiler
): Option[FQNResolution] =
Some(this)
/** @inheritdoc */
override def duplicate(): Option[IRPass.Metadata] = Some(this)
}
sealed trait PartiallyResolvedFQN
case class ResolvedLibrary(namespace: String) extends PartiallyResolvedFQN
case class ResolvedModule(moduleRef: ModuleReference)
extends PartiallyResolvedFQN
}

View File

@ -7,7 +7,8 @@ import org.enso.compiler.data.BindingsMap
import org.enso.compiler.data.BindingsMap.{
Resolution,
ResolutionNotFound,
ResolvedMethod
ResolvedMethod,
ResolvedModule
}
import org.enso.compiler.exception.CompilerError
import org.enso.compiler.pass.IRPass
@ -36,7 +37,7 @@ case object GlobalNames extends IRPass {
/** The passes that this pass depends _directly_ on to run. */
override val precursorPasses: Seq[IRPass] =
Seq(AliasAnalysis, BindingAnalysis)
Seq(AliasAnalysis, BindingAnalysis, FullyQualifiedNames)
/** The passes that are invalidated by running this pass. */
override val invalidatedPasses: Seq[IRPass] = Seq(AliasAnalysis)
@ -150,6 +151,14 @@ case object GlobalNames extends IRPass {
)
)
case lit: IR.Name.Literal =>
lit.getMetadata(FullyQualifiedNames) match {
case Some(
FullyQualifiedNames.FQNResolution(
FullyQualifiedNames.ResolvedModule(modRef)
)
) =>
lit.updateMetadata(this -->> Resolution(ResolvedModule(modRef)))
case _ =>
if (!lit.isMethod && !isLocalVar(lit)) {
val resolution = bindings.resolveName(lit.name)
resolution match {
@ -198,6 +207,7 @@ case object GlobalNames extends IRPass {
} else {
lit
}
}
case app: IR.Application.Prefix =>
app.function match {
case lit: IR.Name.Literal =>

View File

@ -8,8 +8,6 @@ import org.enso.interpreter.runtime.Module
import org.enso.interpreter.runtime.Module.CompilationStage
import org.enso.syntax.text.Parser
import scala.annotation.unused
/** A phase responsible for initializing the builtins' IR from the provided
* source.
*/
@ -30,9 +28,9 @@ object BuiltinsIrBuilder {
* @param passes the compiler's pass manager
*/
def build(
@unused module: Module,
@unused freshNameSupply: FreshNameSupply,
@unused passes: Passes
module: Module,
freshNameSupply: FreshNameSupply,
passes: Passes
): Unit = {
val passManager = passes.passManager
val moduleContext = ModuleContext(

View File

@ -0,0 +1,6 @@
name: Test_Fully_Qualified_Name_Failure
license: APLv2
enso-version: default
version: "0.0.1"
author: "Enso Team <contact@enso.org>"
maintainer: "Enso Team <contact@enso.org>"

View File

@ -0,0 +1,3 @@
main =
Standard.Base.IO.println "Hello world!"
0

View File

@ -0,0 +1,6 @@
name: Test_Fully_Qualified_Name_Success
license: APLv2
enso-version: default
version: "0.0.1"
author: "Enso Team <contact@enso.org>"
maintainer: "Enso Team <contact@enso.org>"

View File

@ -0,0 +1,5 @@
import Standard.Base
main =
Standard.Base.IO.println "Hello world!"
0

View File

@ -158,4 +158,26 @@ class ImportsTest extends PackageTest {
"Constructors" should "be exportable" in {
evalTestProject("Test_Type_Exports").toString shouldEqual "(Some 10)"
}
"Fully qualified names" should "not be resolved when lacking imports" in {
the[InterpreterException] thrownBy evalTestProject(
"Test_Fully_Qualified_Name_Failure"
) should have message "Compilation aborted due to errors."
val outLines = consumeOut
outLines should have length 3
outLines(
2
) shouldEqual "Main.enso[2:14-2:17]: Fully qualified name references a library Standard.Base but an import statement for it is missing."
}
"Fully qualified names" should "be resolved when library has already been loaded" in {
evalTestProject(
"Test_Fully_Qualified_Name_Success"
).toString shouldEqual "0"
val outLines = consumeOut
outLines should have length 1
outLines(0) shouldEqual "Hello world!"
}
}

View File

@ -50,3 +50,7 @@ spec =
Test.specify "should be allowed to be called statically" pending="Needs changes to method dispatch logic" <|
b = Bar.Value 1
Bar.meh b 2 . should_equal 3
Test.group "Fully Qualified Names" <|
Test.specify "should be correctly resolved" <|
a = Standard.Base.Data.Array.Array.new 10
a.length . should_equal 10