mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 18:01:38 +03:00
Implement private modules (#7840)
Adds the ability to declare a module as *private*. Modifies the parser to add the `private` keyword as a reserved keyword. All the checks for private modules are implemented as an independent *Compiler pass*. No checks are done at runtime. # Important Notes - Introduces new keyword - `private` - a reserved keyword. - Modules that have `private` keyword as the first statement are declared as *private* (Project private) - Public module cannot have private submodules and vice versa. - This would require runtime access checks - See #7088 for the specification.
This commit is contained in:
parent
421e3f22a4
commit
c22928ecc2
@ -972,6 +972,7 @@
|
||||
- [Downloadable VSCode extension][7861]
|
||||
- [New `project/status` route for reporting LS state][7801]
|
||||
- [Add Enso-specific assertions][7883])
|
||||
- [Modules can be `private`][7840]
|
||||
|
||||
[3227]: https://github.com/enso-org/enso/pull/3227
|
||||
[3248]: https://github.com/enso-org/enso/pull/3248
|
||||
@ -1117,6 +1118,7 @@
|
||||
[7825]: https://github.com/enso-org/enso/pull/7825
|
||||
[7861]: https://github.com/enso-org/enso/pull/7861
|
||||
[7883]: https://github.com/enso-org/enso/pull/7883
|
||||
[7840]: https://github.com/enso-org/enso/pull/7840
|
||||
|
||||
# Enso 2.0.0-alpha.18 (2021-10-12)
|
||||
|
||||
|
@ -29,3 +29,5 @@ This specification is broken down into the following sections:
|
||||
those of suspended computations.
|
||||
- [**Modules:**](./modules.md) The semantics of Enso's modules.
|
||||
- [**Scoping:**](./scoping.md) Enso's scoping and identifier resolution rules.
|
||||
- [**Encapsulation:**](./encapsulation.md) The semantics of Enso's encapsulation
|
||||
system - access restriction.
|
||||
|
178
docs/semantics/encapsulation.md
Normal file
178
docs/semantics/encapsulation.md
Normal file
@ -0,0 +1,178 @@
|
||||
---
|
||||
layout: developer-doc
|
||||
title: Diagnostics
|
||||
category: semantics
|
||||
tags: [semantics, diagnostics, runtime]
|
||||
order: 11
|
||||
---
|
||||
|
||||
# Encapsulation
|
||||
|
||||
_Encapsulation_ is the system of hiding certain internal **entities** (modules,
|
||||
types, methods, constructors, fields) in one project/library from other
|
||||
projects/libraries. This document is an excerpt from the discussion held at
|
||||
https://github.com/orgs/enso-org/discussions/7088.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Be able to hide an entity on demand. By hiding, we mean that the entity cannot
|
||||
be directly imported, and that it cannot be used via FQN.
|
||||
- i.e. an entity shall be hidden both during compile time (project
|
||||
compilation), and during runtime.
|
||||
- Entity being hidden at runtime implies that it does not have any entry in
|
||||
the Suggestion database, therefore, no entry in the Component browser.
|
||||
- Be able to import all public symbols from a library with
|
||||
`from Library import all`.
|
||||
- Be able to import a selected set of symbols from a library with
|
||||
`from Library import Symbol_1, Symbol_2, ...`.
|
||||
- Import a public symbol directly with
|
||||
`import Library.Public_Module.Public_Type`.
|
||||
- Use a public symbol via FQN: `Library.Public_Module.Public_Type`.
|
||||
|
||||
## Implementation
|
||||
|
||||
Let's introduce a `private` keyword. By prepending (syntax rules discussed
|
||||
below) `private` keyword` to an entity, we declare it as **project private**. A
|
||||
project-private entity is an entity that can be imported and used in the same
|
||||
project, but cannot be imported nor used in different projects. Note that it is
|
||||
not desirable to declare the entities as _module private_, as that would be too
|
||||
restrictive, and would prevent library authors using the entity within the
|
||||
project.
|
||||
|
||||
From now on, let's consider _project-private_ and _private_ synonymous, and
|
||||
**public** as an entity that is not private.
|
||||
|
||||
## Syntax
|
||||
|
||||
All the entities, except modules, shall be declared private by prepending them
|
||||
with `private` keyword. Declaring a module as private shall be done be writing
|
||||
the `private` keyword at the very beginning of the module, before all the import
|
||||
statements, ignoring all the comments before. Fields cannot have `private`
|
||||
keyword, only constructors. Types cannot have `private` keyword as well - only
|
||||
methods and constructors.
|
||||
|
||||
## Semantics
|
||||
|
||||
### Modules
|
||||
|
||||
Modules can be specified as private. Private modules cannot be imported from
|
||||
other projects. Private modules can be imported from the same project.
|
||||
|
||||
A hierarchy of submodules cannot mix public and private modules. In other words,
|
||||
if a module is public, its whole subtree must be public as well. For example,
|
||||
having a public module `A` and private submodule `A.B` is forbidden and shall be
|
||||
reported as an error during compilation. But having a private module `A` as well
|
||||
as private module `A.B` is OK.
|
||||
|
||||
### Types
|
||||
|
||||
_Types cannot be specified as private_, only constructors and methods. A type
|
||||
must have all the constructors private or all the constructors public. This is
|
||||
to prevent a situation when a pattern match can be done on public constructor,
|
||||
but cannot be done on a private constructor from a different project. Mixing
|
||||
public and private constructors in a single type is a compilation error. A type
|
||||
with all constructors public is called an _open_ type and a type with all
|
||||
constructors private is called a _closed_ type.
|
||||
|
||||
Methods on types (or on modules) can be specified private. To check whether a
|
||||
private method is accessed only from within the same project, a runtime check
|
||||
must be performed, as this cannot be checked during the compilation.
|
||||
|
||||
## Example
|
||||
|
||||
Lib/src/Pub_Type.enso:
|
||||
|
||||
```
|
||||
type Pub_Type
|
||||
Constructor field
|
||||
private priv_method self = ...
|
||||
pub_method self = self.field.to_text
|
||||
|
||||
private type Priv_Type
|
||||
```
|
||||
|
||||
Lib/src/Methods.enso:
|
||||
|
||||
```
|
||||
pub_stat_method x y = x + y
|
||||
private priv_stat_method x y = x - y
|
||||
```
|
||||
|
||||
Lib/src/Internal/Helpers.enso:
|
||||
|
||||
```
|
||||
# Mark the whole module as private
|
||||
private
|
||||
|
||||
# OK to import private types in the same project
|
||||
import project.Pub_Type.Priv_Type
|
||||
```
|
||||
|
||||
Lib/src/Main.enso:
|
||||
|
||||
```
|
||||
import project.Pub_Type.Pub_Type
|
||||
export project.Pub_Type.Pub_Type
|
||||
|
||||
import project.Pub_Type.Priv_Type # OK - we can import private types in the same project.
|
||||
export project.Pub_Type.Priv_Type # Failes at compile time - re-exporting private types is forbidden.
|
||||
```
|
||||
|
||||
tmp.enso:
|
||||
|
||||
```
|
||||
from Lib import Pub_Type
|
||||
import Lib.Pub_Type.Priv_Type # Fails during compilation
|
||||
import Lib.Methods
|
||||
|
||||
main =
|
||||
# This constructor is not private, we can use it here.
|
||||
obj = Pub_Type.Constructor field=42
|
||||
obj.field # OK - Constructor is public, therefore, field is public
|
||||
obj.priv_method # Runtime failure - priv_method is private
|
||||
Pub_Type.priv_method self=obj # Runtime failure
|
||||
obj.pub_method # OK
|
||||
|
||||
Lib.Pub_Type.Priv_Type # Fails at runtime - accessing private types via FQN is forbidden
|
||||
|
||||
Methods.pub_stat_method 1 2 # OK
|
||||
Methods.priv_stat_method # Fails at runtime
|
||||
```
|
||||
|
||||
## Checks
|
||||
|
||||
There shall be two checks. One check during **compilation**, that can be
|
||||
implemented as a separate compiler pass, and that will ensure that no private
|
||||
entity is _re-exported_ (exported from a module that is different from the
|
||||
module inside which the entity is defined) and that for every type it holds that
|
||||
either all the constructors are public or all the constructors are private
|
||||
|
||||
The second check shall be done during the **method/name resolution** step. This
|
||||
step happens at runtime, before a method is called. After the method is
|
||||
resolved, there shall be no further checks, so that the peak performance is not
|
||||
affected.
|
||||
|
||||
## Performance impact
|
||||
|
||||
The performance hit on compilation time is minimal, as there are already dozens
|
||||
of different compiler passes. Moreover, in the new compiler pass we shall check
|
||||
only imports and exports statements, no other IR.
|
||||
|
||||
The performance hit on runtime, during method resolution, is minimal as well,
|
||||
because it can be as easy as additional lookup in a hash map. Peak performance
|
||||
will not be affected at all, as there are no further checks after method
|
||||
resolution.
|
||||
|
||||
## Overcoming encapsulation
|
||||
|
||||
Sometimes it is useful to be able to access internal entities. Testing is the
|
||||
most obvious example. Let's introduce a new CLI flag to the Engine launcher
|
||||
called `--disable-private-check`, which will disable all the private checks
|
||||
during compilation and method resolution.
|
||||
|
||||
## Other notes
|
||||
|
||||
- A private module implies that all the entities defined within are private
|
||||
- A private type implies that all the constructors, methods and fields defined
|
||||
within are private
|
||||
- A private constructor implies private fields defined in that constructor.
|
@ -4,6 +4,7 @@ import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.enso.compiler.core.ir.Diagnostic;
|
||||
import org.enso.compiler.core.ir.IdentifiedLocation;
|
||||
import org.enso.compiler.core.ir.CallArgument;
|
||||
import org.enso.compiler.core.ir.DefinitionArgument;
|
||||
@ -40,6 +41,7 @@ import org.enso.syntax2.Tree;
|
||||
|
||||
import org.enso.syntax2.Tree.Invalid;
|
||||
|
||||
import org.enso.syntax2.Tree.Private;
|
||||
import scala.Option;
|
||||
import scala.collection.immutable.LinearSeq;
|
||||
import scala.collection.immutable.List;
|
||||
@ -152,9 +154,11 @@ final class TreeToIr {
|
||||
Module translateModule(Tree module) {
|
||||
return switch (module) {
|
||||
case Tree.BodyBlock b -> {
|
||||
boolean isPrivate = false;
|
||||
List<Definition> bindings = nil();
|
||||
List<Import> imports = nil();
|
||||
List<Export> exports = nil();
|
||||
List<Diagnostic> diag = nil();
|
||||
for (Line line : b.getStatements()) {
|
||||
var expr = line.getExpression();
|
||||
// Documentation found among imports/exports or at the top of the module (if it starts with imports) is
|
||||
@ -169,6 +173,18 @@ final class TreeToIr {
|
||||
bindings = cons(c, bindings);
|
||||
expr = doc.getExpression();
|
||||
}
|
||||
if (expr instanceof Private priv) {
|
||||
if (priv.getBody() != null) {
|
||||
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("Private token with body"));
|
||||
diag = cons(error, diag);
|
||||
}
|
||||
if (isPrivate) {
|
||||
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("Private token specified more than once"));
|
||||
diag = cons(error, diag);
|
||||
}
|
||||
isPrivate = true;
|
||||
continue;
|
||||
}
|
||||
switch (expr) {
|
||||
case Tree.Import imp -> imports = cons(translateImport(imp), imports);
|
||||
case Tree.Export exp -> exports = cons(translateExport(exp), exports);
|
||||
@ -176,11 +192,12 @@ final class TreeToIr {
|
||||
default -> bindings = translateModuleSymbol(expr, bindings);
|
||||
}
|
||||
}
|
||||
yield new Module(imports.reverse(), exports.reverse(), bindings.reverse(), getIdentifiedLocation(module), meta(), diag());
|
||||
yield new Module(imports.reverse(), exports.reverse(), bindings.reverse(), isPrivate, getIdentifiedLocation(module), meta(), DiagnosticStorage.apply(diag));
|
||||
}
|
||||
default -> new Module(
|
||||
nil(), nil(),
|
||||
cons(translateSyntaxError(module, new Syntax.UnsupportedSyntax("translateModule")), nil()),
|
||||
false,
|
||||
getIdentifiedLocation(module), meta(), diag()
|
||||
);
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import org.enso.compiler.core.ir.module.scope.{Definition, Export, Import}
|
||||
* @param imports the import statements that bring other modules into scope
|
||||
* @param exports the export statements for this module
|
||||
* @param bindings the top-level bindings for this module
|
||||
* @param isPrivate whether or not this module is private (project-private)
|
||||
* @param location the source location that the node corresponds to
|
||||
* @param passData the pass metadata associated with this node
|
||||
* @param diagnostics compiler diagnostics for this node
|
||||
@ -23,6 +24,7 @@ sealed case class Module(
|
||||
imports: List[Import],
|
||||
exports: List[Export],
|
||||
bindings: List[Definition],
|
||||
isPrivate: Boolean,
|
||||
override val location: Option[IdentifiedLocation],
|
||||
override val passData: MetadataStorage = MetadataStorage(),
|
||||
override val diagnostics: DiagnosticStorage = DiagnosticStorage()
|
||||
@ -51,7 +53,15 @@ sealed case class Module(
|
||||
id: Identifier = id
|
||||
): Module = {
|
||||
val res =
|
||||
Module(imports, exports, bindings, location, passData, diagnostics)
|
||||
Module(
|
||||
imports,
|
||||
exports,
|
||||
bindings,
|
||||
isPrivate,
|
||||
location,
|
||||
passData,
|
||||
diagnostics
|
||||
)
|
||||
res.id = id
|
||||
res
|
||||
}
|
||||
|
@ -171,6 +171,38 @@ object ImportExport {
|
||||
s"No such constructor ${constructorName} in type $typeName"
|
||||
}
|
||||
|
||||
case class ExportSymbolsFromPrivateModule(
|
||||
moduleName: String
|
||||
) extends Reason {
|
||||
override def message: String =
|
||||
s"Cannot export any symbol from module '$moduleName': The module is private"
|
||||
}
|
||||
|
||||
case class ExportPrivateModule(
|
||||
moduleName: String
|
||||
) extends Reason {
|
||||
override def message: String =
|
||||
s"Cannot export private module '$moduleName'"
|
||||
}
|
||||
|
||||
case class ImportPrivateModule(
|
||||
moduleName: String
|
||||
) extends Reason {
|
||||
override def message: String =
|
||||
s"Cannot import private module '$moduleName'"
|
||||
}
|
||||
|
||||
case class SubmoduleVisibilityMismatch(
|
||||
moduleName: String,
|
||||
submoduleName: String,
|
||||
moduleVisibility: String,
|
||||
submoduleVisibility: String
|
||||
) extends Reason {
|
||||
override def message: String =
|
||||
s"Cannot export submodule '$submoduleName' of module '$moduleName': " +
|
||||
s"the submodule is $submoduleVisibility, but the module is $moduleVisibility"
|
||||
}
|
||||
|
||||
/** Represents an ambiguous import resolution error, where the same symbol is imported more than once refereing
|
||||
* to different objects. The objects are represented by their physical path in the project.
|
||||
*
|
||||
|
@ -5,11 +5,14 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import org.enso.compiler.core.ir.Module;
|
||||
import org.junit.AfterClass;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
@ -1239,6 +1242,29 @@ public class EnsoParserTest {
|
||||
equivalenceTest("a = x", "a = SKIP FREEZE x.f y");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrivateModules() throws Exception {
|
||||
List<String> moduleCodes = List.of(
|
||||
"private",
|
||||
"""
|
||||
# Comment
|
||||
private
|
||||
""",
|
||||
"""
|
||||
# Comment with empty line
|
||||
|
||||
private
|
||||
"""
|
||||
);
|
||||
for (var moduleCode : moduleCodes) {
|
||||
parseTest(moduleCode);
|
||||
var module = compile("private");
|
||||
assertTrue(module.isPrivate());
|
||||
}
|
||||
equivalenceTest("private", "# Line comment \nprivate");
|
||||
equivalenceTest("private", "\n\nprivate");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ise_184219679() throws IOException {
|
||||
parseTest("""
|
||||
|
@ -0,0 +1,204 @@
|
||||
package org.enso.compiler.pass.analyse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.enso.compiler.context.InlineContext;
|
||||
import org.enso.compiler.context.ModuleContext;
|
||||
import org.enso.compiler.core.IR;
|
||||
import org.enso.compiler.core.ir.Expression;
|
||||
import org.enso.compiler.core.ir.Module;
|
||||
import org.enso.compiler.core.ir.expression.errors.ImportExport;
|
||||
import org.enso.compiler.core.ir.module.scope.Export;
|
||||
import org.enso.compiler.core.ir.module.scope.Import;
|
||||
import org.enso.compiler.data.BindingsMap;
|
||||
import org.enso.compiler.pass.IRPass;
|
||||
import org.enso.interpreter.util.ScalaConversions;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
import scala.Option;
|
||||
import scala.collection.immutable.Seq;
|
||||
import scala.jdk.javaapi.CollectionConverters;
|
||||
|
||||
/**
|
||||
* Iterates through all the imports and exports of non-synthetic modules and ensures that:
|
||||
* <ul>
|
||||
* <li>No private module is exported</li>
|
||||
* <li>No private module from a different project is imported</li>
|
||||
* <li>Hierarchy of modules and submodules does not mix private and public modules</li>
|
||||
* </ul>
|
||||
* Inserts errors into imports/exports IRs if the above conditions are violated.
|
||||
*/
|
||||
public final class PrivateModuleAnalysis implements IRPass {
|
||||
private static final PrivateModuleAnalysis singleton = new PrivateModuleAnalysis();
|
||||
private UUID uuid;
|
||||
|
||||
private PrivateModuleAnalysis() {}
|
||||
|
||||
public static PrivateModuleAnalysis getInstance() {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void org$enso$compiler$pass$IRPass$_setter_$key_$eq(UUID v) {
|
||||
this.uuid = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID key() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Seq<IRPass> precursorPasses() {
|
||||
List<IRPass> passes = List.of(
|
||||
BindingAnalysis$.MODULE$,
|
||||
ImportSymbolAnalysis$.MODULE$
|
||||
);
|
||||
return CollectionConverters.asScala(passes).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Seq<IRPass> invalidatedPasses() {
|
||||
return ScalaConversions.nil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Module runModule(Module moduleIr, ModuleContext moduleContext) {
|
||||
var bindingsMap = (BindingsMap) moduleIr.passData().get(BindingAnalysis$.MODULE$).get();
|
||||
var currentPackage = moduleContext.getPackage();
|
||||
List<Import> importErrors = new ArrayList<>();
|
||||
List<Export> exportErrors = new ArrayList<>();
|
||||
var isCurrentModulePrivate = moduleIr.isPrivate();
|
||||
|
||||
// Ensure that imported modules from a different project are not private.
|
||||
bindingsMap.resolvedImports().foreach(resolvedImp -> {
|
||||
var importedModule = resolvedImp.target().module().unsafeAsModule("should succeed");
|
||||
var importedModuleName = importedModule.getName().toString();
|
||||
var importedModulePackage = importedModule.getPackage();
|
||||
if (currentPackage != null
|
||||
&& !currentPackage.equals(importedModulePackage)
|
||||
&& importedModule.isPrivate()) {
|
||||
importErrors.add(ImportExport.apply(
|
||||
resolvedImp.importDef(),
|
||||
new ImportExport.ImportPrivateModule(importedModuleName),
|
||||
ImportExport.apply$default$3(),
|
||||
ImportExport.apply$default$4()
|
||||
));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Ensure that no symbols are exported from a private module.
|
||||
if (isCurrentModulePrivate && containsExport(moduleIr)) {
|
||||
exportErrors.add(ImportExport.apply(
|
||||
moduleIr.exports().apply(0),
|
||||
new ImportExport.ExportSymbolsFromPrivateModule(moduleContext.getName().toString()),
|
||||
ImportExport.apply$default$3(),
|
||||
ImportExport.apply$default$4()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Ensure that private modules are not exported and that the hierarchy of submodules
|
||||
// does not mix public and private modules.
|
||||
bindingsMap
|
||||
.getDirectlyExportedModules()
|
||||
.foreach(expModule -> {
|
||||
var expModuleRef = expModule.target().module().unsafeAsModule("should succeed");
|
||||
if (expModuleRef.isPrivate()) {
|
||||
var associatedExportIR = findExportIRByName(moduleIr, expModuleRef.getName());
|
||||
assert associatedExportIR.isDefined();
|
||||
if (isSubmoduleName(moduleContext.getName(), expModuleRef.getName())) {
|
||||
var haveSameVisibility = isCurrentModulePrivate == expModuleRef.isPrivate();
|
||||
if (!haveSameVisibility) {
|
||||
exportErrors.add(
|
||||
ImportExport.apply(
|
||||
associatedExportIR.get(),
|
||||
new ImportExport.SubmoduleVisibilityMismatch(
|
||||
moduleContext.getName().toString(),
|
||||
expModuleRef.getName().toString(),
|
||||
isCurrentModulePrivate ? "private" : "public",
|
||||
expModuleRef.isPrivate() ? "private" : "public"
|
||||
),
|
||||
ImportExport.apply$default$3(),
|
||||
ImportExport.apply$default$4()
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
exportErrors.add(
|
||||
ImportExport.apply(
|
||||
associatedExportIR.get(),
|
||||
new ImportExport.ExportPrivateModule(expModuleRef.getName().toString()),
|
||||
ImportExport.apply$default$3(),
|
||||
ImportExport.apply$default$4()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
scala.collection.immutable.List<Import> convertedImports =
|
||||
importErrors.isEmpty() ? moduleIr.imports() : CollectionConverters.asScala(importErrors).toList();
|
||||
scala.collection.immutable.List<Export> convertedExports =
|
||||
exportErrors.isEmpty() ? moduleIr.exports() : CollectionConverters.asScala(exportErrors).toList();
|
||||
|
||||
return moduleIr.copy(
|
||||
convertedImports,
|
||||
convertedExports,
|
||||
moduleIr.bindings(),
|
||||
moduleIr.location(),
|
||||
moduleIr.passData(),
|
||||
moduleIr.diagnostics(),
|
||||
moduleIr.id()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isSubmoduleName(QualifiedName parentModName, QualifiedName subModName) {
|
||||
if (subModName.getParent().isDefined()) {
|
||||
return parentModName.item().equals(
|
||||
subModName.getParent().get().item()
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression runExpression(Expression ir, InlineContext inlineContext) {
|
||||
return ir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the given Module's IR contains an export that is not synthetic.
|
||||
*/
|
||||
private static boolean containsExport(Module moduleIr) {
|
||||
return !moduleIr.exports().isEmpty() && moduleIr.exports().exists(exp -> {
|
||||
if (exp instanceof Export.Module moduleExport) {
|
||||
return !moduleExport.isSynthetic();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Option<Export> findExportIRByName(Module moduleIr, QualifiedName fqn) {
|
||||
return moduleIr.exports().find(exp -> {
|
||||
if (exp instanceof Export.Module expMod) {
|
||||
if (expMod.name().parts().last().name().equals(fqn.item())) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("unknown exp: " + exp);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IR> T updateMetadataInDuplicate(T sourceIr, T copyOfIr) {
|
||||
return IRPass.super.updateMetadataInDuplicate(sourceIr, copyOfIr);
|
||||
}
|
||||
}
|
@ -193,6 +193,11 @@ public final class Module implements EnsoObject {
|
||||
return synthetic;
|
||||
}
|
||||
|
||||
/** @return true iff this module is private (project-private). */
|
||||
public boolean isPrivate() {
|
||||
return ir.isPrivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets new literal sources for the module.
|
||||
*
|
||||
|
@ -28,4 +28,14 @@ public class ScalaConversions {
|
||||
public static <T> List<T> asJava(Seq<T> list) {
|
||||
return CollectionConverters.asJava(list);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> scala.collection.immutable.List<T> nil() {
|
||||
return (scala.collection.immutable.List<T>) scala.collection.immutable.Nil$.MODULE$;
|
||||
}
|
||||
|
||||
public static <T> scala.collection.immutable.List<T> cons(
|
||||
T head, scala.collection.immutable.List<T> tail) {
|
||||
return scala.collection.immutable.$colon$colon$.MODULE$.apply(head, tail);
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ class Passes(
|
||||
LambdaShorthandToLambda,
|
||||
ImportSymbolAnalysis,
|
||||
AmbiguousImportsAnalysis,
|
||||
PrivateModuleAnalysis.getInstance(),
|
||||
ShadowedPatternFields,
|
||||
UnreachableMatchBranches,
|
||||
NestedPatternMatch,
|
||||
|
@ -0,0 +1,7 @@
|
||||
name: Test_Private_Modules_1
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
prefer-local-libraries: true
|
@ -0,0 +1,9 @@
|
||||
# Export some "public" stuff from this library
|
||||
from project.Pub_Mod import Pub_Mod_Type
|
||||
from project.Pub_Mod export Pub_Mod_Type
|
||||
|
||||
# Can import and use private modules in the same project
|
||||
from project.Priv_Mod import Type_In_Priv_Mod
|
||||
|
||||
main =
|
||||
Type_In_Priv_Mod.Value 42 . data
|
@ -0,0 +1,4 @@
|
||||
private
|
||||
|
||||
type Type_In_Priv_Mod
|
||||
Value data
|
@ -0,0 +1,4 @@
|
||||
|
||||
type Pub_Mod_Type
|
||||
Value data
|
||||
|
@ -0,0 +1,7 @@
|
||||
name: Test_Private_Modules_2
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
prefer-local-libraries: true
|
@ -0,0 +1,5 @@
|
||||
# Will import just exported (and public) symbols from Test_Private_Modules_1
|
||||
from local.Test_Private_Modules_1 import all
|
||||
|
||||
main =
|
||||
Pub_Mod_Type.Value 42
|
@ -0,0 +1,7 @@
|
||||
name: Test_Private_Modules_3
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
prefer-local-libraries: true
|
@ -0,0 +1,5 @@
|
||||
# This import should fail - importing private module
|
||||
import local.Test_Private_Modules_1.Priv_Mod
|
||||
|
||||
main =
|
||||
"Success"
|
@ -0,0 +1,7 @@
|
||||
name: Test_Private_Modules_4
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
prefer-local-libraries: true
|
@ -0,0 +1,7 @@
|
||||
import project.Sub
|
||||
export project.Sub
|
||||
|
||||
# Fails at compile time - cannot mix private and public modules in a module subtree.
|
||||
|
||||
main =
|
||||
42
|
@ -0,0 +1,4 @@
|
||||
private
|
||||
|
||||
foo = 23
|
||||
|
@ -0,0 +1,3 @@
|
||||
|
||||
foo = 42
|
||||
|
@ -0,0 +1,7 @@
|
||||
name: Test_Private_Modules_5
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
prefer-local-libraries: true
|
@ -0,0 +1,7 @@
|
||||
# This project has a private module tree
|
||||
|
||||
import project.Sub.Priv
|
||||
|
||||
main =
|
||||
Priv.foo
|
||||
|
@ -0,0 +1,3 @@
|
||||
# This is a private non-synthetic module with submodules
|
||||
private
|
||||
|
@ -0,0 +1,3 @@
|
||||
private
|
||||
|
||||
foo = 42
|
@ -258,7 +258,7 @@ trait CompilerRunner {
|
||||
runtime.Module.empty(QualifiedName.simpleName("Test_Module"), null)
|
||||
ModuleTestUtils.unsafeSetIr(
|
||||
mod,
|
||||
Module(List(), List(), List(), None)
|
||||
Module(List(), List(), List(), false, None)
|
||||
.updateMetadata(
|
||||
BindingAnalysis -->> BindingsMap(
|
||||
List(),
|
||||
|
@ -9,7 +9,8 @@ import org.enso.compiler.pass.analyse.{
|
||||
AliasAnalysis,
|
||||
AmbiguousImportsAnalysis,
|
||||
BindingAnalysis,
|
||||
ImportSymbolAnalysis
|
||||
ImportSymbolAnalysis,
|
||||
PrivateModuleAnalysis
|
||||
}
|
||||
import org.enso.compiler.pass.desugar._
|
||||
import org.enso.compiler.pass.lint.{ModuleNameConflicts, ShadowedPatternFields}
|
||||
@ -60,6 +61,7 @@ class PassesTest extends CompilerTest {
|
||||
LambdaShorthandToLambda,
|
||||
ImportSymbolAnalysis,
|
||||
AmbiguousImportsAnalysis,
|
||||
PrivateModuleAnalysis.getInstance(),
|
||||
ShadowedPatternFields,
|
||||
UnreachableMatchBranches,
|
||||
NestedPatternMatch,
|
||||
|
@ -98,6 +98,7 @@ class GatherDiagnosticsTest extends CompilerTest {
|
||||
definition.Method
|
||||
.Explicit(method2Ref, error3, None)
|
||||
),
|
||||
false,
|
||||
None
|
||||
)
|
||||
|
||||
|
@ -905,4 +905,97 @@ class ImportExportTest
|
||||
mainIr.imports.head.isInstanceOf[errors.ImportExport] shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"Private modules" should {
|
||||
"not be able to export private module" in {
|
||||
"""
|
||||
|private
|
||||
|""".stripMargin
|
||||
.createModule(
|
||||
packageQualifiedName.createChild("Priv_Module")
|
||||
)
|
||||
|
||||
val mainIr = s"""
|
||||
|import $namespace.$packageName.Priv_Module
|
||||
|export $namespace.$packageName.Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(packageQualifiedName.createChild("Main"))
|
||||
.getIr
|
||||
|
||||
mainIr.exports.size shouldEqual 1
|
||||
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||
mainIr.exports.head
|
||||
.asInstanceOf[errors.ImportExport]
|
||||
.reason
|
||||
.isInstanceOf[errors.ImportExport.ExportPrivateModule] shouldBe true
|
||||
}
|
||||
|
||||
"not be able to export anything from private module itself" in {
|
||||
val mainIr =
|
||||
s"""
|
||||
|private
|
||||
|
|
||||
|from $namespace.$packageName export Type_In_Priv_Module
|
||||
|
|
||||
|type Type_In_Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(packageQualifiedName.createChild("Main"))
|
||||
.getIr
|
||||
|
||||
mainIr.exports.size shouldEqual 1
|
||||
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||
mainIr.exports.head
|
||||
.asInstanceOf[errors.ImportExport]
|
||||
.reason
|
||||
.isInstanceOf[
|
||||
errors.ImportExport.ExportSymbolsFromPrivateModule
|
||||
] shouldBe true
|
||||
|
||||
}
|
||||
|
||||
"not be able to export anything from private module from Main module" in {
|
||||
"""
|
||||
|private
|
||||
|type Type_In_Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(
|
||||
packageQualifiedName.createChild("Priv_Module")
|
||||
)
|
||||
|
||||
val mainIr =
|
||||
s"""
|
||||
|from $namespace.$packageName.Priv_Module import Type_In_Priv_Module
|
||||
|from $namespace.$packageName.Priv_Module export Type_In_Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(packageQualifiedName.createChild("Main"))
|
||||
.getIr
|
||||
|
||||
mainIr.exports.size shouldEqual 1
|
||||
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||
mainIr.exports.head
|
||||
.asInstanceOf[errors.ImportExport]
|
||||
.reason
|
||||
.isInstanceOf[errors.ImportExport.ExportPrivateModule] shouldBe true
|
||||
}
|
||||
|
||||
"be able to import stuff from private modules in the same library" in {
|
||||
"""
|
||||
|private
|
||||
|type Type_In_Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(
|
||||
packageQualifiedName.createChild("Priv_Module")
|
||||
)
|
||||
|
||||
val mainIr =
|
||||
s"""
|
||||
|from $namespace.$packageName.Priv_Module import Type_In_Priv_Module
|
||||
|""".stripMargin
|
||||
.createModule(packageQualifiedName.createChild("Main"))
|
||||
.getIr
|
||||
|
||||
val errors = mainIr.preorder.filter(x => x.isInstanceOf[Error])
|
||||
errors.size shouldEqual 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -219,4 +219,41 @@ class ImportsTest extends PackageTest {
|
||||
outLines(2) shouldEqual "(D_Mod.Value 1)"
|
||||
}
|
||||
|
||||
"Private modules" should "be able to import and use private modules within the same project" in {
|
||||
evalTestProject(
|
||||
"Test_Private_Modules_1"
|
||||
).toString shouldEqual "42"
|
||||
}
|
||||
|
||||
"Private modules" should "be able to import non-private stuff" in {
|
||||
evalTestProject(
|
||||
"Test_Private_Modules_2"
|
||||
).toString shouldEqual "(Pub_Mod_Type.Value 42)"
|
||||
}
|
||||
|
||||
"Private modules" should "not be able to import private modules from different project" in {
|
||||
the[InterpreterException] thrownBy evalTestProject(
|
||||
"Test_Private_Modules_3"
|
||||
) should have message "Compilation aborted due to errors."
|
||||
val outLines = consumeOut.filterNot(isDiagnosticLine)
|
||||
outLines should have length 1
|
||||
outLines.head should include(
|
||||
"Main.enso:2:1: error: Cannot import private module"
|
||||
)
|
||||
}
|
||||
|
||||
"Private modules" should "not be able to mix private and public submodules" in {
|
||||
val e = the[InterpreterException] thrownBy evalTestProject(
|
||||
"Test_Private_Modules_4"
|
||||
)
|
||||
e.getMessage() should include(
|
||||
"Cannot export submodule 'local.Test_Private_Modules_4.Sub.Priv_SubMod' of module 'local.Test_Private_Modules_4.Sub'"
|
||||
)
|
||||
}
|
||||
|
||||
"Private module" should "be able to have only private submodules" in {
|
||||
evalTestProject(
|
||||
"Test_Private_Modules_5"
|
||||
) shouldEqual 42
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ where T: serde::Serialize + Reflect {
|
||||
TextEnd::reflect(),
|
||||
TextStart::reflect(),
|
||||
Wildcard::reflect(),
|
||||
Private::reflect(),
|
||||
];
|
||||
skip_tokens.into_iter().for_each(|token| to_s_expr.skip(rust_to_meta[&token.id]));
|
||||
let ident_token = rust_to_meta[&Ident::reflect().id];
|
||||
|
@ -1189,6 +1189,35 @@ fn pattern_match_auto_scope() {
|
||||
test(&code.join("\n"), expected);
|
||||
}
|
||||
|
||||
// === Private (project-private) keyword ===
|
||||
#[test]
|
||||
fn private_keyword() {
|
||||
test("private", block![(Private())]);
|
||||
test("private func", block![(Private (Ident func))]);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn private_is_first_statement() {
|
||||
// Comments and empty lines are allowed before `private`.
|
||||
#[rustfmt::skip]
|
||||
let lines = vec![
|
||||
"# Some comment",
|
||||
"# Other comment",
|
||||
"",
|
||||
"private"
|
||||
];
|
||||
test(&lines.join("\n"), block![()()()(Private)]);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let lines = vec![
|
||||
"type T",
|
||||
"",
|
||||
"private"
|
||||
];
|
||||
expect_invalid_node(&lines.join("\n"));
|
||||
}
|
||||
|
||||
// === Array/tuple literals ===
|
||||
|
||||
|
@ -38,6 +38,7 @@ fn statement() -> resolver::SegmentMap<'static> {
|
||||
register_import_macros(&mut macro_map);
|
||||
register_export_macros(&mut macro_map);
|
||||
macro_map.register(type_def());
|
||||
macro_map.register(private());
|
||||
macro_map.register(foreign());
|
||||
macro_map
|
||||
}
|
||||
@ -672,6 +673,10 @@ fn foreign<'s>() -> Definition<'s> {
|
||||
crate::macro_definition! {("foreign", everything()) foreign_body}
|
||||
}
|
||||
|
||||
fn private<'s>() -> Definition<'s> {
|
||||
crate::macro_definition! {("private", everything()) private_keyword}
|
||||
}
|
||||
|
||||
fn skip<'s>() -> Definition<'s> {
|
||||
crate::macro_definition! {("SKIP", everything()) capture_expressions}
|
||||
}
|
||||
@ -680,6 +685,16 @@ fn freeze<'s>() -> Definition<'s> {
|
||||
crate::macro_definition! {("FREEZE", everything()) capture_expressions}
|
||||
}
|
||||
|
||||
fn private_keyword<'s>(
|
||||
segments: NonEmptyVec<MatchedSegment<'s>>,
|
||||
precedence: &mut operator::Precedence<'s>,
|
||||
) -> syntax::Tree<'s> {
|
||||
let segment = segments.pop().0;
|
||||
let keyword = into_private(segment.header);
|
||||
let body = precedence.resolve(segment.result.tokens());
|
||||
syntax::Tree::private(keyword, body)
|
||||
}
|
||||
|
||||
/// Macro body builder that just parses the tokens of each segment as expressions, and places them
|
||||
/// in a [`MultiSegmentApp`].
|
||||
fn capture_expressions<'s>(
|
||||
@ -783,6 +798,11 @@ fn into_ident(token: syntax::token::Token) -> syntax::token::Ident {
|
||||
syntax::token::ident(left_offset, code, false, 0, false, false, false)
|
||||
}
|
||||
|
||||
fn into_private(token: syntax::token::Token) -> syntax::token::Private {
|
||||
let syntax::token::Token { left_offset, code, .. } = token;
|
||||
syntax::token::private(left_offset, code)
|
||||
}
|
||||
|
||||
|
||||
// === Validators ===
|
||||
|
||||
|
@ -281,6 +281,7 @@ macro_rules! with_token_definition { ($f:ident ($($args:tt)*)) => { $f! { $($arg
|
||||
pub base: Option<Base>
|
||||
},
|
||||
NumberBase,
|
||||
Private,
|
||||
TextStart,
|
||||
TextEnd,
|
||||
TextSection,
|
||||
|
@ -112,6 +112,11 @@ macro_rules! with_ast_definition { ($f:ident ($($args:tt)*)) => { $f! { $($args)
|
||||
Ident {
|
||||
pub token: token::Ident<'s>,
|
||||
},
|
||||
/// A `private` keyword, marking associated expressions as project-private.
|
||||
Private {
|
||||
pub keyword: token::Private<'s>,
|
||||
pub body: Option<Tree<'s>>,
|
||||
},
|
||||
/// A numeric literal, like `10`.
|
||||
Number {
|
||||
pub base: Option<token::NumberBase<'s>>,
|
||||
@ -990,6 +995,7 @@ pub fn to_ast(token: Token) -> Tree {
|
||||
| token::Variant::BlockEnd(_)
|
||||
// This should be unreachable: `Precedence::resolve` doesn't calls `to_ast` for operators.
|
||||
| token::Variant::Operator(_)
|
||||
| token::Variant::Private(_)
|
||||
// Map an error case in the lexer to an error in the AST.
|
||||
| token::Variant::Invalid(_) => {
|
||||
let message = format!("Unexpected token: {token:?}");
|
||||
|
Loading…
Reference in New Issue
Block a user