Enforce that abstract members are implemented

This adds a check that abstract members must be implemented.
If any members lack an implementation, an error is thrown describing
the missing members.

Also: we have a bug in `pkl:base`; class `Set` does not implement all
members of class `Collection`.
This commit is contained in:
Dan Chao
2026-06-12 15:25:51 -07:00
parent a9c98e4396
commit a6b02692bb
15 changed files with 261 additions and 5 deletions
@@ -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;
}
@@ -25,7 +25,7 @@ public abstract class Member {
protected final SourceSection headerSection;
protected final int modifiers;
protected int modifiers;
protected final @Nullable Identifier name;
@@ -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<Runnable> __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<ClassMember> getAbstractMembers() {
assert this.superclass != null;
var result = new ArrayList<ClassMember>();
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);
}
}
}
@@ -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}
@@ -0,0 +1,3 @@
abstract module Foo
abstract bar: Int
@@ -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
@@ -0,0 +1,9 @@
abstract class AbstractMethod {
abstract function foo(): Int
}
class MyClass extends AbstractMethod {
}
foo: MyClass
@@ -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
@@ -0,0 +1 @@
extends "../../input-helper/classes/AbstractModule.pkl"
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
+12
View File
@@ -3562,6 +3562,18 @@ external class Set<out Element> extends Collection<Element> {
/// The difference of this set and [other].
external function difference(other: Set): Set<Element>
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].