Atom constructors can be private (#9692)

Closes #8836.

Atom constructors can be declared as private (project-private). project-private constructors can be called only from the same project. See the encapsulation.md docs for more info.

---------

Co-authored-by: Jaroslav Tulach <jaroslav.tulach@enso.org>
Co-authored-by: Radosław Waśko <radoslaw.wasko@enso.org>
Co-authored-by: Hubert Plociniczak <hubert.plociniczak@gmail.com>
Co-authored-by: Kaz Wesley <kaz@lambdaverse.org>
This commit is contained in:
Pavel Marek 2024-04-29 14:43:18 +02:00 committed by GitHub
parent 16e1999040
commit 660c5e7a9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1540 additions and 204 deletions

View File

@ -1127,6 +1127,7 @@
- [Autoscoped constructors][9190]
- [Allow Oracle GraalVM JDK][9322]
- [`Table.join` can access its `right` argument][9410]
- [Atom constructors can be project-private][9692]
[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
@ -1287,6 +1288,7 @@
[8867]: https://github.com/enso-org/enso/pull/8867
[9190]: https://github.com/enso-org/enso/pull/9190
[9322]: https://github.com/enso-org/enso/pull/9322
[9692]: https://github.com/enso-org/enso/pull/9692
[9410]: https://github.com/enso-org/enso/pull/9410
# Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -307,6 +307,32 @@ type Not_Invokable
to_display_text : Text
to_display_text self = "Type error: expected a function, but got "+self.target.to_display_text+"."
@Builtin_Type
type Private_Access
## PRIVATE
An error that occurs when a private (project-private) method is called from a different
project.
Arguments:
- this_project_name: Optional qualified name of the current (caller) project.
- target_project_name: Optional qualified name name of the project of callee.
- target_method_name: The name of the target method call that is project-private and thus cannot be called.
Error (this_project_name : (Text | Nothing)) (target_project_name : (Text | Nothing)) (target_method_name : Text)
## PRIVATE
Convert the Private_Access error to a human-readable format.
to_display_text : Text
to_display_text self =
this_proj = if self.this_project_name.is_nothing then "unknown project" else "project '"+self.this_project_name+"'"
target_proj = if self.target_project_name.is_nothing then "unknown project" else "project '"+self.target_project_name+"'"
"Private access error: The project-private method '"
+ self.target_method_name
+ "' in "
+ target_proj
+ " cannot be accessed from "
+ this_proj
+ "."
@Builtin_Type
type Unsupported_Argument_Types
## PRIVATE

View File

@ -78,6 +78,11 @@ 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.
### Polyglot access
No polyglot foreign code can access private entities. For all the foreign code,
private entities are not visible.
## Example
Lib/src/Pub_Type.enso:
@ -87,8 +92,6 @@ type Pub_Type
Constructor field
private priv_method self = ...
pub_method self = self.field.to_text
private type Priv_Type
```
Lib/src/Methods.enso:
@ -104,8 +107,6 @@ 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:
@ -113,17 +114,14 @@ 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
import Lib.Internal.Helpers # Fails during compilation - cannot import private module
main =
# This constructor is not private, we can use it here.
@ -133,8 +131,6 @@ main =
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
```

View File

@ -16,6 +16,9 @@ class Function(val value: Value) {
*/
def execute(args: AnyRef*): Value = value.execute(args: _*)
/** Helper method for java that just delegates to the other execute method */
def execute(): Value = value.execute()
/** Checks function equality by checking the identity of the underlying
* objects.
*

View File

@ -9,6 +9,8 @@ import java.util.Objects;
import java.util.stream.Collectors;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.MetadataStorage;
import org.enso.compiler.pass.IRPass.IRMetadata;
import org.enso.compiler.pass.resolve.DocumentationComments;
/**
* Represents a node in the GraphViz graph.
@ -84,6 +86,9 @@ record GraphVizNode(
private Map<String, String> additionalAttrs = new HashMap<>();
private Object object;
private static final List<Class<? extends IRMetadata>> metadataToSkip =
List.of(DocumentationComments.Doc.class);
static Builder fromObject(Object obj) {
var className = className(obj);
var id = Utils.id(obj);
@ -129,8 +134,10 @@ record GraphVizNode(
ir.passData()
.map(
(pass, metadata) -> {
var metaName = metadata.metadataName();
bldr.addLabelLine(" - " + metaName);
if (!metadataToSkip.contains(metadata.getClass())) {
var metaName = metadata.metadataName();
bldr.addLabelLine(" - " + metaName);
}
return null;
});
} else {

View File

@ -0,0 +1,91 @@
package org.enso.compiler.pass.analyse;
import java.util.List;
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.Syntax;
import org.enso.compiler.core.ir.expression.errors.Syntax.InconsistentConstructorVisibility$;
import org.enso.compiler.core.ir.module.scope.Definition;
import org.enso.compiler.pass.IRPass;
import scala.collection.immutable.Seq;
import scala.jdk.javaapi.CollectionConverters;
/**
* Ensures that all type definitions have either all constructors public, or all constructors
* private.
*/
public final class PrivateConstructorAnalysis implements IRPass {
public static final PrivateConstructorAnalysis INSTANCE = new PrivateConstructorAnalysis();
private UUID uuid;
private PrivateConstructorAnalysis() {}
@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(PrivateModuleAnalysis.INSTANCE);
return CollectionConverters.asScala(passes).toList();
}
@Override
public Seq<IRPass> invalidatedPasses() {
Object obj = scala.collection.immutable.Nil$.MODULE$;
return (scala.collection.immutable.List<IRPass>) obj;
}
@Override
public Module runModule(Module ir, ModuleContext moduleContext) {
var newBindings =
ir.bindings()
.map(
binding -> {
if (binding instanceof Definition.Type type) {
var privateCtorsCnt = type.members().filter(ctor -> ctor.isPrivate()).size();
var publicCtorsCnt = type.members().filter(ctor -> !ctor.isPrivate()).size();
var ctorsCnt = type.members().size();
if (!(privateCtorsCnt == ctorsCnt || publicCtorsCnt == ctorsCnt)) {
assert type.location().isDefined();
return Syntax.apply(
type.location().get(),
InconsistentConstructorVisibility$.MODULE$,
type.passData(),
type.diagnostics());
}
}
return binding;
});
return ir.copy(
ir.imports(),
ir.exports(),
newBindings,
ir.location(),
ir.passData(),
ir.diagnostics(),
ir.id());
}
/** Not supported on a single expression. */
@Override
public Expression runExpression(Expression ir, InlineContext inlineContext) {
return ir;
}
@Override
public <T extends IR> T updateMetadataInDuplicate(T sourceIr, T copyOfIr) {
return IRPass.super.updateMetadataInDuplicate(sourceIr, copyOfIr);
}
}

View File

@ -0,0 +1,210 @@
package org.enso.compiler.pass.analyse;
import java.util.List;
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.Implicits;
import org.enso.compiler.core.ir.Expression;
import org.enso.compiler.core.ir.Module;
import org.enso.compiler.core.ir.Name;
import org.enso.compiler.core.ir.Pattern;
import org.enso.compiler.core.ir.expression.Case;
import org.enso.compiler.core.ir.expression.Case.Branch;
import org.enso.compiler.data.BindingsMap;
import org.enso.compiler.data.BindingsMap.ResolvedConstructor;
import org.enso.compiler.data.BindingsMap.ResolvedModule;
import org.enso.compiler.data.BindingsMap.ResolvedName;
import org.enso.compiler.pass.IRPass;
import org.enso.compiler.pass.resolve.Patterns$;
import org.enso.pkg.QualifiedName;
import scala.collection.immutable.Seq;
import scala.jdk.javaapi.CollectionConverters;
/**
* Iterates all the symbols in the IR and checks for their {@link
* org.enso.compiler.data.BindingsMap.Resolution} metadata. If they are present, it checks if the
* symbol is private and if it is used outside of its defining project. If so, a private-access
* error IR is generated.
*/
public class PrivateSymbolsAnalysis implements IRPass {
public static final PrivateSymbolsAnalysis INSTANCE = new PrivateSymbolsAnalysis();
private UUID uuid;
private PrivateSymbolsAnalysis() {}
@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(
PrivateModuleAnalysis.INSTANCE, PrivateConstructorAnalysis.INSTANCE, Patterns$.MODULE$);
return CollectionConverters.asScala(passes).toList();
}
@Override
public Seq<IRPass> invalidatedPasses() {
return nil();
}
@Override
public <T extends IR> T updateMetadataInDuplicate(T sourceIr, T copyOfIr) {
return IRPass.super.updateMetadataInDuplicate(sourceIr, copyOfIr);
}
@Override
public Module runModule(Module ir, ModuleContext moduleContext) {
var bindingsMap =
(BindingsMap)
Implicits.AsMetadata(ir)
.unsafeGetMetadata(BindingAnalysis$.MODULE$, () -> "BindingsMap should be present");
var newBindings =
ir.bindings()
.map(binding -> binding.mapExpressions(expr -> processExpression(expr, bindingsMap)));
return ir.copy(
ir.imports(),
ir.exports(),
newBindings,
ir.location(),
ir.passData(),
ir.diagnostics(),
ir.id());
}
/** Not supported for expressions. */
@Override
public Expression runExpression(Expression ir, InlineContext inlineContext) {
return ir;
}
@SuppressWarnings("unchecked")
private Expression processExpression(Expression expr, BindingsMap bindingsMap) {
return switch (expr) {
case Case.Expr caseExpr -> {
var newBranches =
caseExpr.branches().map(branch -> processCaseBranch(branch, bindingsMap)).toSeq();
var newScrutinee = processExpression(caseExpr.scrutinee(), bindingsMap);
yield caseExpr.copy(
newScrutinee,
newBranches,
caseExpr.isNested(),
caseExpr.location(),
caseExpr.passData(),
caseExpr.diagnostics(),
caseExpr.id());
}
case Name name -> processName(name, bindingsMap);
default -> expr.mapExpressions(e -> processExpression(e, bindingsMap));
};
}
private Branch processCaseBranch(Branch branch, BindingsMap bindingsMap) {
var pat = branch.pattern();
var newPat = processCasePattern(pat, bindingsMap);
var newExpr = processExpression(branch.expression(), bindingsMap);
return branch.copy(
newPat,
newExpr,
branch.terminalBranch(),
branch.location(),
branch.passData(),
branch.diagnostics(),
branch.id());
}
private Pattern processCasePattern(Pattern pattern, BindingsMap bindingsMap) {
if (pattern instanceof Pattern.Constructor cons) {
var consName = cons.constructor();
var resolvedCons = tryResolveName(consName, bindingsMap);
if (resolvedCons != null && isProjectPrivate(resolvedCons)) {
var curProjName = getProjName(bindingsMap.currentModule().getName());
var resolvedProjName = getProjName(resolvedCons.module().getName());
if (!curProjName.equals(resolvedProjName)) {
var reason =
new org.enso.compiler.core.ir.expression.errors.Pattern.PrivateConstructor(
consName.name(), curProjName, resolvedProjName);
return new org.enso.compiler.core.ir.expression.errors.Pattern(
cons, reason, cons.passData(), cons.diagnostics());
}
}
}
return pattern.mapExpressions(e -> processExpression(e, bindingsMap));
}
private Expression processName(Name name, BindingsMap bindingsMap) {
var resolvedName = tryResolveName(name, bindingsMap);
if (resolvedName != null && isProjectPrivate(resolvedName)) {
var curProjName = getProjName(bindingsMap.currentModule().getName());
var resolvedProjName = getProjName(resolvedName.module().getName());
if (!curProjName.equals(resolvedProjName)) {
var reason =
new org.enso.compiler.core.ir.expression.errors.Resolution.PrivateEntity(
curProjName, resolvedProjName);
return new org.enso.compiler.core.ir.expression.errors.Resolution(
name, reason, name.passData(), name.diagnostics());
}
}
return name.mapExpressions(e -> processExpression(e, bindingsMap));
}
private static boolean isProjectPrivate(ResolvedName resolvedName) {
return switch (resolvedName) {
case ResolvedConstructor resolvedCons -> resolvedCons.cons().isProjectPrivate();
case ResolvedModule resolvedMod -> {
if (resolvedMod.module()
instanceof org.enso.compiler.data.BindingsMap$ModuleReference$Concrete concreteMod) {
yield concreteMod.module().isPrivate();
} else {
yield false;
}
}
default -> false;
};
}
private static String getProjName(QualifiedName qualName) {
if (qualName.pathAsJava().size() < 2) {
return "unknown";
} else {
return qualName.pathAsJava().get(0) + "." + qualName.pathAsJava().get(1);
}
}
private ResolvedName tryResolveName(Name name, BindingsMap bindingsMap) {
return switch (name) {
case Name.Literal lit -> {
var resolved = bindingsMap.resolveName(lit.name());
if (resolved.isRight()) {
yield (ResolvedName) resolved.getOrElse(() -> null);
} else {
yield null;
}
}
case Name.Qualified qual -> {
var nameParts = qual.parts().map(Name::name);
var resolved = bindingsMap.resolveQualifiedName(nameParts);
if (resolved.isRight()) {
yield (ResolvedName) resolved.getOrElse(() -> null);
} else {
yield null;
}
}
default -> null;
};
}
@SuppressWarnings("unchecked")
private static <T> scala.collection.immutable.List<T> nil() {
return (scala.collection.immutable.List<T>) scala.collection.immutable.Nil$.MODULE$;
}
}

View File

@ -50,7 +50,8 @@ class Passes(
AmbiguousImportsAnalysis
) ++ (if (config.privateCheckEnabled) {
List(
PrivateModuleAnalysis.INSTANCE
PrivateModuleAnalysis.INSTANCE,
PrivateConstructorAnalysis.INSTANCE
)
} else List())
++ List(
@ -89,7 +90,10 @@ class Passes(
DemandAnalysis,
AliasAnalysis,
TailCall,
Patterns,
Patterns
) ++ (if (config.privateCheckEnabled) {
List(PrivateSymbolsAnalysis.INSTANCE)
} else List()) ++ List(
AliasAnalysis,
DataflowAnalysis,
CachePreferenceAnalysis,

View File

@ -717,8 +717,14 @@ object BindingsMap {
* @param name the name of the constructor.
* @param arity the number of fields in the constructor.
* @param allFieldsDefaulted whether all fields provide a default value.
* @param isProjectPrivate whether this constructor is project-private.
*/
case class Cons(name: String, arity: Int, allFieldsDefaulted: Boolean)
case class Cons(
name: String,
arity: Int,
allFieldsDefaulted: Boolean,
isProjectPrivate: Boolean
)
/** A representation of a sum type
*

View File

@ -63,7 +63,8 @@ case object BindingAnalysis extends IRPass {
Cons(
m.name.name,
m.arguments.length,
m.arguments.forall(_.defaultValue.isDefined)
m.arguments.forall(_.defaultValue.isDefined),
m.isPrivate
)
),
isBuiltinType

View File

@ -111,7 +111,7 @@ object Patterns extends IRPass {
selfTypeResolution.map(Right(_))
case _ => None
}
val resolvedName = resolution
val resolvedName: Name = resolution
.map {
case Left(err) =>
val r = errors.Resolution(

View File

@ -521,6 +521,20 @@ public class ErrorCompilerTest extends CompilerTest {
27);
}
@Test
public void testUnsupportedPrivateModifierInTypeDefinition() throws Exception {
var ir = parse("""
type T
private method self = 42
""");
assertSingleSyntaxError(
ir,
Syntax.UnexpectedDeclarationInType$.MODULE$,
"Unexpected declaration in the body of a type",
9,
33);
}
@Test
public void testAnnotation1() throws Exception {
var ir = parse("""

View File

@ -0,0 +1,218 @@
package org.enso.interpreter.test;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.enso.interpreter.util.ScalaConversions;
import org.enso.polyglot.PolyglotContext;
import org.enso.polyglot.RuntimeOptions;
import org.graalvm.polyglot.PolyglotException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class PrivateAccessTest extends TestBase {
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void privateConstructorCanBeCalledInUnknownProject() {
var src =
"""
type My_Type
private Cons data
main =
obj = My_Type.Cons 42
obj.data
""";
try (var ctx = createDefaultContext()) {
var res = TestBase.evalModule(ctx, src);
assertThat(res.isNumber(), is(true));
assertThat(res.asInt(), is(42));
}
}
@Test
public void privateFieldIsNotExposedToPolyglot() throws IOException {
var mainSrc =
"""
type My_Type
private Cons data
main = My_Type.Cons 42
""";
var projDir = createProject("My_Project", mainSrc);
var mainSrcPath = projDir.resolve("src").resolve("Main.enso");
try (var ctx =
defaultContextBuilder()
.option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString())
.build()) {
var polyCtx = new PolyglotContext(ctx);
var mainMod = polyCtx.evalModule(mainSrcPath.toFile());
var assocType = mainMod.getAssociatedType();
var mainMethod = mainMod.getMethod(assocType, "main").get();
var res = mainMethod.execute();
assertThat(res.hasMember("data"), is(false));
assertThat(res.canInvokeMember("data"), is(false));
assertThat(res.getMember("data"), is(nullValue()));
}
}
@Test
public void privateConstructorIsNotExposedToPolyglot() throws IOException {
var mainSrc = """
type My_Type
private Cons data
""";
var projDir = createProject("My_Project", mainSrc);
var mainSrcPath = projDir.resolve("src").resolve("Main.enso");
try (var ctx =
defaultContextBuilder()
.option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString())
.build()) {
var polyCtx = new PolyglotContext(ctx);
var mainMod = polyCtx.evalModule(mainSrcPath.toFile());
var myType = mainMod.getType("My_Type");
assertThat(myType.hasMember("Cons"), is(false));
}
}
@Test
public void typeWithPrivateConstructorExposesPublicMethodsToPolyglot() throws IOException {
var mainSrc =
"""
type My_Type
private Cons data
get_data self = self.data
main =
My_Type.Cons 42
""";
var projDir = createProject("My_Project", mainSrc);
var mainSrcPath = projDir.resolve("src").resolve("Main.enso");
try (var ctx =
defaultContextBuilder()
.option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString())
.build()) {
var polyCtx = new PolyglotContext(ctx);
var mainMod = polyCtx.evalModule(mainSrcPath.toFile());
var myType = mainMod.getType("My_Type");
var getDataMethod = mainMod.getMethod(myType, "get_data").get();
var assocType = mainMod.getAssociatedType();
var mainMethod = mainMod.getMethod(assocType, "main").get();
var res = mainMethod.execute();
assertThat("Atoms should generally have members", res.hasMembers(), is(true));
assertThat("data is a private field", res.hasMember("data"), is(false));
assertThat("get_data is a public method", res.hasMember("get_data"), is(true));
var data = getDataMethod.execute(ScalaConversions.seq(List.of(res)));
assertThat("public accessor method can be called from polyglot", data.isNumber(), is(true));
assertThat("public accessor method can be called from polyglot", data.asInt(), is(42));
}
}
@Test
public void canPatternMatchOnPrivateConstructorFromSameProject() throws IOException {
var mainSrc =
"""
type My_Type
private Cons data
main =
obj = My_Type.Cons 42
case obj of
My_Type.Cons x -> x
_ -> 0
""";
var projDir = createProject("My_Project", mainSrc);
var mainSrcPath = projDir.resolve("src").resolve("Main.enso");
try (var ctx =
defaultContextBuilder()
.option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString())
.build()) {
var polyCtx = new PolyglotContext(ctx);
var mainMod = polyCtx.evalModule(mainSrcPath.toFile());
var assocType = mainMod.getAssociatedType();
var mainMethod = mainMod.getMethod(assocType, "main").get();
var res = mainMethod.execute();
assertThat(res.isNumber(), is(true));
assertThat(res.asInt(), is(42));
}
}
/** Tests that pattern matching on private constructors fails in compilation. */
@Test
public void cannotPatternMatchOnPrivateConstructorFromDifferentProject() throws IOException {
var libSrc =
"""
type My_Type
private Cons data
create x = My_Type.Cons x
""";
createProject("Lib", libSrc);
var projSrc =
"""
from local.Lib import My_Type
main =
obj = My_Type.create 42
case obj of
My_Type.Cons x -> x
""";
var projDir = createProject("Proj", projSrc);
var out = new ByteArrayOutputStream();
try (var ctx =
defaultContextBuilder()
.option(RuntimeOptions.PROJECT_ROOT, projDir.toAbsolutePath().toString())
.option(RuntimeOptions.STRICT_ERRORS, "true")
.option(RuntimeOptions.DISABLE_IR_CACHES, "true")
.out(out)
.err(out)
.build()) {
var polyCtx = new PolyglotContext(ctx);
try {
polyCtx.getTopScope().compile(true);
fail("Expected compiler error");
} catch (PolyglotException e) {
assertThat(
out.toString(),
allOf(
containsString("error:"),
containsString("Project-private constructor"),
containsString("cannot be used from")));
}
}
}
/**
* Creates temporary project directory structure with a given main source content. No need to
* clean it up, as it is managed by JUnit TemporaryFolder rule. Note that we need to create a
* project, otherwise the private stuff won't work.
*
* @param projName Name of the project (as defined in package.yaml).
* @param mainSrc Main.enso source content
* @return Path to the newly created directly structure - a project directory.
*/
private Path createProject(String projName, String mainSrc) throws IOException {
var projDir = tempFolder.newFolder(projName);
assert projDir.exists();
var projYaml =
"""
name: %s
version: 0.0.1
prefer-local-libraries: true
""".formatted(projName);
var yamlPath = projDir.toPath().resolve("package.yaml");
Files.writeString(yamlPath, projYaml);
assert yamlPath.toFile().exists();
var srcDir = tempFolder.newFolder(projName, "src");
assert srcDir.exists();
var mainSrcPath = srcDir.toPath().resolve("Main.enso");
Files.writeString(mainSrcPath, mainSrc);
assert mainSrcPath.toFile().exists();
return projDir.toPath();
}
}

View File

@ -11,6 +11,7 @@ import org.enso.compiler.pass.analyse.{
BindingAnalysis,
ExportSymbolAnalysis,
ImportSymbolAnalysis,
PrivateConstructorAnalysis,
PrivateModuleAnalysis
}
import org.enso.compiler.pass.desugar._
@ -63,6 +64,7 @@ class PassesTest extends CompilerTest {
ImportSymbolAnalysis,
AmbiguousImportsAnalysis,
PrivateModuleAnalysis.INSTANCE,
PrivateConstructorAnalysis.INSTANCE,
ExportSymbolAnalysis.INSTANCE,
ShadowedPatternFields,
UnreachableMatchBranches,

View File

@ -78,7 +78,7 @@ class BindingAnalysisTest extends CompilerTest {
val metadata = ir.unsafeGetMetadata(BindingAnalysis, "Should exist.")
metadata.definedEntities should contain theSameElementsAs List(
Type("Foo", List(), List(Cons("Mk_Foo", 3, false)), false),
Type("Foo", List(), List(Cons("Mk_Foo", 3, false, false)), false),
Type("Bar", List(), List(), false),
Type("Baz", List("x", "y"), List(), false),
PolyglotSymbol("MyClass"),

View File

@ -0,0 +1,94 @@
package org.enso.compiler.test.pass.analyse
import org.enso.compiler.Passes
import org.enso.compiler.context.{FreshNameSupply, ModuleContext}
import org.enso.compiler.core.ir.Module
import org.enso.compiler.core.ir.expression.errors.Syntax
import org.enso.compiler.core.ir.module.scope.Definition
import org.enso.compiler.pass.analyse.PrivateConstructorAnalysis
import org.enso.compiler.pass.{PassConfiguration, PassGroup, PassManager}
import org.enso.compiler.test.CompilerTest
class PrivateModifierTest extends CompilerTest {
// === Utilities ============================================================
val passes = new Passes(defaultConfig)
/** The passes that need to be run before the alias analysis pass. */
val precursorPasses: PassGroup =
passes.getPrecursors(PrivateConstructorAnalysis.INSTANCE).get
val passConfig: PassConfiguration = PassConfiguration()
implicit val passManager: PassManager =
new PassManager(List(precursorPasses), passConfig)
/** Adds an extension method to run private constructor analysis on an [[Module]].
*
* @param ir the module to run private constructor analysis on
*/
implicit class AnalyseModule(ir: Module) {
/** Runs alias analysis on a module.
*
* @return [[ir]], with attached aliasing information
*/
def analyse: Module = {
PrivateConstructorAnalysis.INSTANCE.runModule(
ir,
buildModuleContext(passConfiguration = Some(passConfig))
)
}
}
/** Creates a defaulted module context.
*
* @return a defaulted module context
*/
def mkModuleContext: ModuleContext = {
buildModuleContext(freshNameSupply = Some(new FreshNameSupply))
}
// === The Tests ============================================================
"Private constructor analysis" should {
implicit val ctx: ModuleContext = mkModuleContext
"should accept a single private constructor" in {
val ir =
"""
|type My_Type
| private Value a b c
|""".stripMargin.preprocessModule.analyse
ir.bindings.size shouldBe 1
ir.bindings.head.isInstanceOf[Definition.Type] shouldBe true
val tp = ir.bindings.head.asInstanceOf[Definition.Type]
tp.members.size shouldBe 1
tp.members.head
}
"should reject mix of public and private constructors in a type" in {
val ir =
"""
|type My_Type
| private Priv_Cons a b
| Pub_Cons c d
|""".stripMargin.preprocessModule.analyse
val errors = ir.preorder().collect { case err: Syntax => err }
errors.size shouldBe 1
errors.head.reason shouldBe (Syntax.InconsistentConstructorVisibility)
}
"should accept more private constructors in a type" in {
val ir =
"""
|type My_Type
| Pub_Cons_1 a b
| Pub_Cons_2 c d
|""".stripMargin.preprocessModule.analyse
val errors = ir.preorder().collect { case err: Syntax => err }
errors.isEmpty shouldBe true
}
}
}

View File

@ -5,7 +5,6 @@ import com.oracle.truffle.api.TruffleContext;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.TruffleLogger;
import com.oracle.truffle.api.nodes.Node;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
/**
@ -21,7 +20,6 @@ final class EpbContext {
private final boolean isInner;
private final TruffleLanguage.Env env;
private @CompilationFinal TruffleContext innerContext;
private final ReentrantLock lock = new ReentrantLock();
private final TruffleLogger log;
/**

View File

@ -175,7 +175,7 @@ final class TreeToIr {
}
if (expr instanceof Private priv) {
if (priv.getBody() != null) {
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("Private token with body"));
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("The `private` keyword is currently not supported for entities other than constructors."));
diag = join(error, diag);
}
if (isPrivate) {
@ -338,12 +338,12 @@ final class TreeToIr {
return res.reverse();
}
IR translateConstructorDefinition(Tree.ConstructorDefinition cons, Tree inputAst) {
IR translateConstructorDefinition(Tree.ConstructorDefinition cons, Tree inputAst, boolean isPrivate) {
try {
var constructorName = buildName(inputAst, cons.getConstructor());
List<DefinitionArgument> args = translateArgumentsDefinition(cons.getArguments());
var cAt = getIdentifiedLocation(inputAst);
return new Definition.Data(constructorName, args, nil(), cAt, meta(), diag());
return new Definition.Data(constructorName, args, nil(), cAt, isPrivate, meta(), diag());
} catch (SyntaxException ex) {
return ex.toError();
}
@ -369,7 +369,17 @@ final class TreeToIr {
return switch (inputAst) {
case null -> appendTo;
case Tree.ConstructorDefinition cons -> join(translateConstructorDefinition(cons, inputAst), appendTo);
case Tree.Private priv -> {
if (priv.getBody() instanceof Tree.ConstructorDefinition consDef) {
var translated = translateConstructorDefinition(consDef, priv, true);
yield join(translated, appendTo);
} else {
var errorIr = translateSyntaxError(priv, Syntax.UnexpectedDeclarationInType$.MODULE$);
yield join(errorIr, appendTo);
}
}
case Tree.ConstructorDefinition cons -> join(translateConstructorDefinition(cons, inputAst, false), appendTo);
case Tree.TypeDef def -> {
var ir = translateSyntaxError(def, Syntax.UnexpectedDeclarationInType$.MODULE$);

View File

@ -113,4 +113,18 @@ object Pattern {
s"Wrong number of fields when matching on $consName." +
s" Expected $expected fields, but provided $actual"
}
/** An error when a project-private constructor is used in the pattern.
* @param consName Name of the constructor. Does not have to be fully qualified.
* @param callerProject The project name of the caller.
* @param calleeProject The project name of the callee. The constructor is in this project.
*/
case class PrivateConstructor(
consName: String,
callerProject: String,
calleeProject: String
) extends Reason {
override def explain: String =
s"Project-private constructor '$consName' in project '$calleeProject' cannot be used from project '$callerProject'"
}
}

View File

@ -191,6 +191,18 @@ object Resolution {
s"Variable `${originalName.name}` is not defined"
}
/** An error when a project-private entity (module, type, method) is used from a different project.
* @param callerProject Name of the project of caller.
* @param calleeProject Name of the project of callee.
*/
case class PrivateEntity(
callerProject: String,
calleeProject: String
) extends Reason {
override def explain(originalName: Name): String =
s"Project-private entity '${originalName.name}' in project '$calleeProject' cannot be used from project '$callerProject'"
}
/** An error coming from name resolver.
*
* @param err the original error.

View File

@ -147,6 +147,11 @@ object Syntax {
s"Syntax is not supported yet: $syntaxName"
}
case object InconsistentConstructorVisibility extends Reason {
override def explanation: String =
"Private and public constructors cannot be mixed within a single type"
}
case object InvalidUnderscore extends Reason {
override def explanation: String =
s"Invalid use of _"

View File

@ -155,6 +155,7 @@ object Definition {
* @param arguments the arguments to the atom constructor
* @param annotations the list of annotations
* @param location the source location that the node corresponds to
* @param isPrivate If the constructor is private (project-private).
* @param passData the pass metadata associated with this node
* @param diagnostics compiler diagnostics for this node
*/
@ -163,6 +164,7 @@ object Definition {
arguments: List[DefinitionArgument],
annotations: List[Name.GenericAnnotation],
location: Option[IdentifiedLocation],
isPrivate: Boolean = false,
passData: MetadataStorage = new MetadataStorage(),
diagnostics: DiagnosticStorage = DiagnosticStorage()
) extends IR
@ -194,6 +196,7 @@ object Definition {
arguments,
annotations,
location,
isPrivate,
passData,
diagnostics
)

View File

@ -1400,6 +1400,24 @@ public class EnsoParserTest {
equivalenceTest("private", "\n\nprivate");
}
@Test
public void testPrivateConstructor() throws Exception {
parseTest(
"""
type My_Type
private Ctor_1 a b
private Ctor_2 c d
""");
// Mixing public and private constructor is a semantic error, not a syntax error.
// So parsing should be fine.
parseTest(
"""
type My_Type
private Ctor_1 a b
Ctor_2 c d
""");
}
@Test
public void ise_184219679() throws IOException {
parseTest(

View File

@ -94,6 +94,7 @@ final class SuggestionBuilder[A: IndexedSource](
annotations,
_,
_,
_,
_
) =>
buildAtomConstructor(

View File

@ -29,9 +29,9 @@ public abstract class EnsoRootNode extends RootNode {
*
* @param language the language instance in which this will execute
* @param localScope a reference to the construct local scope
* @param moduleScope a reference to the construct module scope
* @param moduleScope a reference to the construct module scope. May be {@code null}.
* @param name the name of the construct
* @param sourceSection a reference to the source code being executed
* @param sourceSection a reference to the source code being executed. May be {@code null}.
*/
protected EnsoRootNode(
EnsoLanguage language,
@ -41,6 +41,7 @@ public abstract class EnsoRootNode extends RootNode {
SourceSection sourceSection) {
super(language, buildFrameDescriptor(localScope));
Objects.requireNonNull(language);
Objects.requireNonNull(localScope);
this.name = name;
this.localScope = localScope;
this.moduleScope = moduleScope;

View File

@ -30,13 +30,11 @@ import org.enso.interpreter.node.expression.builtin.meta.AtomWithAHoleNode;
import org.enso.interpreter.node.expression.builtin.meta.IsValueOfTypeNode;
import org.enso.interpreter.node.expression.literal.LiteralNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.Annotation;
import org.enso.interpreter.runtime.callable.UnresolvedConstructor;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition.ExecutionMode;
import org.enso.interpreter.runtime.callable.argument.CallArgument;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
import org.enso.interpreter.runtime.data.EnsoMultiValue;
@ -479,14 +477,11 @@ public abstract class ReadArgumentCheckNode extends Node {
@Child private ReadArgumentCheckNode check;
static final FunctionSchema SCHEMA =
new FunctionSchema(
FunctionSchema.CallerFrameAccess.NONE,
new ArgumentDefinition[] {
new ArgumentDefinition(0, "delegate", null, null, ExecutionMode.EXECUTE)
},
new boolean[] {true},
new CallArgumentInfo[0],
new Annotation[0]);
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(0, "delegate", null, null, ExecutionMode.EXECUTE))
.hasPreapplied(true)
.build();
LazyCheckRootNode(TruffleLanguage<?> language, ReadArgumentCheckNode check) {
super(language);

View File

@ -1,5 +1,7 @@
package org.enso.interpreter.node.callable.dispatch;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.ImportStatic;
import com.oracle.truffle.api.dsl.NeverDefault;
@ -12,6 +14,8 @@ import com.oracle.truffle.api.source.SourceSection;
import java.util.UUID;
import org.enso.interpreter.Constants;
import org.enso.interpreter.node.BaseNode;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.node.MethodRootNode;
import org.enso.interpreter.node.callable.CaptureCallerInfoNode;
import org.enso.interpreter.node.callable.FunctionCallInstrumentationNode;
import org.enso.interpreter.node.callable.InvokeCallableNode;
@ -22,7 +26,11 @@ import org.enso.interpreter.runtime.callable.CallerInfo;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
import org.enso.interpreter.runtime.data.atom.AtomConstructor;
import org.enso.interpreter.runtime.error.PanicException;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.interpreter.runtime.state.State;
import org.enso.pkg.Package;
/**
* This class represents the protocol for remapping the arguments provided at a call site into the
@ -86,6 +94,25 @@ public abstract class InvokeFunctionNode extends BaseNode {
return EnsoContext.get(this);
}
@TruffleBoundary
private PanicException makePrivateAccessPanic(Function targetFunction) {
String thisProjName = null;
if (getThisProject() != null) {
thisProjName = getThisProject().libraryName().qualifiedName();
}
String targetProjName = null;
if (getFunctionProject(targetFunction) != null) {
targetProjName = getFunctionProject(targetFunction).libraryName().qualifiedName();
}
var funcName = targetFunction.getName();
var err =
EnsoContext.get(this)
.getBuiltins()
.error()
.makePrivateAccessError(thisProjName, targetProjName, funcName);
return new PanicException(err, this);
}
@Specialization(
guards = {"!getContext().isInlineCachingDisabled()", "function.getSchema() == cachedSchema"},
limit = Constants.CacheSizes.ARGUMENT_SORTER_NODE)
@ -103,6 +130,10 @@ public abstract class InvokeFunctionNode extends BaseNode {
"build(argumentMapping, getDefaultsExecutionMode(), getArgumentsExecutionMode(),"
+ " getTailStatus())")
CurryNode curryNode) {
if (cachedSchema.isProjectPrivate() && !isInSameProject(function)) {
throw makePrivateAccessPanic(function);
}
ArgumentSorterNode.MappedArguments mappedArguments =
mappingNode.execute(callerFrame, function, state, arguments);
CallerInfo callerInfo = null;
@ -143,6 +174,10 @@ public abstract class InvokeFunctionNode extends BaseNode {
Object[] arguments,
@Cached IndirectArgumentSorterNode mappingNode,
@Cached IndirectCurryNode curryNode) {
if (function.getSchema().isProjectPrivate() && !isInSameProject(function)) {
throw makePrivateAccessPanic(function);
}
CallArgumentInfo.ArgumentMapping argumentMapping =
CallArgumentInfo.ArgumentMappingBuilder.generate(function.getSchema(), getSchema());
@ -224,4 +259,44 @@ public abstract class InvokeFunctionNode extends BaseNode {
public UUID getId() {
return functionCallInstrumentationNode.getId();
}
/** Returns true if the given function is in the same project as this node. */
private boolean isInSameProject(Function function) {
var thisProj = getThisProject();
var funcProj = getFunctionProject(function);
return thisProj == funcProj;
}
private Package<TruffleFile> getThisProject() {
if (getRootNode() instanceof EnsoRootNode thisRootNode) {
var modScope = thisRootNode.getModuleScope();
if (modScope != null) {
return modScope.getModule().getPackage();
}
}
return null;
}
private Package<TruffleFile> getFunctionProject(Function function) {
var modScope = getModuleScopeForFunction(function);
if (modScope != null) {
return modScope.getModule().getPackage();
}
return null;
}
private ModuleScope getModuleScopeForFunction(Function function) {
var cons = AtomConstructor.accessorFor(function);
if (cons != null) {
return cons.getDefinitionScope();
}
cons = MethodRootNode.constructorFor(function);
if (cons != null) {
return cons.getDefinitionScope();
}
if (function.getCallTarget().getRootNode() instanceof EnsoRootNode ensoRootNode) {
return ensoRootNode.getModuleScope();
}
return null;
}
}

View File

@ -25,7 +25,7 @@ public class CreateFunctionNode extends ExpressionNode {
private CreateFunctionNode(RootCallTarget callTarget, ArgumentDefinition[] args) {
this.callTarget = callTarget;
this.schema = new FunctionSchema(args);
this.schema = FunctionSchema.newBuilder().argumentDefinitions(args).build();
}
/**

View File

@ -60,8 +60,8 @@ public abstract class Builtin {
}
type =
containsValues()
? Type.create(name, scope, supertype, builtins.get(Any.class).getType(), true)
: Type.createSingleton(name, scope, supertype, true);
? Type.create(name, scope, supertype, builtins.get(Any.class).getType(), true, false)
: Type.createSingleton(name, scope, supertype, true, false);
}
if (constructors == null) {
var conses = getDeclaredConstructors();

View File

@ -0,0 +1,19 @@
package org.enso.interpreter.node.expression.builtin.error;
import java.util.List;
import org.enso.interpreter.dsl.BuiltinType;
import org.enso.interpreter.node.expression.builtin.UniquelyConstructibleBuiltin;
@BuiltinType
public class PrivateAccess extends UniquelyConstructibleBuiltin {
@Override
protected String getConstructorName() {
return "Error";
}
@Override
protected List<String> getConstructorParamNames() {
return List.of("this_project_name", "target_project_name", "target_method_name");
}
}

View File

@ -15,7 +15,6 @@ import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.callable.InvokeCallableNode;
import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.Annotation;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
@ -154,17 +153,14 @@ public abstract class AtomWithAHoleNode extends Node {
private SwapAtomFieldNode() {
super(null);
this.schema =
new FunctionSchema(
FunctionSchema.CallerFrameAccess.NONE,
new ArgumentDefinition[] {
new ArgumentDefinition(
0, "lazy", null, null, ArgumentDefinition.ExecutionMode.EXECUTE),
new ArgumentDefinition(
1, "value", null, null, ArgumentDefinition.ExecutionMode.EXECUTE)
},
new boolean[] {true, false},
new CallArgumentInfo[0],
new Annotation[0]);
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0, "lazy", null, null, ArgumentDefinition.ExecutionMode.EXECUTE),
new ArgumentDefinition(
1, "value", null, null, ArgumentDefinition.ExecutionMode.EXECUTE))
.hasPreapplied(true, false)
.build();
}
@NeverDefault

View File

@ -9,24 +9,21 @@ import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.Annotation;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
import org.enso.polyglot.debugger.IdExecutionService;
final class InstrumentorEvalNode extends RootNode {
private static final FunctionSchema SUSPENDED_EVAL =
new FunctionSchema(
FunctionSchema.CallerFrameAccess.NONE,
new ArgumentDefinition[] {
new ArgumentDefinition(0, "expr", null, null, ArgumentDefinition.ExecutionMode.EXECUTE),
new ArgumentDefinition(1, "info", null, null, ArgumentDefinition.ExecutionMode.EXECUTE)
},
new boolean[] {true, true},
new CallArgumentInfo[0],
new Annotation[0]);
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0, "expr", null, null, ArgumentDefinition.ExecutionMode.EXECUTE),
new ArgumentDefinition(
1, "info", null, null, ArgumentDefinition.ExecutionMode.EXECUTE))
.hasPreapplied(true, true)
.build();
private static Reference<InstrumentorEvalNode> last = new WeakReference<>(null);
private InstrumentorEvalNode(EnsoLanguage language) {

View File

@ -72,10 +72,10 @@ public class LiteralNode extends ExpressionNode implements Patchable {
}
/**
* Returns the constant value of this string literal.
* Returns the constant value of this literal.
*
* @param frame the stack frame for execution
* @return the string value this node was created with
* @return the value this node was created with
*/
@Override
public Object executeGeneric(VirtualFrame frame) {

View File

@ -24,6 +24,7 @@ import org.enso.interpreter.node.expression.builtin.error.NoSuchMethod;
import org.enso.interpreter.node.expression.builtin.error.NotInvokable;
import org.enso.interpreter.node.expression.builtin.error.NumberParseError;
import org.enso.interpreter.node.expression.builtin.error.Panic;
import org.enso.interpreter.node.expression.builtin.error.PrivateAccess;
import org.enso.interpreter.node.expression.builtin.error.SyntaxError;
import org.enso.interpreter.node.expression.builtin.error.TypeError;
import org.enso.interpreter.node.expression.builtin.error.Unimplemented;
@ -32,6 +33,7 @@ import org.enso.interpreter.node.expression.builtin.error.UnsupportedArgumentTyp
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.data.EnsoObject;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.atom.Atom;
import org.enso.interpreter.runtime.data.text.Text;
@ -58,6 +60,7 @@ public final class Error {
private final UnsupportedArgumentTypes unsupportedArgumentsError;
private final ModuleDoesNotExist moduleDoesNotExistError;
private final NotInvokable notInvokable;
private final PrivateAccess privateAccessError;
private final InvalidConversionTarget invalidConversionTarget;
private final NoSuchField noSuchField;
private final NumberParseError numberParseError;
@ -96,6 +99,7 @@ public final class Error {
unsupportedArgumentsError = builtins.getBuiltinType(UnsupportedArgumentTypes.class);
moduleDoesNotExistError = builtins.getBuiltinType(ModuleDoesNotExist.class);
notInvokable = builtins.getBuiltinType(NotInvokable.class);
privateAccessError = builtins.getBuiltinType(PrivateAccess.class);
invalidConversionTarget = builtins.getBuiltinType(InvalidConversionTarget.class);
noSuchField = builtins.getBuiltinType(NoSuchField.class);
numberParseError = builtins.getBuiltinType(NumberParseError.class);
@ -310,6 +314,22 @@ public final class Error {
return notInvokable.newInstance(target);
}
/**
* @param thisProjectName Current project name. May be null.
* @param targetProjectName Target method project name. May be null.
* @param targetMethodName Name of the method that is project-private and cannot be accessed.
*/
public Atom makePrivateAccessError(
String thisProjectName, String targetProjectName, String targetMethodName) {
assert targetMethodName != null;
EnsoObject thisProjName =
thisProjectName != null ? Text.create(thisProjectName) : context.getNothing();
EnsoObject targetProjName =
targetProjectName != null ? Text.create(targetProjectName) : context.getNothing();
return privateAccessError.newInstance(
thisProjName, targetProjName, Text.create(targetMethodName));
}
public ForbiddenOperation getForbiddenOperation() {
return forbiddenOperation;
}
@ -324,7 +344,7 @@ public final class Error {
/**
* @param index the position at which the original error occured
* @param inner_error the original error
* @param innerError the original error
* @return an error indicating the index of the error
*/
public Atom makeMapError(long index, Object innerError) {

View File

@ -218,12 +218,13 @@ public final class CallArgumentInfo {
this.existingOversaturatedArgs.length,
newOversaturatedArgInfo.length);
return new FunctionSchema(
originalSchema.getCallerFrameAccess(),
definitions,
argumentUsed,
oversaturatedArgInfo,
originalSchema.getAnnotations());
return FunctionSchema.newBuilder()
.callerFrameAccess(originalSchema.getCallerFrameAccess())
.argumentDefinitions(definitions)
.hasPreapplied(argumentUsed)
.oversaturatedArguments(oversaturatedArgInfo)
.annotations(originalSchema.getAnnotations())
.build();
}
}

View File

@ -23,9 +23,9 @@ import org.enso.interpreter.node.callable.InteropApplicationNode;
import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode;
import org.enso.interpreter.node.expression.builtin.BuiltinRootNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.Annotation;
import org.enso.interpreter.runtime.callable.CallerInfo;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
import org.enso.interpreter.runtime.callable.function.FunctionSchema.CallerFrameAccess;
import org.enso.interpreter.runtime.data.EnsoObject;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.vector.ArrayLikeHelpers;
@ -93,7 +93,7 @@ public final class Function implements EnsoObject {
*/
public static Function fromBuiltinRootNode(BuiltinRootNode node, ArgumentDefinition... args) {
RootCallTarget callTarget = node.getCallTarget();
FunctionSchema schema = new FunctionSchema(args);
FunctionSchema schema = FunctionSchema.newBuilder().argumentDefinitions(args).build();
return new Function(callTarget, null, schema);
}
@ -111,7 +111,10 @@ public final class Function implements EnsoObject {
BuiltinRootNode node, ArgumentDefinition... args) {
RootCallTarget callTarget = node.getCallTarget();
FunctionSchema schema =
new FunctionSchema(FunctionSchema.CallerFrameAccess.FULL, new Annotation[0], args);
FunctionSchema.newBuilder()
.argumentDefinitions(args)
.callerFrameAccess(CallerFrameAccess.FULL)
.build();
return new Function(callTarget, null, schema);
}

View File

@ -31,7 +31,8 @@ public final class FunctionSchema {
}
}
public static final FunctionSchema THUNK = new FunctionSchema();
public static final FunctionSchema THUNK =
FunctionSchema.newBuilder().callerFrameAccess(CallerFrameAccess.NONE).build();
private final @CompilationFinal(dimensions = 1) ArgumentDefinition[] argumentInfos;
private final @CompilationFinal(dimensions = 1) boolean[] hasPreApplied;
@ -41,6 +42,11 @@ public final class FunctionSchema {
private final boolean hasOversaturatedArguments;
private final CallerFrameAccess callerFrameAccess;
private final boolean isFullyApplied;
private final boolean isProjectPrivate;
public static Builder newBuilder() {
return new Builder();
}
/**
* Creates an {@link FunctionSchema} instance.
@ -54,17 +60,19 @@ public final class FunctionSchema {
* this function so far.
* @param annotations the list of annotations defined on this function.
*/
public FunctionSchema(
private FunctionSchema(
CallerFrameAccess callerFrameAccess,
ArgumentDefinition[] argumentInfos,
boolean[] hasPreApplied,
CallArgumentInfo[] oversaturatedArguments,
Annotation[] annotations) {
Annotation[] annotations,
boolean isProjectPrivate) {
this.argumentInfos = argumentInfos;
this.oversaturatedArguments = oversaturatedArguments;
this.hasPreApplied = hasPreApplied;
this.callerFrameAccess = callerFrameAccess;
this.annotations = annotations;
this.isProjectPrivate = isProjectPrivate;
boolean hasAnyPreApplied = false;
for (boolean b : hasPreApplied) {
if (b) {
@ -78,51 +86,6 @@ public final class FunctionSchema {
this.isFullyApplied = isFullyApplied(InvokeCallableNode.DefaultsExecutionMode.EXECUTE);
}
/**
* Creates an {@link FunctionSchema} instance assuming the function has no partially applied
* arguments.
*
* @param callerFrameAccess the declaration of need to access the caller frame from the function.
* @param argumentInfos Definition site arguments information.
* @param annotations the list of annotations defined on this function.
*/
public FunctionSchema(
CallerFrameAccess callerFrameAccess,
Annotation[] annotations,
ArgumentDefinition... argumentInfos) {
this(
callerFrameAccess,
argumentInfos,
new boolean[argumentInfos.length],
new CallArgumentInfo[0],
annotations);
}
/**
* Creates an {@link FunctionSchema} instance assuming the function has no partially applied
* arguments.
*
* <p>Caller frame access is assumed to be {@link CallerFrameAccess#NONE}.
*
* @param annotations the list of annotations defined on this function.
* @param argumentInfos Definition site arguments information.
*/
public FunctionSchema(Annotation[] annotations, ArgumentDefinition... argumentInfos) {
this(CallerFrameAccess.NONE, annotations, argumentInfos);
}
/**
* Creates an {@link FunctionSchema} instance assuming the function has no annotations or
* partially applied arguments.
*
* <p>Caller frame access is assumed to be {@link CallerFrameAccess#NONE}.
*
* @param argumentInfos Definition site arguments information
*/
public FunctionSchema(ArgumentDefinition... argumentInfos) {
this(CallerFrameAccess.NONE, new Annotation[0], argumentInfos);
}
/**
* Does this function have a partially applied argument at a given position?
*
@ -154,6 +117,10 @@ public final class FunctionSchema {
return this.hasOversaturatedArguments;
}
public boolean isProjectPrivate() {
return isProjectPrivate;
}
/**
* Return the definition site arguments information.
*
@ -264,4 +231,77 @@ public final class FunctionSchema {
public boolean isFullyApplied() {
return isFullyApplied;
}
public static final class Builder {
private boolean isProjectPrivate = false;
private Annotation[] annotations = new Annotation[0];
private ArgumentDefinition[] argDefs = new ArgumentDefinition[0];
private CallArgumentInfo[] callArgInfos = new CallArgumentInfo[0];
private CallerFrameAccess callerFrameAccess = CallerFrameAccess.NONE;
private boolean[] hasPreapplied = new boolean[0];
private Builder() {}
/** Set the function to be project private. */
public Builder projectPrivate() {
this.isProjectPrivate = true;
return this;
}
/**
* @param annotations the list of annotations defined on this function.
*/
public Builder annotations(Annotation... annotations) {
this.annotations = annotations;
return this;
}
/**
* @param argDefs Definition site arguments information.
*/
public Builder argumentDefinitions(ArgumentDefinition... argDefs) {
if (hasPreapplied.length > argDefs.length) {
throw new AssertionError("hasPreapplied must not be specified before argument definitions");
}
this.argDefs = argDefs;
this.hasPreapplied = new boolean[argDefs.length];
return this;
}
/**
* @param callArgumentInfos information about any unused, oversaturated arguments passed to this
* function so far.
*/
public Builder oversaturatedArguments(CallArgumentInfo... callArgumentInfos) {
this.callArgInfos = callArgumentInfos;
return this;
}
/**
* @param callerFrameAccess the declaration of whether access to caller frame is required for
* this function.
*/
public Builder callerFrameAccess(CallerFrameAccess callerFrameAccess) {
this.callerFrameAccess = callerFrameAccess;
return this;
}
/**
* @param hasPreapplied A flags collection such that {@code hasPreApplied[i]} is true iff a
* function has a partially applied argument at position {@code i}.
*/
public Builder hasPreapplied(boolean... hasPreapplied) {
if (hasPreapplied.length != argDefs.length) {
throw new AssertionError("Argument definitions must be specified before hasPreapplied");
}
this.hasPreapplied = hasPreapplied;
return this;
}
public FunctionSchema build() {
assert argDefs.length == hasPreapplied.length;
return new FunctionSchema(
callerFrameAccess, argDefs, hasPreapplied, callArgInfos, annotations, isProjectPrivate);
}
}
}

View File

@ -34,47 +34,65 @@ public final class Type implements EnsoObject {
private final Type supertype;
private final Type eigentype;
private final Map<String, AtomConstructor> constructors;
private final boolean isProjectPrivate;
private boolean gettersGenerated;
private Type(
String name, ModuleScope definitionScope, Type supertype, Type eigentype, boolean builtin) {
String name,
ModuleScope definitionScope,
Type supertype,
Type eigentype,
boolean builtin,
boolean isProjectPrivate) {
this.name = name;
this.definitionScope = definitionScope;
this.supertype = supertype;
this.builtin = builtin;
this.isProjectPrivate = isProjectPrivate;
this.eigentype = Objects.requireNonNullElse(eigentype, this);
this.constructors = new HashMap<>();
}
public static Type createSingleton(
String name, ModuleScope definitionScope, Type supertype, boolean builtin) {
var result = new Type(name, definitionScope, supertype, null, builtin);
String name,
ModuleScope definitionScope,
Type supertype,
boolean builtin,
boolean isProjectPrivate) {
var result = new Type(name, definitionScope, supertype, null, builtin, isProjectPrivate);
result.generateQualifiedAccessor();
return result;
}
public static Type create(
String name, ModuleScope definitionScope, Type supertype, Type any, boolean builtin) {
var eigentype = new Type(name + ".type", definitionScope, any, null, builtin);
var result = new Type(name, definitionScope, supertype, eigentype, builtin);
String name,
ModuleScope 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();
return result;
}
public static Type noType() {
return new Type("null", null, null, null, false);
return new Type("null", null, null, null, false, false);
}
private void generateQualifiedAccessor() {
var node = new ConstantNode(null, this);
var function =
new Function(
node.getCallTarget(),
null,
new FunctionSchema(
var schemaBldr =
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0, "this", null, null, ArgumentDefinition.ExecutionMode.EXECUTE)));
0, "this", null, null, ArgumentDefinition.ExecutionMode.EXECUTE));
if (isProjectPrivate) {
schemaBldr.projectPrivate();
}
var function = new Function(node.getCallTarget(), null, schemaBldr.build());
definitionScope.registerMethod(definitionScope.getAssociatedType(), this.name, function);
}
@ -118,6 +136,18 @@ public final class Type implements EnsoObject {
return builtin;
}
/**
* 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
* 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.
*/
public boolean isProjectPrivate() {
return isProjectPrivate;
}
private Type getSupertype() {
if (supertype == null) {
if (builtin) {
@ -152,17 +182,20 @@ public final class Type implements EnsoObject {
var roots = AtomConstructor.collectFieldAccessors(language, this);
roots.forEach(
(name, node) -> {
var f =
new Function(
node.getCallTarget(),
null,
new FunctionSchema(
var schemaBldr =
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0,
Constants.Names.SELF_ARGUMENT,
null,
null,
ArgumentDefinition.ExecutionMode.EXECUTE)));
ArgumentDefinition.ExecutionMode.EXECUTE));
if (isProjectPrivate) {
schemaBldr.projectPrivate();
}
var funcSchema = schemaBldr.build();
var f = new Function(node.getCallTarget(), null, funcSchema);
definitionScope.registerMethod(this, name, f);
});
}
@ -270,18 +303,29 @@ public final class Type implements EnsoObject {
@ExportMessage
@CompilerDirectives.TruffleBoundary
EnsoObject getMembers(boolean includeInternal) {
return ArrayLikeHelpers.wrapStrings(constructors.keySet().toArray(String[]::new));
if (isProjectPrivate) {
return ArrayLikeHelpers.empty();
} else {
return ArrayLikeHelpers.wrapStrings(constructors.keySet().toArray(String[]::new));
}
}
@ExportMessage
@CompilerDirectives.TruffleBoundary
boolean isMemberReadable(String member) {
return constructors.containsKey(member);
if (isProjectPrivate) {
return false;
} else {
return constructors.containsKey(member);
}
}
@ExportMessage
@CompilerDirectives.TruffleBoundary
Object readMember(String member) throws UnknownIdentifierException {
if (isProjectPrivate) {
throw UnknownIdentifierException.create(member);
}
var result = constructors.get(member);
if (result == null) {
throw UnknownIdentifierException.create(member);
@ -308,6 +352,11 @@ public final class Type implements EnsoObject {
return eigentype == this;
}
/**
* Registers a constructor in this type.
*
* @param constructor The constructor to register in this type.
*/
public void registerConstructor(AtomConstructor constructor) {
constructors.put(constructor.getName(), constructor);
gettersGenerated = false;

View File

@ -1,7 +1,9 @@
package org.enso.interpreter.runtime.data.atom;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Idempotent;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.interop.ArityException;
@ -134,40 +136,64 @@ public abstract class Atom implements EnsoObject {
return true;
}
/**
* Returns a polyglot list of all the public members of Atom. A member is any method on the atom.
* A member is public if it is not project-private.
*/
@ExportMessage
@CompilerDirectives.TruffleBoundary
EnsoObject getMembers(boolean includeInternal) {
Set<String> members =
constructor.getDefinitionScope().getMethodNamesForType(constructor.getType());
Set<String> allMembers = new HashSet<>();
EnsoObject getMembers(boolean includeInternal) throws UnsupportedMessageException {
Set<Function> members =
constructor.getDefinitionScope().getMethodsForType(constructor.getType());
Set<Function> allFuncMembers = new HashSet<>();
if (members != null) {
allMembers.addAll(members);
allFuncMembers.addAll(members);
}
members =
constructor.getType().getDefinitionScope().getMethodNamesForType(constructor.getType());
members = constructor.getType().getDefinitionScope().getMethodsForType(constructor.getType());
if (members != null) {
allMembers.addAll(members);
allFuncMembers.addAll(members);
}
String[] mems = allMembers.toArray(new String[0]);
return ArrayLikeHelpers.wrapStrings(mems);
String[] publicMembers =
allFuncMembers.stream()
.filter(method -> !method.getSchema().isProjectPrivate())
.map(Function::getName)
.distinct()
.toArray(String[]::new);
return ArrayLikeHelpers.wrapStrings(publicMembers);
}
protected boolean isMethodProjectPrivate(Type type, String methodName) {
Function method = constructor.getDefinitionScope().getMethodForType(type, methodName);
if (method != null) {
return method.getSchema().isProjectPrivate();
}
method = constructor.getType().getDefinitionScope().getMethodForType(type, methodName);
return method != null && method.getSchema().isProjectPrivate();
}
/** A member is invocable if it is a method on the Atom and it is public. */
@ExportMessage
@CompilerDirectives.TruffleBoundary
final boolean isMemberInvocable(String member) {
Set<String> members =
constructor.getDefinitionScope().getMethodNamesForType(constructor.getType());
var type = constructor.getType();
Set<String> members = constructor.getDefinitionScope().getMethodNamesForType(type);
if (members != null && members.contains(member)) {
return true;
return !isMethodProjectPrivate(type, member);
}
members =
constructor.getType().getDefinitionScope().getMethodNamesForType(constructor.getType());
return members != null && members.contains(member);
members = type.getDefinitionScope().getMethodNamesForType(type);
if (members != null && members.contains(member)) {
return !isMethodProjectPrivate(type, member);
}
return false;
}
/** A member is readable if it is an atom constructor and if the atom constructor is public. */
@ExportMessage
@ExplodeLoop
final boolean isMemberReadable(String member) {
if (hasProjectPrivateConstructor()) {
return false;
}
for (int i = 0; i < constructor.getArity(); i++) {
if (member.equals(constructor.getFields()[i].getName())) {
return true;
@ -176,10 +202,22 @@ public abstract class Atom implements EnsoObject {
return false;
}
/**
* Reads a field of the atom.
*
* @param member An identifier of a field.
* @return Value of the member.
* @throws UnknownIdentifierException If an unknown field is requested.
* @throws UnsupportedMessageException If the atom constructor is project-private, and thus all
* the fields are project-private.
*/
@ExportMessage
@ExplodeLoop
final Object readMember(String member, @CachedLibrary(limit = "3") StructsLibrary structs)
throws UnknownIdentifierException {
throws UnknownIdentifierException, UnsupportedMessageException {
if (hasProjectPrivateConstructor()) {
throw UnsupportedMessageException.create();
}
for (int i = 0; i < constructor.getArity(); i++) {
if (member.equals(constructor.getFields()[i].getName())) {
return structs.getField(this, i);
@ -188,6 +226,7 @@ public abstract class Atom implements EnsoObject {
throw UnknownIdentifierException.create(member);
}
/** Only public (non project-private) methods can be invoked. */
@ExportMessage
static class InvokeMember {
@ -196,7 +235,11 @@ public abstract class Atom implements EnsoObject {
}
@Specialization(
guards = {"receiver.getConstructor() == cachedConstructor", "member.equals(cachedMember)"},
guards = {
"receiver.getConstructor() == cachedConstructor",
"member.equals(cachedMember)",
"!isProjectPrivate(cachedConstructor, cachedMember)"
},
limit = "3")
static Object doCached(
Atom receiver,
@ -210,6 +253,7 @@ public abstract class Atom implements EnsoObject {
ArityException,
UnsupportedTypeException,
UnknownIdentifierException {
assert !isProjectPrivate(cachedConstructor, cachedMember);
Object[] args = new Object[arguments.length + 1];
args[0] = receiver;
System.arraycopy(arguments, 0, args, 1, arguments.length);
@ -234,10 +278,24 @@ public abstract class Atom implements EnsoObject {
ArityException,
UnsupportedTypeException,
UnknownIdentifierException {
if (isProjectPrivate(receiver.getConstructor(), member)) {
throw UnsupportedMessageException.create();
}
UnresolvedSymbol symbol = buildSym(receiver.getConstructor(), member);
return doCached(
receiver, member, arguments, receiver.getConstructor(), member, symbol, symbols);
}
@Idempotent
@TruffleBoundary
protected static boolean isProjectPrivate(AtomConstructor cons, String member) {
Function method = cons.getDefinitionScope().getMethodForType(cons.getType(), member);
if (method != null) {
return method.getSchema().isProjectPrivate();
}
method = cons.getType().getDefinitionScope().getMethodForType(cons.getType(), member);
return method != null && method.getSchema().isProjectPrivate();
}
}
@ExportMessage
@ -305,4 +363,8 @@ public abstract class Atom implements EnsoObject {
boolean hasMetaObject() {
return true;
}
private boolean hasProjectPrivateConstructor() {
return constructor.getType().isProjectPrivate();
}
}

View File

@ -86,7 +86,7 @@ public final class AtomConstructor implements EnsoObject {
/**
* Is the constructor initialized or not.
*
* @return {@code true} if {@link initializeFields} method has already been called
* @return {@code true} if {@link #initializeFields} method has already been called
*/
public boolean isInitialized() {
return constructorFunction != null;
@ -172,19 +172,25 @@ public final class AtomConstructor implements EnsoObject {
MethodRootNode.buildConstructor(
language, localScope, definitionScope, instantiateBlock, section, this);
RootCallTarget callTarget = rootNode.getCallTarget();
return new Function(callTarget, null, new FunctionSchema(annotations, args));
var schemaBldr = FunctionSchema.newBuilder().annotations(annotations).argumentDefinitions(args);
if (type.isProjectPrivate()) {
schemaBldr.projectPrivate();
}
return new Function(callTarget, null, schemaBldr.build());
}
private Function generateQualifiedAccessor(EnsoLanguage lang) {
var node = new QualifiedAccessorNode(lang, this);
var node = new QualifiedAccessorNode(lang, this, getDefinitionScope());
var callTarget = node.getCallTarget();
var function =
new Function(
callTarget,
null,
new FunctionSchema(
var schemaBldr =
FunctionSchema.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0, "self", null, null, ArgumentDefinition.ExecutionMode.EXECUTE)));
0, "self", null, null, ArgumentDefinition.ExecutionMode.EXECUTE));
if (type.isProjectPrivate()) {
schemaBldr.projectPrivate();
}
var function = new Function(callTarget, null, schemaBldr.build());
definitionScope.registerMethod(type.getEigentype(), this.name, function);
return function;
}
@ -287,7 +293,7 @@ public final class AtomConstructor implements EnsoObject {
}
/**
* Creates field accessors for all fields in given constructors.
* Creates field accessors for all fields in all constructors from the given type.
*
* @param language the language instance to create getters for
* @param type type to create accessors for
@ -319,7 +325,9 @@ public final class AtomConstructor implements EnsoObject {
} else {
var cons = constructors.toArray(AtomConstructor[]::new)[0];
for (var field : cons.getFields()) {
var node = new GetFieldNode(language, field.getPosition(), type, field.getName());
var node =
new GetFieldNode(
language, field.getPosition(), type, field.getName(), cons.getDefinitionScope());
roots.put(field.getName(), node);
}
}

View File

@ -1,15 +1,16 @@
package org.enso.interpreter.runtime.data.atom;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.nodes.RootNode;
import org.enso.compiler.context.LocalScope;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.scope.ModuleScope;
@NodeInfo(shortName = "get_field", description = "A base for auto-generated Atom getters.")
final class GetFieldNode extends RootNode {
final class GetFieldNode extends EnsoRootNode {
private final int index;
private final String name;
private final Type type;
@ -22,8 +23,8 @@ final class GetFieldNode extends RootNode {
* @param language the current language instance.
* @param index the index this node should use for field lookup.
*/
GetFieldNode(TruffleLanguage<?> language, int index, Type type, String name) {
super(language);
GetFieldNode(EnsoLanguage language, int index, Type type, String name, ModuleScope moduleScope) {
super(language, LocalScope.root(), moduleScope, name, null);
this.index = index;
this.type = type;
this.name = name;
@ -64,6 +65,6 @@ final class GetFieldNode extends RootNode {
@Override
protected GetFieldNode cloneUninitialized() {
return new GetFieldNode(getLanguage(EnsoLanguage.class), index, type, name);
return new GetFieldNode(getLanguage(EnsoLanguage.class), index, type, name, getModuleScope());
}
}

View File

@ -1,15 +1,17 @@
package org.enso.interpreter.runtime.data.atom;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.nodes.RootNode;
import org.enso.compiler.context.LocalScope;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.scope.ModuleScope;
@NodeInfo(
shortName = "get_cons",
description = "A base for auto-generated module-level atom constructor getters.")
final class QualifiedAccessorNode extends RootNode {
final class QualifiedAccessorNode extends EnsoRootNode {
private final AtomConstructor atomConstructor;
/**
@ -18,8 +20,14 @@ final class QualifiedAccessorNode extends RootNode {
* @param language the current language instance.
* @param atomConstructor the constructor to return.
*/
QualifiedAccessorNode(TruffleLanguage<?> language, AtomConstructor atomConstructor) {
super(language);
QualifiedAccessorNode(
EnsoLanguage language, AtomConstructor atomConstructor, ModuleScope moduleScope) {
super(
language,
LocalScope.root(),
moduleScope,
atomConstructor.getQualifiedName().toString(),
null);
this.atomConstructor = atomConstructor;
}

View File

@ -54,7 +54,7 @@ public final class ModuleScope implements EnsoObject {
this.imports = new HashSet<>();
this.exports = new HashSet<>();
this.module = module;
this.associatedType = Type.createSingleton(module.getName().item(), this, null, false);
this.associatedType = Type.createSingleton(module.getName().item(), this, null, false, false);
}
public ModuleScope(
@ -103,7 +103,7 @@ public final class ModuleScope implements EnsoObject {
}
/**
* Returns a map of methods defined in this module for a given constructor.
* Returns a map of methods defined in this module for a given type.
*
* @param type the type for which method map is requested
* @return a map containing all the defined methods by name
@ -328,8 +328,8 @@ public final class ModuleScope implements EnsoObject {
/**
* Returns the names of methods for the given type.
*
* @param tpe the type in the scope
* @return names of methods
* @param tpe the type in the scope. If null, treated as {@code noType}.
* @return names of methods or null
*/
public Set<String> getMethodNamesForType(Type tpe) {
Type tpeKey = tpe == null ? noTypeKey : tpe;
@ -337,6 +337,22 @@ public final class ModuleScope implements EnsoObject {
return allTpeMethods == null ? null : allTpeMethods.keySet();
}
/**
* Returns a set of all the functions for a type, or null.
*
* @param tpe the type in the scope. If null, treated as {@code noType}.
* @return set of methods or null.
*/
public Set<Function> getMethodsForType(Type tpe) {
Type tpeKey = tpe == null ? noTypeKey : tpe;
var allTpeMethods = methods.get(tpeKey);
if (allTpeMethods != null) {
return allTpeMethods.values().stream().map(Supplier::get).collect(Collectors.toSet());
} else {
return null;
}
}
/**
* Registers all methods of a type in the provided scope.
*

View File

@ -42,6 +42,10 @@ public class ScalaConversions {
return scala.collection.immutable.$colon$colon$.MODULE$.apply(head, tail);
}
public static <T> scala.collection.immutable.Seq<T> seq(List<T> list) {
return CollectionConverters.asScala(list).toSeq();
}
/**
* Create a Scala set from the provided elements.
*

View File

@ -521,13 +521,14 @@ class IrToTruffle(
builtinRootNode
.setModuleName(moduleScope.getModule.getName)
builtinRootNode.setTypeName(cons.getQualifiedName)
val funcSchema = FunctionSchema
.newBuilder()
.argumentDefinitions(bodyBuilder.args(): _*)
.build()
new RuntimeFunction(
m.getFunction.getCallTarget,
null,
new FunctionSchema(
new Array[RuntimeAnnotation](0),
bodyBuilder.args(): _*
)
funcSchema
)
} else {
m.getFunction
@ -632,12 +633,17 @@ class IrToTruffle(
}
}
val funcSchema = FunctionSchema
.newBuilder()
.annotations(annotations: _*)
.argumentDefinitions(arguments: _*)
.build()
Right(
Some(
new RuntimeFunction(
callTarget,
null,
new FunctionSchema(annotations.toArray, arguments: _*)
funcSchema
)
)
)
@ -716,10 +722,14 @@ class IrToTruffle(
)
val callTarget = rootNode.getCallTarget
val arguments = bodyBuilder.args()
val funcSchema = FunctionSchema
.newBuilder()
.argumentDefinitions(arguments: _*)
.build()
new RuntimeFunction(
callTarget,
null,
new FunctionSchema(arguments: _*)
funcSchema
)
case _ =>
throw new CompilerError(
@ -904,10 +914,9 @@ class IrToTruffle(
constructor.getAccessorFunction()
def mkTypeGetter(tp: Type): RuntimeFunction = {
new RuntimeFunction(
new ConstantNode(language, tp).getCallTarget,
null,
new FunctionSchema(
val funcSchema = FunctionSchema
.newBuilder()
.argumentDefinitions(
new ArgumentDefinition(
0,
ConstantsNames.SELF_ARGUMENT,
@ -916,6 +925,11 @@ class IrToTruffle(
ArgumentDefinition.ExecutionMode.EXECUTE
)
)
.build()
new RuntimeFunction(
new ConstantNode(language, tp).getCallTarget,
null,
funcSchema
)
}

View File

@ -48,15 +48,36 @@ class RuntimeStubsGenerator(builtins: Builtins) {
scope.registerType(builtinType.getType)
builtinType.getType.setShadowDefinitions(scope, true)
} else {
val isTypeProjectPrivate =
tp.members.nonEmpty && tp.members.forall(_.isProjectPrivate)
val createdType =
if (tp.members.nonEmpty || tp.builtinType) {
Type.create(tp.name, scope, builtins.any(), builtins.any(), false)
Type.create(
tp.name,
scope,
builtins.any(),
builtins.any(),
false,
isTypeProjectPrivate
)
} else {
Type.createSingleton(tp.name, scope, builtins.any(), false)
Type.createSingleton(
tp.name,
scope,
builtins.any(),
false,
isTypeProjectPrivate
)
}
val rtp = scope.registerType(createdType)
tp.members.foreach { cons =>
val constructor = new AtomConstructor(cons.name, scope, rtp)
val constructor =
new AtomConstructor(
cons.name,
scope,
rtp,
false
)
rtp.registerConstructor(constructor)
}
}

View File

@ -256,6 +256,66 @@ fn type_constructors() {
test(&code.join("\n"), expected);
}
#[test]
fn type_constructor_private() {
#[rustfmt::skip]
let code = [
"type Foo",
" private Bar"
];
#[rustfmt::skip]
let expected = block![
(TypeDef type Foo #()
#((Private (ConstructorDefinition Bar #() #()))))];
test(&code.join("\n"), expected);
#[rustfmt::skip]
let code = [
"type Foo",
" private Bar",
" Foo"
];
#[rustfmt::skip]
let expected = block![
(TypeDef type Foo #()
#((Private (ConstructorDefinition Bar #() #()))
(ConstructorDefinition Foo #() #()))
)
];
test(&code.join("\n"), expected);
#[rustfmt::skip]
let code = [
"type Geo",
" private Circle",
" radius",
" x",
" Rectangle width height",
" Point",
];
#[rustfmt::skip]
let expected = block![
(TypeDef type Geo #()
#((Private(ConstructorDefinition
Circle #() #(((() (Ident radius) () ())) ((() (Ident x) () ())))))
(ConstructorDefinition
Rectangle #((() (Ident width) () ()) (() (Ident height) () ())) #())
(ConstructorDefinition Point #() #())))];
test(&code.join("\n"), expected);
#[rustfmt::skip]
let code = [
"type My_Type",
" private Value a b c"
];
let expected = block![
(TypeDef type My_Type #()
#((Private (ConstructorDefinition Value #((() (Ident a) () ()) (() (Ident b) () ()) (() (Ident c) () ())) #())))
)
];
test(&code.join("\n"), expected);
}
#[test]
fn type_methods() {
let code = ["type Geo", " number =", " x", " area self = x + x"];
@ -1309,10 +1369,43 @@ fn pattern_match_suspended_default_arguments() {
}
// === Private (project-private) keyword ===
// So far, private keyword is only allowed to either have empty body, or to be followed by
// a constructor definition.
// Nothing else can be private yet.
#[test]
fn private_keyword() {
test("private", block![(Private())]);
test("private func", block![(Private (Ident func))]);
expect_invalid_node("private func");
expect_invalid_node("private ConstructorOutsideType");
#[rustfmt::skip]
let code = [
"type My_Type",
" private method self = self.x"
];
expect_invalid_node(&code.join("\n"));
#[rustfmt::skip]
let code = [
"type My_Type",
" private"
];
expect_invalid_node(&code.join("\n"));
#[rustfmt::skip]
let code = [
"pub_method x y = x + y",
"private priv_method x y = x + y"
];
expect_invalid_node(&code.join("\n"));
#[rustfmt::skip]
let code = [
"private type My_Type",
" Ctor"
];
expect_invalid_node(&code.join("\n"));
}

View File

@ -337,6 +337,35 @@ fn type_def_body<'s>(
fn to_body_statement(mut line_expression: syntax::Tree<'_>) -> syntax::Tree<'_> {
use syntax::tree::*;
// Unwrap `Private` tree from any `Invalid` added in expression context; it will be revalidated
// in the new context.
if let Tree {
variant:
box Variant::Invalid(Invalid {
ast: mut inner @ Tree { variant: box Variant::Private(_), .. },
..
}),
span,
} = line_expression
{
inner.span = span;
return to_body_statement(inner);
}
// Recurse into body of `Private` keyword; validate usage of the keyword in type-body context.
if let Tree { variant: box Variant::Private(ref mut private), .. } = &mut line_expression {
let body_statement = private.body.take().map(to_body_statement);
let error = match body_statement.as_ref().map(|tree| &*tree.variant) {
Some(Variant::ConstructorDefinition(_)) => None,
None => Some("Expected declaration after `private` keyword in type definition."),
_ => Some("The `private` keyword inside a type definition may only be applied to a constructor definition."),
};
private.body = body_statement;
return match error {
Some(error) => line_expression.with_error(error),
None => line_expression,
};
}
if let Tree { variant: box Variant::Documented(Documented { expression, .. }), .. } =
&mut line_expression
{
@ -715,14 +744,27 @@ fn freeze<'s>() -> Definition<'s> {
crate::macro_definition! {("FREEZE", everything()) capture_expressions}
}
/// private can be either specified as the very first statement in the module, marking the
/// whole module as private. Or it can be prepended to some definitions. For example it can
/// be prepended to atom constructor definition.
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)
let body_opt = precedence.resolve(segment.result.tokens());
match body_opt {
Some(body) => {
syntax::Tree::private(keyword, Some(body))
.with_error("The 'private' keyword cannot be applied to any expression outside of a type definition")
}
None => {
// Just a private keyword without a body. This is valid as the first statement in the
// module, to declare the module as private.
syntax::Tree::private(keyword, None)
}
}
}
/// Macro body builder that just parses the tokens of each segment as expressions, and places them

View File

@ -42,6 +42,15 @@ pub struct Location {
pub col16: u32,
}
impl Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Location{{")?;
write!(f, "utf8 = {utf8}, utf16 = {utf16}, ", utf8 = self.utf8, utf16 = self.utf16)?;
write!(f, "line = {line}, col16 = {col16}", line = self.line, col16 = self.col16)?;
write!(f, "}}")
}
}
impl Add<Length> for Location {
type Output = Self;
@ -386,4 +395,14 @@ pub mod debug {
assert_eq!(&non_char_boundary_locations, &[]);
}
}
impl Display for LocationCheck {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LocationCheck{{")?;
for (k, v) in &self.locations {
write!(f, "{k}: {v}")?;
}
write!(f, "}}")
}
}
}

View File

@ -5,3 +5,5 @@ version: 0.0.1
license: MIT
author: enso-dev@enso.org
maintainer: enso-dev@enso.org
# Base_Tests import stuff from Helpers sibling project. So we need this property here.
prefer-local-libraries: true

View File

@ -16,6 +16,7 @@ import project.Semantic.Equals_Spec
import project.Semantic.Runtime_Spec
import project.Semantic.Self_Type_Spec
import project.Semantic.Warnings_Spec
import project.Semantic.Private_Spec
import project.Semantic.Java_Interop_Spec
import project.Semantic.Js_Interop_Spec
@ -168,6 +169,7 @@ main filter=Nothing =
Statistics_Spec.add_specs suite_builder
Regression_Spec.add_specs suite_builder
Warnings_Spec.add_specs suite_builder
Private_Spec.add_specs suite_builder
System_Spec.add_specs suite_builder
Random_Spec.add_specs suite_builder
XML_Spec.add_specs suite_builder

View File

@ -0,0 +1,87 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Private_Access
from Standard.Test import all
from enso_dev.Helpers import Priv_Type
polyglot java import org.enso.base_test_helpers.CallbackHelper
type My_Priv_Type
private Cons data
add_specs spec_builder =
spec_builder.group "Private constructors" group_builder->
group_builder.specify "cannot directly call private constructor" <|
Test.expect_panic Private_Access <| Priv_Type.Cons 42
# Optimally, we want `Priv_Type.Cons` expression to result in a compilation error.
# All the references to private methods/constructors are theoretically resolvable
# at compilation time. However, the current state of the static compiler does not
# allow us to do that easily.
# For more info, see https://github.com/enso-org/enso/issues/6729
group_builder.specify "can get reference to private constructor, but cannot call it" <|
cons_fn data = Priv_Type.Cons data
Test.expect_panic Private_Access <| cons_fn 42
group_builder.specify "can call private constructor via public factory method" <|
obj = Priv_Type.create 42
obj.is_nothing . should_be_false
group_builder.specify "cannot get private field" <|
obj = Priv_Type.create 42
Test.expect_panic Private_Access <| obj.data
group_builder.specify "can get private field via public accessor" <|
obj = Priv_Type.create 42
obj.get_data . should_equal 42
group_builder.specify "cannot get private field from JS" <|
obj = Priv_Type.create 42
# When JS tries to access a private field, it does not throw a panic, as it does not
# see the field. It returns undefined.
ret = js_access_field obj
ret.is_nothing . should_be_true
group_builder.specify "can call private constructor via public factory method from JS" <|
create_fn x = Priv_Type.create x
obj = js_create_obj create_fn
obj.is_nothing . should_be_false
group_builder.specify "can get private field via public accessor from JS" <|
obj = Priv_Type.create 42
js_access_field_via_getter obj . should_equal 42
group_builder.specify "cannot call private constructor from Java" <|
cons_fn data = Priv_Type.Cons data
Test.expect_panic Private_Access <| CallbackHelper.runCallbackInt cons_fn 42
group_builder.specify "cannot call private constructor from a lambda method" <|
Test.expect_panic Private_Access <|
Priv_Type.in_ctx <|
Priv_Type.Cons 42
# Note that pattern matching on project-private constructor from a different project
# is a compilation error. So we can only test it for the same project.
group_builder.specify "can pattern match on private constructor from the same project" <|
obj = My_Priv_Type.Cons 42
res = case obj of
My_Priv_Type.Cons x -> x
_ -> 0
res . should_equal 42
foreign js js_access_field obj = """
return obj.data;
foreign js js_access_field_via_getter obj = """
return obj.get_data();
foreign js js_create_obj cons_fn = """
return cons_fn(42);
main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder
suite.run_with_filter filter

4
test/Helpers/README.md Normal file
View File

@ -0,0 +1,4 @@
Just a simple test project that contains some private-project entities that can
be called from different test projects.
This project is not meant to be run directly.

View File

@ -0,0 +1,7 @@
name: Helpers
namespace: enso_dev
enso-version: default
version: 0.0.1
license: MIT
author: enso-dev@enso.org
maintainer: enso-dev@enso.org

View File

@ -0,0 +1,15 @@
type Priv_Type
## Both constructor `Cons` and field `data` are private
private Cons data
## Public factory method
create data = Priv_Type.Cons data
## Public accessor for the private `data` field
get_data self = self.data
in_ctx ~action =
result = action
result

View File

@ -25,7 +25,7 @@ add_specs suite_builder =
enter d1844837-30e7-46b7-bde3-72f7afec52cf
result d1844837-30e7-46b7-bde3-72f7afec52cf result: UnresolvedSymbol<Count> type: Function
enter 00e33517-6d19-4f5a-84c5-126f59a93b67
callfn 00e33517-6d19-4f5a-84c5-126f59a93b67 fn: null self=_ args: [Aggregate_Column]
callfn 00e33517-6d19-4f5a-84c5-126f59a93b67 fn: Standard.Table.Aggregate_Column.Aggregate_Column.Count self=_ args: [Aggregate_Column]
result 00e33517-6d19-4f5a-84c5-126f59a93b67 result: (Count '') type: Aggregate_Column
pair.first.column_names . should_equal pair.second.column_names