Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class EmitStatement {
static void emitScopeExitNullStores(EmitterContext ctx, int scopeIndex, boolean closeIO) {
if (closeIO) {
// For scalar variables in loop bodies, call cleanup to close IO
// on anonymous globs (deterministic DESTROY for lexical file handles).
// on anonymous globs and fire DESTROY on blessed objects.
java.util.List<Integer> scalarIndices = ctx.symbolTable.getMyScalarIndicesInScope(scopeIndex);
for (int idx : scalarIndices) {
ctx.mv.visitVarInsn(Opcodes.ALOAD, idx);
Expand All @@ -60,7 +60,7 @@ static void emitScopeExitNullStores(EmitterContext ctx, int scopeIndex, boolean
}
}

/** Convenience overload: null stores only, no IO cleanup (safe for all contexts). */
/** Convenience overload: null stores only, no IO/DESTROY cleanup (safe for all contexts). */
static void emitScopeExitNullStores(EmitterContext ctx, int scopeIndex) {
emitScopeExitNullStores(ctx, scopeIndex, false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "92020251e";
public static final String gitCommitId = "865da3648";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public abstract class RuntimeBase implements DynamicState, Iterable<RuntimeScala
// Index to the class that this reference belongs
public int blessId;

// Guard flag to prevent calling DESTROY more than once on the same object.
// Set to true after DESTROY has been called (or is being called).
public boolean destroyCalled;

/**
* Adds this entity to the specified RuntimeList.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ public RuntimeScalar delete(RuntimeScalar key) {
var value = elements.remove(k);
if (byteKeys != null) byteKeys.remove(k);
if (value != null) {
// Call DESTROY on blessed references being removed from a hash.
// In Perl, delete() on the last reference triggers DESTROY.
// Without ref counting, we call it eagerly; the destroyCalled
// flag on RuntimeBase prevents double-DESTROY.
RuntimeScalar.callDestroyIfNeeded(value);
yield new RuntimeScalar(value);
}
yield new RuntimeScalar();
Expand All @@ -432,6 +437,7 @@ public RuntimeScalar delete(String key) {
var value = elements.remove(key);
if (byteKeys != null) byteKeys.remove(key);
if (value != null) {
RuntimeScalar.callDestroyIfNeeded(value);
yield new RuntimeScalar(value);
}
yield new RuntimeScalar();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,9 @@ public RuntimeScalar undefine() {
InheritanceResolver.invalidateCache();
return this;
}
// Call DESTROY on blessed references before clearing.
// undef $obj explicitly discards the reference, so DESTROY should fire.
callDestroyIfNeeded(this);
// Close IO handles when dropping a glob reference.
// This mimics Perl's internal sv_clear behavior where IO handles are closed
// when the glob's reference count drops to zero (independent of DESTROY).
Expand Down Expand Up @@ -1840,6 +1843,55 @@ public static void scopeExitCleanup(RuntimeScalar scalar) {
}
}
}
// Also handle blessed references with DESTROY methods.
// When a lexical variable holding a blessed reference goes out of scope,
// call DESTROY on the object. This is an approximation of Perl's
// reference-counted DESTROY: without ref counting, we may call DESTROY
// early if other references exist. The destroyCalled flag prevents
// double-DESTROY.
callDestroyIfNeeded(scalar);
}

/**
* Calls DESTROY on a blessed object if:
* 1. The scalar holds a blessed reference (blessId != 0)
* 2. DESTROY hasn't been called yet on this object (destroyCalled flag)
* 3. The class hierarchy defines a DESTROY method
* <p>
* Perl semantics: DESTROY exceptions are caught and warned "(in cleanup)".
* DESTROY is called with the blessed reference as $_[0].
* <p>
* Note: Without reference counting, this may call DESTROY while other
* references to the object still exist. The destroyCalled flag on
* RuntimeBase prevents double-DESTROY.
*/
public static void callDestroyIfNeeded(RuntimeScalar scalar) {
if (scalar == null) return;
int blessId = RuntimeScalarType.blessedId(scalar);
if (blessId == 0) return;
RuntimeBase base = (RuntimeBase) scalar.value;
if (base.destroyCalled) return;
base.destroyCalled = true;

String className = NameNormalizer.getBlessStr(blessId);
RuntimeScalar destroyMethod = InheritanceResolver.findMethodInHierarchy(
"DESTROY", className, null, 0);
if (destroyMethod == null || destroyMethod.type != CODE) {
base.destroyCalled = false; // No DESTROY found, allow future attempts
return;
}

try {
// Call DESTROY($self) in void context
RuntimeArray args = new RuntimeArray();
args.push(scalar);
RuntimeCode.apply(destroyMethod, args, RuntimeContextType.VOID);
} catch (Exception e) {
// Perl: DESTROY exceptions are turned into warnings
String msg = e.getMessage();
if (msg == null) msg = e.getClass().getName();
Warnings.warn(new RuntimeArray(new RuntimeScalar("(in cleanup) " + msg + "\n")), RuntimeContextType.VOID);
}
}

public RuntimeScalar defined() {
Expand Down