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:
Pavel Marek 2023-10-04 12:33:10 +02:00 committed by GitHub
parent 421e3f22a4
commit c22928ecc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 770 additions and 4 deletions

View File

@ -972,6 +972,7 @@
- [Downloadable VSCode extension][7861] - [Downloadable VSCode extension][7861]
- [New `project/status` route for reporting LS state][7801] - [New `project/status` route for reporting LS state][7801]
- [Add Enso-specific assertions][7883]) - [Add Enso-specific assertions][7883])
- [Modules can be `private`][7840]
[3227]: https://github.com/enso-org/enso/pull/3227 [3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248 [3248]: https://github.com/enso-org/enso/pull/3248
@ -1117,6 +1118,7 @@
[7825]: https://github.com/enso-org/enso/pull/7825 [7825]: https://github.com/enso-org/enso/pull/7825
[7861]: https://github.com/enso-org/enso/pull/7861 [7861]: https://github.com/enso-org/enso/pull/7861
[7883]: https://github.com/enso-org/enso/pull/7883 [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) # Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -29,3 +29,5 @@ This specification is broken down into the following sections:
those of suspended computations. those of suspended computations.
- [**Modules:**](./modules.md) The semantics of Enso's modules. - [**Modules:**](./modules.md) The semantics of Enso's modules.
- [**Scoping:**](./scoping.md) Enso's scoping and identifier resolution rules. - [**Scoping:**](./scoping.md) Enso's scoping and identifier resolution rules.
- [**Encapsulation:**](./encapsulation.md) The semantics of Enso's encapsulation
system - access restriction.

View 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.

View File

@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import org.enso.compiler.core.ir.Diagnostic;
import org.enso.compiler.core.ir.IdentifiedLocation; import org.enso.compiler.core.ir.IdentifiedLocation;
import org.enso.compiler.core.ir.CallArgument; import org.enso.compiler.core.ir.CallArgument;
import org.enso.compiler.core.ir.DefinitionArgument; 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.Invalid;
import org.enso.syntax2.Tree.Private;
import scala.Option; import scala.Option;
import scala.collection.immutable.LinearSeq; import scala.collection.immutable.LinearSeq;
import scala.collection.immutable.List; import scala.collection.immutable.List;
@ -152,9 +154,11 @@ final class TreeToIr {
Module translateModule(Tree module) { Module translateModule(Tree module) {
return switch (module) { return switch (module) {
case Tree.BodyBlock b -> { case Tree.BodyBlock b -> {
boolean isPrivate = false;
List<Definition> bindings = nil(); List<Definition> bindings = nil();
List<Import> imports = nil(); List<Import> imports = nil();
List<Export> exports = nil(); List<Export> exports = nil();
List<Diagnostic> diag = nil();
for (Line line : b.getStatements()) { for (Line line : b.getStatements()) {
var expr = line.getExpression(); var expr = line.getExpression();
// Documentation found among imports/exports or at the top of the module (if it starts with imports) is // 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); bindings = cons(c, bindings);
expr = doc.getExpression(); 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) { switch (expr) {
case Tree.Import imp -> imports = cons(translateImport(imp), imports); case Tree.Import imp -> imports = cons(translateImport(imp), imports);
case Tree.Export exp -> exports = cons(translateExport(exp), exports); case Tree.Export exp -> exports = cons(translateExport(exp), exports);
@ -176,11 +192,12 @@ final class TreeToIr {
default -> bindings = translateModuleSymbol(expr, bindings); 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( default -> new Module(
nil(), nil(), nil(), nil(),
cons(translateSyntaxError(module, new Syntax.UnsupportedSyntax("translateModule")), nil()), cons(translateSyntaxError(module, new Syntax.UnsupportedSyntax("translateModule")), nil()),
false,
getIdentifiedLocation(module), meta(), diag() getIdentifiedLocation(module), meta(), diag()
); );
}; };

View File

@ -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 imports the import statements that bring other modules into scope
* @param exports the export statements for this module * @param exports the export statements for this module
* @param bindings the top-level bindings 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 location the source location that the node corresponds to
* @param passData the pass metadata associated with this node * @param passData the pass metadata associated with this node
* @param diagnostics compiler diagnostics for this node * @param diagnostics compiler diagnostics for this node
@ -23,6 +24,7 @@ sealed case class Module(
imports: List[Import], imports: List[Import],
exports: List[Export], exports: List[Export],
bindings: List[Definition], bindings: List[Definition],
isPrivate: Boolean,
override val location: Option[IdentifiedLocation], override val location: Option[IdentifiedLocation],
override val passData: MetadataStorage = MetadataStorage(), override val passData: MetadataStorage = MetadataStorage(),
override val diagnostics: DiagnosticStorage = DiagnosticStorage() override val diagnostics: DiagnosticStorage = DiagnosticStorage()
@ -51,7 +53,15 @@ sealed case class Module(
id: Identifier = id id: Identifier = id
): Module = { ): Module = {
val res = val res =
Module(imports, exports, bindings, location, passData, diagnostics) Module(
imports,
exports,
bindings,
isPrivate,
location,
passData,
diagnostics
)
res.id = id res.id = id
res res
} }

View File

@ -171,6 +171,38 @@ object ImportExport {
s"No such constructor ${constructorName} in type $typeName" 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 /** 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. * to different objects. The objects are represented by their physical path in the project.
* *

View File

@ -5,11 +5,14 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import org.enso.compiler.core.ir.Module; import org.enso.compiler.core.ir.Module;
import org.junit.AfterClass; import org.junit.AfterClass;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -1239,6 +1242,29 @@ public class EnsoParserTest {
equivalenceTest("a = x", "a = SKIP FREEZE x.f y"); 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 @Test
public void ise_184219679() throws IOException { public void ise_184219679() throws IOException {
parseTest(""" parseTest("""

View File

@ -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);
}
}

View File

@ -193,6 +193,11 @@ public final class Module implements EnsoObject {
return synthetic; return synthetic;
} }
/** @return true iff this module is private (project-private). */
public boolean isPrivate() {
return ir.isPrivate();
}
/** /**
* Sets new literal sources for the module. * Sets new literal sources for the module.
* *

View File

@ -28,4 +28,14 @@ public class ScalaConversions {
public static <T> List<T> asJava(Seq<T> list) { public static <T> List<T> asJava(Seq<T> list) {
return CollectionConverters.asJava(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);
}
} }

View File

@ -49,6 +49,7 @@ class Passes(
LambdaShorthandToLambda, LambdaShorthandToLambda,
ImportSymbolAnalysis, ImportSymbolAnalysis,
AmbiguousImportsAnalysis, AmbiguousImportsAnalysis,
PrivateModuleAnalysis.getInstance(),
ShadowedPatternFields, ShadowedPatternFields,
UnreachableMatchBranches, UnreachableMatchBranches,
NestedPatternMatch, NestedPatternMatch,

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
private
type Type_In_Priv_Mod
Value data

View File

@ -0,0 +1,4 @@
type Pub_Mod_Type
Value data

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
# This import should fail - importing private module
import local.Test_Private_Modules_1.Priv_Mod
main =
"Success"

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
private
foo = 23

View File

@ -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

View File

@ -0,0 +1,7 @@
# This project has a private module tree
import project.Sub.Priv
main =
Priv.foo

View File

@ -0,0 +1,3 @@
# This is a private non-synthetic module with submodules
private

View File

@ -0,0 +1,3 @@
private
foo = 42

View File

@ -258,7 +258,7 @@ trait CompilerRunner {
runtime.Module.empty(QualifiedName.simpleName("Test_Module"), null) runtime.Module.empty(QualifiedName.simpleName("Test_Module"), null)
ModuleTestUtils.unsafeSetIr( ModuleTestUtils.unsafeSetIr(
mod, mod,
Module(List(), List(), List(), None) Module(List(), List(), List(), false, None)
.updateMetadata( .updateMetadata(
BindingAnalysis -->> BindingsMap( BindingAnalysis -->> BindingsMap(
List(), List(),

View File

@ -9,7 +9,8 @@ import org.enso.compiler.pass.analyse.{
AliasAnalysis, AliasAnalysis,
AmbiguousImportsAnalysis, AmbiguousImportsAnalysis,
BindingAnalysis, BindingAnalysis,
ImportSymbolAnalysis ImportSymbolAnalysis,
PrivateModuleAnalysis
} }
import org.enso.compiler.pass.desugar._ import org.enso.compiler.pass.desugar._
import org.enso.compiler.pass.lint.{ModuleNameConflicts, ShadowedPatternFields} import org.enso.compiler.pass.lint.{ModuleNameConflicts, ShadowedPatternFields}
@ -60,6 +61,7 @@ class PassesTest extends CompilerTest {
LambdaShorthandToLambda, LambdaShorthandToLambda,
ImportSymbolAnalysis, ImportSymbolAnalysis,
AmbiguousImportsAnalysis, AmbiguousImportsAnalysis,
PrivateModuleAnalysis.getInstance(),
ShadowedPatternFields, ShadowedPatternFields,
UnreachableMatchBranches, UnreachableMatchBranches,
NestedPatternMatch, NestedPatternMatch,

View File

@ -98,6 +98,7 @@ class GatherDiagnosticsTest extends CompilerTest {
definition.Method definition.Method
.Explicit(method2Ref, error3, None) .Explicit(method2Ref, error3, None)
), ),
false,
None None
) )

View File

@ -905,4 +905,97 @@ class ImportExportTest
mainIr.imports.head.isInstanceOf[errors.ImportExport] shouldBe false 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
}
}
} }

View File

@ -219,4 +219,41 @@ class ImportsTest extends PackageTest {
outLines(2) shouldEqual "(D_Mod.Value 1)" 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
}
} }

View File

@ -55,6 +55,7 @@ where T: serde::Serialize + Reflect {
TextEnd::reflect(), TextEnd::reflect(),
TextStart::reflect(), TextStart::reflect(),
Wildcard::reflect(), Wildcard::reflect(),
Private::reflect(),
]; ];
skip_tokens.into_iter().for_each(|token| to_s_expr.skip(rust_to_meta[&token.id])); 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]; let ident_token = rust_to_meta[&Ident::reflect().id];

View File

@ -1189,6 +1189,35 @@ fn pattern_match_auto_scope() {
test(&code.join("\n"), expected); 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 === // === Array/tuple literals ===

View File

@ -38,6 +38,7 @@ fn statement() -> resolver::SegmentMap<'static> {
register_import_macros(&mut macro_map); register_import_macros(&mut macro_map);
register_export_macros(&mut macro_map); register_export_macros(&mut macro_map);
macro_map.register(type_def()); macro_map.register(type_def());
macro_map.register(private());
macro_map.register(foreign()); macro_map.register(foreign());
macro_map macro_map
} }
@ -672,6 +673,10 @@ fn foreign<'s>() -> Definition<'s> {
crate::macro_definition! {("foreign", everything()) foreign_body} crate::macro_definition! {("foreign", everything()) foreign_body}
} }
fn private<'s>() -> Definition<'s> {
crate::macro_definition! {("private", everything()) private_keyword}
}
fn skip<'s>() -> Definition<'s> { fn skip<'s>() -> Definition<'s> {
crate::macro_definition! {("SKIP", everything()) capture_expressions} crate::macro_definition! {("SKIP", everything()) capture_expressions}
} }
@ -680,6 +685,16 @@ fn freeze<'s>() -> Definition<'s> {
crate::macro_definition! {("FREEZE", everything()) capture_expressions} 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 /// Macro body builder that just parses the tokens of each segment as expressions, and places them
/// in a [`MultiSegmentApp`]. /// in a [`MultiSegmentApp`].
fn capture_expressions<'s>( 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) 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 === // === Validators ===

View File

@ -281,6 +281,7 @@ macro_rules! with_token_definition { ($f:ident ($($args:tt)*)) => { $f! { $($arg
pub base: Option<Base> pub base: Option<Base>
}, },
NumberBase, NumberBase,
Private,
TextStart, TextStart,
TextEnd, TextEnd,
TextSection, TextSection,

View File

@ -112,6 +112,11 @@ macro_rules! with_ast_definition { ($f:ident ($($args:tt)*)) => { $f! { $($args)
Ident { Ident {
pub token: token::Ident<'s>, 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`. /// A numeric literal, like `10`.
Number { Number {
pub base: Option<token::NumberBase<'s>>, pub base: Option<token::NumberBase<'s>>,
@ -990,6 +995,7 @@ pub fn to_ast(token: Token) -> Tree {
| token::Variant::BlockEnd(_) | token::Variant::BlockEnd(_)
// This should be unreachable: `Precedence::resolve` doesn't calls `to_ast` for operators. // This should be unreachable: `Precedence::resolve` doesn't calls `to_ast` for operators.
| token::Variant::Operator(_) | token::Variant::Operator(_)
| token::Variant::Private(_)
// Map an error case in the lexer to an error in the AST. // Map an error case in the lexer to an error in the AST.
| token::Variant::Invalid(_) => { | token::Variant::Invalid(_) => {
let message = format!("Unexpected token: {token:?}"); let message = format!("Unexpected token: {token:?}");