Skip to content

Commit

Permalink
Types with private constructors can be public (#12052)
Browse files Browse the repository at this point in the history
  • Loading branch information
JaroslavTulach authored Jan 15, 2025
1 parent 4324e65 commit 27d9419
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 87 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
parentheses. [This is now a syntax error.][11856]
- [Native libraries of projects can be added to `polyglot/lib` directory][11874]
- [Prefer module methods over `Any` instance ones][12048]
- [Types without constructors can be public][12052]
- Symetric, transitive and reflexive [equality for intersection types][11897]
- [IR definitions are generated by an annotation processor][11770]

Expand All @@ -38,6 +39,7 @@
[11856]: https://github.com/enso-org/enso/pull/11856
[11874]: https://github.com/enso-org/enso/pull/11874
[12048]: https://github.com/enso-org/enso/pull/12048
[12052]: https://github.com/enso-org/enso/pull/12052
[11897]: https://github.com/enso-org/enso/pull/11897
[11770]: https://github.com/enso-org/enso/pull/11770

Expand Down
44 changes: 32 additions & 12 deletions docs/semantics/encapsulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ 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.

To make a _type private_ put it into a _private module_. Then it is hidden, just
like anything else in the module.

### Methods

Methods on types (or on modules) can be specified private. To check whether a
Expand All @@ -89,52 +92,69 @@ private entities are not visible.

## Example

Lib/src/Pub_Type.enso:
Lib/src/Open_Type.enso:

```
type Pub_Type
```ruby
type Open_Type
Constructor field
private priv_method self = ...
pub_method self = self.field.to_text
```

Lib/src/Methods.enso:
Lib/src/Closed_Type.enso:

```ruby
type Closed_Type
private Constructor field
private priv_method self = ...
factory field = Closed_Type.Constructor field
pub_method self = self.field.to_text
```

Lib/src/Methods.enso:

```ruby
pub_stat_method x y = x + y
private priv_stat_method x y = x - y
```

Lib/src/Internal/Helpers.enso:

```
```ruby
# Mark the whole module as private
private

```

Lib/src/Main.enso:

```
import project.Pub_Type.Pub_Type
export project.Pub_Type.Pub_Type
```ruby
import project.Open_Type.Open_Type
export project.Open_Type.Open_Type
```

tmp.enso:

```
from Lib import Pub_Type
```ruby
from Lib import Open_Type, Closed_Type
import Lib.Methods
import Lib.Internal.Helpers # Fails during compilation - cannot import private module from different project

main =
# This constructor is not private, we can use it here.
obj = Pub_Type.Constructor field=42
obj = Open_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
Open_Type.priv_method self=obj # Runtime failure
obj.pub_method # OK

# This constructor is private, we have to use factory method.
opaque = Closed_Type.Constructor field=42
opaque.field # Runtime failure - Constructor is private, therefore, no getter is generated
opaque.priv_method # Runtime failure - priv_method is private
Closed_Type.priv_method self=opaque # Runtime failure
opaque.pub_method # OK

Methods.pub_stat_method 1 2 # OK
Methods.priv_stat_method # Fails at runtime
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.oracle.truffle.api.interop.InteropLibrary;
Expand Down Expand Up @@ -38,6 +39,32 @@ public void privateConstructorCanBeCalledInUnknownProject() {
}
}

@Test
public void accessMethodOnATypeWithAllPrivateConstructors() throws IOException {
var codeA =
"""
type A
private Cons data
find d = A.Cons d
""";
var codeUse =
"""
import local.Proj_A
create x = local.Proj_A.Main.A.find x
main = create
""";
var proj1Dir = tempFolder.newFolder("Proj_A").toPath();
ProjectUtils.createProject("Proj_A", codeA, proj1Dir);
var proj2Dir = tempFolder.newFolder("Proj_Use").toPath();
ProjectUtils.createProject("Proj_Use", codeUse, proj2Dir);
ProjectUtils.testProjectRun(
proj2Dir,
create -> {
var res = create.execute("Hello");
assertEquals("It is object: " + res, "(Cons 'Hello')", res.toString());
});
}

@Test
public void privateConstructorIsNotExposedToPolyglot() throws IOException {
var mainSrc = """
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package org.enso.interpreter.node;

import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.RootNode;
import org.enso.compiler.context.LocalScope;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.runtime.scope.ModuleScope;

public final class ConstantNode extends RootNode {
public final class ConstantNode extends EnsoRootNode {
private final Object constant;

/**
* Creates a new instance of this node.
*
* @param language the current language instance.
* @param atomConstructor the constructor to return.
* @param moduleScope the scope
* @param constant the value to return.
*/
public ConstantNode(TruffleLanguage<?> language, Object constant) {
super(language);
public ConstantNode(EnsoLanguage language, ModuleScope moduleScope, Object constant) {
super(language, LocalScope.empty(), moduleScope, constant.toString(), null);
this.constant = constant;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public final void initialize(
}
type =
containsValues()
? Type.create(name, scope, supertype, builtins.get(Any.class).getType(), true, false)
? Type.create(
language, name, scope, supertype, builtins.get(Any.class).getType(), true, false)
: Type.createSingleton(name, scope, supertype, true, false);
}
if (constructors == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public final class ConstantObjectNode extends ExpressionNode {
private final Object object;

private ConstantObjectNode(Object object) {
assert object != null;
this.object = Objects.requireNonNull(object);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public static class Debug {
}
}

private final EnsoContext context;
private final Map<Class<? extends Builtin>, Builtin> builtins;
private final Map<String, Map<String, Supplier<LoadedBuiltinMethod>>> builtinMethodNodes;
private final Map<String, Builtin> builtinsByName;
Expand Down Expand Up @@ -129,6 +130,7 @@ public static class Debug {
* @param context the current {@link EnsoContext} instance
*/
public Builtins(EnsoContext context) {
this.context = context;
EnsoLanguage language = context.getLanguage();
module = Module.empty(QualifiedName.fromString(MODULE_NAME), null);
module.compileScope(context); // Dummy compilation for an empty module
Expand Down Expand Up @@ -780,6 +782,10 @@ public Module getModule() {
return module;
}

public EnsoLanguage getLanguage() {
return context.getLanguage();
}

private static class LoadedBuiltinMetaMethod {

private LoadedBuiltinMethod method;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public final class Type extends EnsoObject {
private final Type supertype;
private final Type eigentype;
private final Map<String, AtomConstructor> constructors;
private final boolean isProjectPrivate;
private final boolean hasAllConstructorsPrivate;

private boolean gettersGenerated;
private Map<String, Function> methods;
Expand All @@ -59,12 +59,12 @@ private Type(
Type supertype,
Type eigentype,
boolean builtin,
boolean isProjectPrivate) {
boolean hasAllConstructorsPrivate) {
this.name = name;
this.definitionScope = definitionScope;
this.supertype = supertype;
this.builtin = builtin;
this.isProjectPrivate = isProjectPrivate;
this.hasAllConstructorsPrivate = hasAllConstructorsPrivate;
this.eigentype = Objects.requireNonNullElse(eigentype, this);
this.constructors = new HashMap<>();
}
Expand All @@ -74,35 +74,39 @@ public static Type createSingleton(
ModuleScope.Builder definitionScope,
Type supertype,
boolean builtin,
boolean isProjectPrivate) {
return new Type(name, definitionScope, supertype, null, builtin, isProjectPrivate);
boolean hasAllConstructorsPrivate) {
return new Type(name, definitionScope, supertype, null, builtin, hasAllConstructorsPrivate);
}

public static Type create(
EnsoLanguage lang,
String name,
ModuleScope.Builder definitionScope,
Type supertype,
Type any,
boolean builtin,
boolean isProjectPrivate) {
var eigentype = new Type(name + ".type", definitionScope, any, null, builtin, isProjectPrivate);
var result = new Type(name, definitionScope, supertype, eigentype, builtin, isProjectPrivate);
result.generateQualifiedAccessor();
boolean hasAllConstructorsPrivate) {
var eigentype =
new Type(name + ".type", definitionScope, any, null, builtin, hasAllConstructorsPrivate);
var result =
new Type(name, definitionScope, supertype, eigentype, builtin, hasAllConstructorsPrivate);
result.generateQualifiedAccessor(lang);
return result;
}

public static Type noType() {
return new Type("null", null, null, null, false, false);
}

private void generateQualifiedAccessor() {
var node = new ConstantNode(null, this);
private void generateQualifiedAccessor(EnsoLanguage lang) {
assert lang != null;
var node = new ConstantNode(lang, getDefinitionScope(), this);
var schemaBldr =
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0, "this", null, null, ArgumentDefinition.ExecutionMode.EXECUTE));
if (isProjectPrivate) {
if (isProjectPrivate()) {
schemaBldr.projectPrivate();
}
var function = new Function(node.getCallTarget(), null, schemaBldr.build());
Expand All @@ -118,17 +122,18 @@ public QualifiedName getQualifiedName() {
}
}

public void setShadowDefinitions(ModuleScope.Builder scope, boolean generateAccessorsInTarget) {
public void setShadowDefinitions(
EnsoLanguage lang, ModuleScope.Builder scope, boolean generateAccessorsInTarget) {
if (builtin) {
// Ensure that synthetic methods, such as getters for fields are in the scope.
CompilerAsserts.neverPartOfCompilation();
this.definitionScope.registerAllMethodsOfTypeToScope(this, scope);
this.definitionScope = scope;
if (generateAccessorsInTarget) {
generateQualifiedAccessor();
generateQualifiedAccessor(lang);
}
if (getEigentype() != this) {
getEigentype().setShadowDefinitions(scope, false);
getEigentype().setShadowDefinitions(lang, scope, false);
}
} else {
throw new RuntimeException(
Expand All @@ -149,15 +154,24 @@ public boolean isBuiltin() {
}

/**
* Returns true iff this type is project-private. A type is project-private iff all its
* constructors are project-private. Note that during the compilation, it is ensured by the {@link
* Right now types cannot be project private. Waiting for implementation of #8835.
*
* @return {@code false}
*/
public final boolean isProjectPrivate() {
return false;
}

/**
* Returns true iff this type has project-private constructors only. Note that during the
* compilation, it is ensured by the {@link
* org.enso.compiler.pass.analyse.PrivateConstructorAnalysis} compiler pass that all the
* constructors are either public or project-private.
*
* @return true iff this type is project-private.
* @return true iff this type constructors are project-private.
*/
public boolean isProjectPrivate() {
return isProjectPrivate;
public boolean hasAllConstructorsPrivate() {
return hasAllConstructorsPrivate;
}

private Type getSupertype() {
Expand Down Expand Up @@ -240,7 +254,7 @@ public void generateGetters(EnsoLanguage language) {
null,
null,
ArgumentDefinition.ExecutionMode.EXECUTE));
if (isProjectPrivate) {
if (hasAllConstructorsPrivate) {
schemaBldr.projectPrivate();
}
var funcSchema = schemaBldr.build();
Expand Down Expand Up @@ -362,7 +376,7 @@ boolean hasMembers() {
@ExportMessage
@CompilerDirectives.TruffleBoundary
EnsoObject getMembers(boolean includeInternal) {
if (isProjectPrivate) {
if (hasAllConstructorsPrivate) {
return ArrayLikeHelpers.empty();
}
var consNames = constructors.keySet();
Expand All @@ -379,7 +393,7 @@ EnsoObject getMembers(boolean includeInternal) {
@ExportMessage
@CompilerDirectives.TruffleBoundary
boolean isMemberReadable(String member) {
if (isProjectPrivate) {
if (hasAllConstructorsPrivate) {
return false;
} else {
return constructors.containsKey(member) || methods().containsKey(member);
Expand Down Expand Up @@ -451,7 +465,7 @@ static InvokeFunctionNode buildInvokeFuncNode(Function func) {
@ExportMessage
@CompilerDirectives.TruffleBoundary
Object readMember(String member) throws UnknownIdentifierException {
if (isProjectPrivate) {
if (hasAllConstructorsPrivate) {
throw UnknownIdentifierException.create(member);
}
var cons = constructors.get(member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,6 @@ boolean hasMetaObject() {
}

private boolean hasProjectPrivateConstructor() {
return constructor.getType().isProjectPrivate();
return constructor.getType().hasAllConstructorsPrivate();
}
}
Loading

0 comments on commit 27d9419

Please sign in to comment.