diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java index 3aeb29c72..8b717f551 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java @@ -24,7 +24,7 @@ import org.pkl.core.ast.VmModifier; import org.pkl.core.runtime.*; public final class ClassProperty extends ClassMember { - private final @Nullable PropertyTypeNode typeNode; + private @Nullable PropertyTypeNode typeNode; private final ObjectMember initializer; public ClassProperty( @@ -64,6 +64,11 @@ public final class ClassProperty extends ClassMember { return VmModifier.getMirrors(mods, false); } + public void lateInitTypeNodeAndModifiers(@Nullable PropertyTypeNode typeNode, int modifiers) { + this.typeNode = typeNode; + this.modifiers = modifiers; + } + public @Nullable PropertyTypeNode getTypeNode() { return typeNode; } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java index 7b7e3c8fd..235941a33 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java @@ -25,7 +25,7 @@ public abstract class Member { protected final SourceSection headerSection; - protected final int modifiers; + protected int modifiers; protected final @Nullable Identifier name; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index 2cad2146e..feb519f8e 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -22,6 +22,7 @@ import com.oracle.truffle.api.dsl.Idempotent; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.source.SourceSection; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.*; import org.graalvm.collections.*; import org.jspecify.annotations.Nullable; @@ -84,6 +85,13 @@ public final class VmClass extends VmValue { private final Object allHiddenPropertyNamesLock = new Object(); + @GuardedBy("finalizersLock") + private @Nullable List __finalizers = null; + + private final Object finalizersLock = new Object(); + + private final AtomicInteger uninitializedSuperclassCount = new AtomicInteger(0); + // Helps to overcome recursive initialization issues // between classes and annotations in pkl.base. @CompilationFinal private volatile boolean isInitialized; @@ -150,6 +158,83 @@ public final class VmClass extends VmValue { prototype.lateInitParent(superclass.getPrototype()); } + @TruffleBoundary + private void checkAbstractMembers() { + if (this.isAbstract()) return; + // minimize allocations in the non-error case + if (!hasAbstractMember()) return; + var abstractMembers = getAbstractMembers(); + if (abstractMembers.size() == 1) { + var member = abstractMembers.get(0); + var message = + member instanceof ClassProperty + ? "noImplementationForAbstractProperty" + : "noImplementationForAbstractMethod"; + throw new VmExceptionBuilder() + .evalError(message, getDisplayName(), member.getName().toString()) + .withSourceSection(getHeaderSection()) + .build(); + } + var memberList = new StringBuilder(); + var isFirst = true; + for (var member : abstractMembers) { + if (isFirst) { + isFirst = false; + } else { + memberList.append('\n'); + } + memberList.append("* "); + if (member instanceof ClassProperty) { + memberList.append("property "); + } else { + memberList.append("method "); + } + memberList.append('`').append(member.getName()).append('`'); + } + throw new VmExceptionBuilder() + .evalError("noImplementationForAbstractMembers", getDisplayName(), memberList.toString()) + .withSourceSection(getHeaderSection()) + .build(); + } + + private boolean hasAbstractMember() { + var propertyCursor = getAllProperties().getEntries(); + while (propertyCursor.advance()) { + var property = propertyCursor.getValue(); + if (property.isAbstract()) { + return true; + } + } + var methodCursor = getAllMethods().getEntries(); + while (methodCursor.advance()) { + var method = methodCursor.getValue(); + if (method.isAbstract()) { + return true; + } + } + return false; + } + + private List getAbstractMembers() { + assert this.superclass != null; + var result = new ArrayList(); + var propertyCursor = getAllProperties().getEntries(); + while (propertyCursor.advance()) { + var property = propertyCursor.getValue(); + if (property.isAbstract()) { + result.add(property); + } + } + var methodCursor = getAllMethods().getEntries(); + while (methodCursor.advance()) { + var method = methodCursor.getValue(); + if (method.isAbstract()) { + result.add(method); + } + } + return result; + } + @TruffleBoundary public void addProperty(ClassProperty property) { prototype.addProperty(property.getInitializer()); @@ -190,8 +275,46 @@ public final class VmClass extends VmValue { } } + private void onInitialized(Runnable runnable) { + synchronized (finalizersLock) { + if (this.__finalizers == null) { + this.__finalizers = new ArrayList<>(); + } + this.__finalizers.add(runnable); + } + } + // Note: Superclasses may not have finished their initialization when this method is called. public void notifyInitialized() { + var sc = superclass; + var isAllInitialized = true; + var uninitializedCount = 0; + while (sc != null) { + if (!sc.isInitialized) { + sc.onInitialized( + () -> { + var count = uninitializedSuperclassCount.decrementAndGet(); + if (count == 0) { + checkAbstractMembers(); + } + }); + uninitializedCount++; + isAllInitialized = false; + } + sc = sc.superclass; + } + uninitializedSuperclassCount.set(uninitializedCount); + if (isAllInitialized) { + checkAbstractMembers(); + } + lock: + synchronized (finalizersLock) { + if (__finalizers == null) break lock; + for (var finalizer : __finalizers) { + finalizer.run(); + } + this.__finalizers = null; + } isInitialized = true; } @@ -735,13 +858,27 @@ public final class VmClass extends VmValue { for (var property : EconomicMaps.getValues(declaredProperties)) { if (property.isLocal()) continue; - // A property is considered a class property definition - // if it has a type annotation or has no superdefinition (ad-hoc case). + // A property is considered a class property definition if any of the following are true: + // + // 1. It has a type annotation. + // 2. It has no superdefinition. + // 3. It is not abstract but its superclass is. + // // Otherwise, it is considered an object property definition, // which means it affects the class prototype but not the class itself. // An example for the latter is when `Module.output` is overridden with `output { ... }`. - if (property.getTypeNode() != null || !EconomicMaps.containsKey(result, property.getName())) { + if (property.getTypeNode() != null) { EconomicMaps.put(result, property.getName(), property); + } else { + var existingProperty = EconomicMaps.get(result, property.getName()); + if (existingProperty != null && existingProperty.isAbstract() && !this.isAbstract()) { + property.lateInitTypeNodeAndModifiers( + existingProperty.getTypeNode(), + existingProperty.getModifiers() & ~VmModifier.ABSTRACT); + EconomicMaps.put(result, property.getName(), property); + } else if (existingProperty == null) { + EconomicMaps.put(result, property.getName(), property); + } } } diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index 7ad9603aa..3c23038e2 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -1176,3 +1176,13 @@ Cannot follow redirect from ''https:'' URL to ''http:'' URL.\ \n\ HTTP Request: `GET {0}`\n\ Redirected to: `{1}` + +noImplementationForAbstractProperty=\ +Class `{0}` should either be declared `abstract`, or should implement property `{1}`. + +noImplementationForAbstractMethod=\ +Class `{0}` should either be declared `abstract`, or should implement method `{1}`. + +noImplementationForAbstractMembers=\ +Class `{0}` should either be declared `abstract`, or implement the following members:\n\ +{1} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/classes/AbstractModule.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/classes/AbstractModule.pkl new file mode 100644 index 000000000..6a592292d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/classes/AbstractModule.pkl @@ -0,0 +1,3 @@ +abstract module Foo + +abstract bar: Int diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented1.pkl new file mode 100644 index 000000000..57861cce5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented1.pkl @@ -0,0 +1,19 @@ +abstract class AbstractProperty { + abstract foo: Int +} + +abstract class AbstractMethod { + abstract function foo(): Int +} + +abstract class AbstractPropertiesAndMethods { + abstract foo: Int + abstract bar: Int + abstract function foo(): Int + abstract function bar(): Int +} + +class MyClass extends AbstractProperty { +} + +foo: MyClass diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented2.pkl new file mode 100644 index 000000000..4498fff19 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented2.pkl @@ -0,0 +1,9 @@ +abstract class AbstractMethod { + abstract function foo(): Int +} + +class MyClass extends AbstractMethod { +} + +foo: MyClass + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented3.pkl new file mode 100644 index 000000000..9c627a573 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented3.pkl @@ -0,0 +1,10 @@ +abstract class AbstractPropertiesAndMethods { + abstract foo: Int + abstract bar: Int + abstract function foo(): Int + abstract function bar(): Int +} + +class MyClass extends AbstractPropertiesAndMethods {} + +foo: MyClass diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented4.pkl new file mode 100644 index 000000000..d84a1a6e3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/abstractMemberNotImplemented4.pkl @@ -0,0 +1 @@ +extends "../../input-helper/classes/AbstractModule.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented1.err new file mode 100644 index 000000000..7921b4f8c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented1.err @@ -0,0 +1,10 @@ +–– Pkl Error –– +Class `abstractMemberNotImplemented1#MyClass` should either be declared `abstract`, or should implement property `foo`. + +xx | class MyClass extends AbstractProperty { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at abstractMemberNotImplemented1#MyClass (file:///$snippetsDir/input/errors/abstractMemberNotImplemented1.pkl) + +xx | foo: MyClass + ^^^^^^^ +at abstractMemberNotImplemented1 (file:///$snippetsDir/input/errors/abstractMemberNotImplemented1.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented2.err new file mode 100644 index 000000000..1284ebe65 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented2.err @@ -0,0 +1,10 @@ +–– Pkl Error –– +Class `abstractMemberNotImplemented2#MyClass` should either be declared `abstract`, or should implement method `foo`. + +x | class MyClass extends AbstractMethod { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at abstractMemberNotImplemented2#MyClass (file:///$snippetsDir/input/errors/abstractMemberNotImplemented2.pkl) + +x | foo: MyClass + ^^^^^^^ +at abstractMemberNotImplemented2 (file:///$snippetsDir/input/errors/abstractMemberNotImplemented2.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented3.err new file mode 100644 index 000000000..6c9430957 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented3.err @@ -0,0 +1,14 @@ +–– Pkl Error –– +Class `abstractMemberNotImplemented3#MyClass` should either be declared `abstract`, or implement the following members: +* property `foo` +* property `bar` +* method `foo` +* method `bar` + +x | class MyClass extends AbstractPropertiesAndMethods {} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at abstractMemberNotImplemented3#MyClass (file:///$snippetsDir/input/errors/abstractMemberNotImplemented3.pkl) + +xx | foo: MyClass + ^^^^^^^ +at abstractMemberNotImplemented3 (file:///$snippetsDir/input/errors/abstractMemberNotImplemented3.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented4.err new file mode 100644 index 000000000..8993a9999 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMemberNotImplemented4.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Class `abstractMemberNotImplemented4` should either be declared `abstract`, or should implement property `bar`. + +x | extends "../../input-helper/classes/AbstractModule.pkl" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at abstractMemberNotImplemented4 (file:///$snippetsDir/input/errors/abstractMemberNotImplemented4.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMethodsNotImplemented.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMethodsNotImplemented.err new file mode 100644 index 000000000..671c3d490 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/abstractMethodsNotImplemented.err @@ -0,0 +1,10 @@ +–– Pkl Error –– +Class `abstractMethodsNotImplemented#MyClass` either needs to be abstract, or should implement property `foo`. + +x | class MyClass extends MyAbstractClass { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at abstractMethodsNotImplemented#MyClass (file:///$snippetsDir/input/errors/abstractMethodsNotImplemented.pkl) + +x | foo: MyClass + ^^^^^^^ +at abstractMethodsNotImplemented (file:///$snippetsDir/input/errors/abstractMethodsNotImplemented.pkl) diff --git a/stdlib/base.pkl b/stdlib/base.pkl index 2b5efaf79..400493ae7 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -3562,6 +3562,18 @@ external class Set extends Collection { /// The difference of this set and [other]. external function difference(other: Set): Set + + function indexOf(_): Int = throw("Set does not implement `indexOf`") + function indexOfOrNull(_): Int? = throw("Set does not implement `indexOfOrNull`") + + function lastIndexOf(_): Int = throw("Set does not implement `lastIndexOf`") + function lastIndexOfOrNull(_): Int? = throw("Set does not implement `lastIndexOfOrNull`") + + function findIndex(_): Int = throw("Set does not implement `findIndex`") + function findIndexOrNull(_): Int? = throw("Set does not implement `findIndexOrNull`") + + function findLastIndex(_): Int = throw("Set does not implement `findLastIndex`") + function findLastIndexOrNull(_): Int? = throw("Set does not implement `findLastIndexOrNull`") } /// Creates a map containing the given alternating [keysAndValues].