Skip to content

Enforce that abstract methods are implemented - #1785

Open
HT154 wants to merge 2 commits into
apple:mainfrom
HT154:abstract-method-implementation-check
Open

Enforce that abstract methods are implemented#1785
HT154 wants to merge 2 commits into
apple:mainfrom
HT154:abstract-method-implementation-check

Conversation

@HT154

@HT154 HT154 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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; class Set does not implement all members of class Collection.

Supersedes #1690
Fixes #1262

Comment on lines +2 to +4
Class `abstractMethodNotImplemented2#MyClass` should either be declared `abstract`, or implement the following methods:
foo()
bar()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a style change compared to #1690 to match how candidates in "Cannot find method" errors are displayed.

@HT154
HT154 force-pushed the abstract-method-implementation-check branch 4 times, most recently from fd9fc8a to 4fb7ec8 Compare July 20, 2026 18:01
@HT154 HT154 changed the title Enforce that abstract members are implemented Enforce that abstract methods are implemented Jul 21, 2026
@HT154
HT154 marked this pull request as ready for review July 21, 2026 17:13
@HT154
HT154 force-pushed the abstract-method-implementation-check branch 5 times, most recently from ae5cf97 to 78fafb2 Compare July 30, 2026 18:13

@bioball bioball left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 VmClass and 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;

Comment thread stdlib/base.pkl Outdated
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
HT154 force-pushed the abstract-method-implementation-check branch from 0112ff4 to ad0e5fb Compare July 31, 2026 17:30
@HT154
HT154 requested a review from bioball July 31, 2026 17:37
@HT154
HT154 force-pushed the abstract-method-implementation-check branch from ad0e5fb to 8caadbc Compare July 31, 2026 17:50

@bioball bioball left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@HT154
HT154 requested a review from stackoverflow July 31, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extending an abstract class **without** overriding an abstract property crashes with ambiguous message

2 participants