Enforce that abstract methods are implemented - #1785
Open
HT154 wants to merge 2 commits into
Open
Conversation
HT154
commented
Jul 20, 2026
Comment on lines
+2
to
+4
| Class `abstractMethodNotImplemented2#MyClass` should either be declared `abstract`, or implement the following methods: | ||
| foo() | ||
| bar() |
Contributor
Author
There was a problem hiding this comment.
This is a style change compared to #1690 to match how candidates in "Cannot find method" errors are displayed.
HT154
force-pushed
the
abstract-method-implementation-check
branch
4 times, most recently
from
July 20, 2026 18:01
fd9fc8a to
4fb7ec8
Compare
HT154
marked this pull request as ready for review
July 21, 2026 17:13
HT154
force-pushed
the
abstract-method-implementation-check
branch
5 times, most recently
from
July 30, 2026 18:13
ae5cf97 to
78fafb2
Compare
bioball
requested changes
Jul 31, 2026
bioball
left a comment
Member
There was a problem hiding this comment.
I think I have a much more elegant solution for this. Basically, we can keep track of class initialization state in VmLocalContext, and use that to determine whether we've fully initialized a class or not.
This is really nice for a couple reasons:
- This is lock free
- No added fields to
VmClassand the overhead that comes with that - No
List<Runnable>, which felt really hacky when I wrote it
Diff here:
diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java
index 79a6f4cb..ed5e3989 100644
--- a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java
+++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java
@@ -123,37 +123,45 @@ public final class ClassNode extends ExpressionNode {
typeParameters,
prototype);
- if (unresolvedSupertypeNode != null) {
- var supertypeNode = unresolvedSupertypeNode.execute(frame);
- var superclass = supertypeNode.getVmClass();
+ var localContext = VmLanguage.get(this).localContext.get();
+ localContext.beginClassInit(cachedClass);
- checkSupertype(supertypeNode, superclass);
- cachedClass.initSupertype(supertypeNode, superclass);
- }
+ try {
+ if (unresolvedSupertypeNode != null) {
+ var supertypeNode = unresolvedSupertypeNode.execute(frame);
+ var superclass = supertypeNode.getVmClass();
- // The superclass resolved above may not itself have completed the below initializations yet.
- // That's because these initializations may have indirectly or directly triggered
- // resolution of this class, in which case the `resolveSuperclass()` call above
- // will have returned the partially initialized `cachedClass` of the superclass.
- // As a consequence, initializations that require a fully initialized class hierarchy
- // are done lazily in VmClass rather than here.
- // A fully initialized class hierarchy is only required for initialization of internal caches,
- // which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to
- // defer.
+ checkSupertype(supertypeNode, superclass);
+ cachedClass.initSupertype(supertypeNode, superclass);
+ }
- VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
+ // The superclass resolved above may not itself have completed the below initializations yet.
+ // That's because these initializations may have indirectly or directly triggered
+ // resolution of this class, in which case the `resolveSuperclass()` call above
+ // will have returned the partially initialized `cachedClass` of the superclass.
+ // As a consequence, initializations that require a fully initialized class hierarchy
+ // are done lazily in VmClass rather than here.
+ // A fully initialized class hierarchy is only required for initialization of internal caches,
+ // which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to
+ // defer.
- for (var node : unresolvedPropertyNodes) {
- cachedClass.addProperty(node.execute(frame, cachedClass));
- }
+ VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
- for (var node : unresolvedMethodNodes) {
- cachedClass.addMethod(node.execute(frame, cachedClass));
- }
+ for (var node : unresolvedPropertyNodes) {
+ cachedClass.addProperty(node.execute(frame, cachedClass));
+ }
- cachedClass.notifyInitialized();
+ for (var node : unresolvedMethodNodes) {
+ cachedClass.addMethod(node.execute(frame, cachedClass));
+ }
- return cachedClass;
+ cachedClass.onOwnClassInitialized();
+ localContext.endClassInit();
+ return cachedClass;
+ } catch (Throwable e) {
+ localContext.clearClassInitState();
+ throw e;
+ }
}
private void checkSupertype(TypeNode supertypeNode, @Nullable VmClass superclass) {
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 ac7bb2c2..a78d2b3d 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,7 +22,6 @@ 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;
@@ -86,13 +85,6 @@ 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;
@@ -249,49 +241,20 @@ 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) {
- checkAbstractMethods();
- }
- });
- uninitializedCount++;
- isAllInitialized = false;
- }
- sc = sc.superclass;
- }
- uninitializedSuperclassCount.set(uninitializedCount);
- if (isAllInitialized) {
- checkAbstractMethods();
- }
- lock:
- synchronized (finalizersLock) {
- if (__finalizers == null) break lock;
- for (var finalizer : __finalizers) {
- finalizer.run();
- }
- this.__finalizers = null;
- }
+ /**
+ * Called when this class itself has been initialized.
+ *
+ * <p>Superclasses may not have been initialized yet.
+ */
+ public void onOwnClassInitialized() {
isInitialized = true;
}
+ /** Called when the entire class hierarchy is completely initialized, including superclasses. */
+ public void onFullyInitialized() {
+ checkAbstractMethods();
+ }
+
public int getTypeParameterCount() {
return typeParameters.size();
}
diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java
index f3f799ce..36c21327 100644
--- a/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java
+++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmLocalContext.java
@@ -15,6 +15,9 @@
*/
package org.pkl.core.runtime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
/** A per-context thread-local value that can be used to influence execution. */
public class VmLocalContext {
private boolean shouldEagerTypecheck = false;
@@ -22,6 +25,12 @@ public class VmLocalContext {
/** Whether we are currently inside a type test ({@code is} check). */
private boolean inTypeTest = false;
+ /** The number of classes currently being initialized. */
+ private int classDepth = 0;
+
+ /** The classes currently being initialized. */
+ private final Deque<VmClass> pendingClasses = new ArrayDeque<>();
+
/**
* Number of active {@link VmValueTracker} instances. Used to determine if instrumentation is
* already active.
@@ -48,6 +57,27 @@ public class VmLocalContext {
return inTypeTest;
}
+ public void beginClassInit(VmClass vmClass) {
+ classDepth++;
+ pendingClasses.add(vmClass);
+ }
+
+ public void endClassInit() {
+ classDepth--;
+ if (classDepth > 0) {
+ return;
+ }
+ while (!pendingClasses.isEmpty()) {
+ var clazz = pendingClasses.pop();
+ clazz.onFullyInitialized();
+ }
+ }
+
+ public void clearClassInitState() {
+ pendingClasses.clear();
+ classDepth = 0;
+ }
+
public void enterTracker() {
activeTrackerDepth++;
instrumentationEverUsed = true;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`.
HT154
force-pushed
the
abstract-method-implementation-check
branch
from
July 31, 2026 17:30
0112ff4 to
ad0e5fb
Compare
HT154
force-pushed
the
abstract-method-implementation-check
branch
from
July 31, 2026 17:50
ad0e5fb to
8caadbc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a check that abstract methods must be implemented. If any members lack an implementation, an error is thrown describing the missing methods.
Also: we have a bug in
pkl:base; classSetdoes not implement all members of classCollection.Supersedes #1690
Fixes #1262