Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,18 @@ public static int executeSelect(
}

/**
* SLOW_LOAD_GLOB: rd = getGlobalIO(name)
* SLOW_LOAD_GLOB: rd = getGlobalIO(name).createDetachedCopy()
* Format: [SLOW_LOAD_GLOB] [rd] [name_idx]
* Effect: Loads a glob/filehandle from global variables
*
* <p><b>IMPORTANT:</b> This returns a detached copy of the glob.
* This is crucial for the {@code do { local *FH; *FH }} pattern used to create anonymous
* filehandles. The detached copy captures the current IO slot, so that when the local
* scope ends and restores the global glob, the captured copy retains its IO.
*
* <p>If we returned the global glob directly, the copy would only be made when the
* glob is assigned to a variable (in RuntimeScalar constructor), which happens AFTER
* the local scope ends, and by that time the IO would have been restored to the original.
*/
public static int executeLoadGlob(
int[] bytecode,
Expand All @@ -398,7 +407,8 @@ public static int executeLoadGlob(
// Call GlobalVariable.getGlobalIO() to get the RuntimeGlob
RuntimeGlob glob = GlobalVariable.getGlobalIO(globName);

registers[rd] = glob;
// Return a detached copy to preserve IO during local scope
registers[rd] = glob.createDetachedCopy();
return pc;
}

Expand Down
17 changes: 16 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,23 @@ static void handleLocal(EmitterVisitor emitterVisitor, OperatorNode node) {
lvalueContext = LValueVisitor.getContext(varToLocal);
}

varToLocal.accept(emitterVisitor.with(lvalueContext));
boolean isTypeglob = varToLocal instanceof OperatorNode operatorNode && operatorNode.operator.equals("*");

// For local *GLOB, we must NOT create a detached copy - we need to localize the actual
// global glob from globalIORefs, so that later accesses via *GLOB see the new IO slot.
// This is critical for the `do { local *FH; *FH }` pattern to work correctly.
if (isTypeglob && varToLocal instanceof OperatorNode opNode2 && opNode2.operand instanceof IdentifierNode idNode) {
String fullName = NameNormalizer.normalizeVariableName(idNode.name, emitterVisitor.ctx.symbolTable.getCurrentPackage());
mv.visitLdcInsn(fullName);
mv.visitMethodInsn(org.objectweb.asm.Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/GlobalVariable",
"getGlobalIO",
"(Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeGlob;",
false);
} else {
varToLocal.accept(emitterVisitor.with(lvalueContext));
}

// save the old value
if (isTypeglob) {
emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC,
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("GETVAR " + sigil + name);

if (sigil.equals("*")) {
// typeglob
// typeglob - return a detached copy to preserve IO during local scope
// This is crucial for the `do { local *FH; *FH }` pattern
String fullName = NameNormalizer.normalizeVariableName(name, emitterVisitor.ctx.symbolTable.getCurrentPackage());
mv.visitLdcInsn(fullName); // emit string
emitterVisitor.ctx.mv.visitMethodInsn(
Expand All @@ -355,6 +356,13 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n
"getGlobalIO",
"(Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeGlob;",
false);
// Create detached copy NOW (before local restores) to capture current IO
emitterVisitor.ctx.mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeGlob",
"createDetachedCopy",
"()Lorg/perlonjava/runtime/runtimetypes/RuntimeGlob;",
false);
return;
}

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 = "36a15d5a2";
public static final String gitCommitId = "861aaf63a";

/**
* 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 @@ -344,15 +344,11 @@ public RuntimeScalar sync() {
*
* <p>Java's FileChannel does not expose the underlying OS file descriptor.
* We return undef to match Perl's behavior for handles without a real fd.
* Note: Validity checks should be done in the Java backend, not via fileno().
*
* @return RuntimeScalar with undef (Java doesn't expose real fds)
*/
@Override
public RuntimeScalar fileno() {
// Java's FileChannel does not expose the underlying OS file descriptor.
// Return undef to match Perl's behavior for handles without a real fd.
// Note: Validity checks should be done in the Java backend, not via fileno().
return RuntimeScalarCache.scalarUndef;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,24 @@ public static RuntimeGlob getGlobalIO(String key) {
return glob;
}

/**
* Retrieves a detached copy of a global IO reference, wrapped in a RuntimeScalar.
*
* <p>This method is crucial for the {@code do { local *FH; *FH }} pattern used to create
* anonymous filehandles. By creating the detached copy immediately when the glob is
* evaluated, we capture the current IO slot BEFORE the local scope ends and restores
* the original IO.
*
* <p>The detached copy has the same globName (for stringification) but its own IO
* reference that is independent of the global glob after the copy is made.
*
* @param key The key of the global IO reference.
* @return A RuntimeScalar containing a detached copy of the glob.
*/
public static RuntimeScalar getGlobalIOCopy(String key) {
return new RuntimeScalar(getGlobalIO(key));
}

/**
* Checks if a global IO reference exists.
*
Expand Down
80 changes: 62 additions & 18 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,33 @@ public RuntimeGlob(String globName) {
}

/**
* Creates a detached copy of this glob that shares the current IO slot reference.
* Used when assigning a glob to a scalar: `my $fh = *FH`
* Creates a detached copy of this glob that has its own independent IO slot.
*
* <p>This is crucial for `local *GLOB` semantics. When you do:
* <p>This is crucial for the {@code do { local *GLOB; *GLOB }} pattern used to create
* anonymous filehandles. When you do:
* <pre>
* local *FH;
* open FH, ...;
* my $captured = *FH;
* return $captured;
* my $fh = do { local *FH; open FH, ...; *FH };
* </pre>
* After the local scope ends, *FH's IO is restored, but $captured should
* still have the IO that was opened. This method creates a new RuntimeGlob
* that points to the CURRENT IO object, so when local restores the original
* glob, the captured copy is unaffected.
* The returned glob must retain the IO that was opened, even after the local scope
* ends and restores the global *FH. This method creates a new RuntimeGlob that:
* <ul>
* <li>Has the same globName (for stringification)</li>
* <li>Shares the CURRENT IO RuntimeScalar reference, so that opening a file
* on the original glob also affects this copy</li>
* </ul>
*
* <p>Subclasses (like RuntimeStashEntry) should override this to return
* the same instance, preserving their special ref() behavior.
* <p><b>IMPORTANT:</b> The copy shares the same IO RuntimeScalar object as the
* original at the time of copying. This means:
* <ul>
* <li>If you call {@code setIO()} on the original, it modifies the shared IO in place,
* so the copy sees the change</li>
* <li>When {@code local} restores the original glob's IO reference (via
* {@code this.IO = savedIO}), the copy's IO reference is NOT affected because
* it's a separate field</li>
* </ul>
*
* <p>Subclasses (like RuntimeStashEntry) should override this to return the same
* instance, preserving their special ref() behavior.
*
* @return A new RuntimeGlob with the same globName and IO reference.
*/
Expand All @@ -66,6 +76,34 @@ public RuntimeGlob createDetachedCopy() {
return copy;
}

/**
* Returns a hash code based on the glob name.
* This ensures that all copies of the same glob (including detached copies)
* have the same hash code, which is necessary for correct stringification
* and equality comparisons in Perl code like `$_[0] eq \*FOO`.
*
* @return Hash code based on the glob name
*/
@Override
public int hashCode() {
return globName != null ? globName.hashCode() : 0;
}

/**
* Checks equality based on glob name.
* Two RuntimeGlob objects are equal if they have the same globName.
* This ensures that detached copies compare equal to the original glob.
*
* @param obj The object to compare
* @return true if both are RuntimeGlob with the same globName
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof RuntimeGlob other)) return false;
return globName != null ? globName.equals(other.globName) : other.globName == null;
}

public static boolean isGlobAssigned(String globName) {
return GlobalVariable.globalGlobs.getOrDefault(globName, false);
}
Expand Down Expand Up @@ -235,8 +273,11 @@ public RuntimeScalar set(RuntimeGlob value) {
InheritanceResolver.invalidateCache();

// Alias the IO slot: both names point to the same IO object
// Must update BOTH this.IO (for detached copies) AND the global glob's IO
RuntimeGlob sourceIO = GlobalVariable.getGlobalIO(globName);
RuntimeGlob targetIO = GlobalVariable.getGlobalIO(this.globName);
this.IO = sourceIO.IO;
targetIO.IO = sourceIO.IO;

// Alias the ARRAY slot: both names point to the same RuntimeArray object
RuntimeArray sourceArray = GlobalVariable.getGlobalArray(globName);
Expand Down Expand Up @@ -503,22 +544,25 @@ public String toStringRef() {

/**
* Returns an integer representation of the typeglob reference.
* This is the hash code of the current instance.
* This is the unsigned interpretation of the hash code.
* Note: This may overflow for hash codes > Integer.MAX_VALUE, but
* getDoubleRef() returns the correct unsigned value.
*
* @return The hash code of this instance.
* @return The hash code of this instance as unsigned (may overflow).
*/
public int getIntRef() {
return this.hashCode();
}

/**
* Returns a double representation of the typeglob reference.
* This is the hash code of the current instance, cast to a double.
* This is the unsigned interpretation of the hash code, matching what
* hex() would return from the stringified address in toStringRef().
*
* @return The hash code of this instance as a double.
* @return The hash code as an unsigned value (as double).
*/
public double getDoubleRef() {
return this.hashCode();
return Integer.toUnsignedLong(this.hashCode());
}

/**
Expand Down
54 changes: 48 additions & 6 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -768,18 +768,37 @@ public static void closeAllHandles() {
/**
* Extracts a RuntimeIO from various Perl scalar types.
*
* <p>Handles:
* <p>This method handles several common filehandle representations:
* <ul>
* <li>Direct I/O handles</li>
* <li>Glob references (\*STDOUT)</li>
* <li>Globs (*STDOUT)</li>
* <li>Direct I/O handles (RuntimeIO stored directly in the scalar)</li>
* <li>Glob references (\*STDOUT) - dereferences to get the glob's IO slot</li>
* <li>Globs (*STDOUT) - accesses the glob's IO slot directly</li>
* <li>Symbolic names ("STDOUT") - looks up the global handle by name</li>
* </ul>
*
* <p><b>IMPORTANT for detached glob copies:</b> When a glob is captured via
* {@code do { local *FH; *FH }}, the returned glob is a "detached copy" created
* by {@link RuntimeGlob#createDetachedCopy()}. This copy has its own IO slot
* that is independent of the global *FH after the local scope ends. This method
* must extract the IO from the detached copy's own IO slot, NOT from the global
* handle looked up by name. The fallback lookup by globName (lines 820-828) should
* only be used when the glob's own IO slot is genuinely null/empty, not just
* because it contains an empty RuntimeScalar.
*
* @param runtimeScalar the scalar containing or referencing an I/O handle
* @return the extracted RuntimeIO
* @return the extracted RuntimeIO, or null if no IO handle found
*/
public static RuntimeIO getRuntimeIO(RuntimeScalar runtimeScalar) {
RuntimeIO fh = null;
boolean ioDebug = System.getenv("JPERL_IO_DEBUG") != null;

if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO ENTRY: type=" + runtimeScalar.type +
" valueClass=" + (runtimeScalar.value != null ? runtimeScalar.value.getClass().getSimpleName() : "null") +
" valueId=" + (runtimeScalar.value != null ? System.identityHashCode(runtimeScalar.value) : 0));
System.err.flush();
}

// Handle: my $fh2 = \*STDOUT;
// Handle: my $fh = *STDOUT;

Expand Down Expand Up @@ -813,11 +832,25 @@ public static RuntimeIO getRuntimeIO(RuntimeScalar runtimeScalar) {

if (runtimeScalar.value instanceof RuntimeGlob runtimeGlob) {
RuntimeScalar ioScalar = runtimeGlob.getIO();
if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO: glob=" + runtimeGlob.globName +
" globId=" + System.identityHashCode(runtimeGlob) +
" ioScalar=" + (ioScalar != null ? ioScalar.type + "/" + System.identityHashCode(ioScalar) : "null") +
" ioValue=" + (ioScalar != null ? (ioScalar.value != null ? ioScalar.value.getClass().getSimpleName() + "/" + System.identityHashCode(ioScalar.value) : "null") : "N/A"));
System.err.flush();
}
if (ioScalar != null) {
fh = ioScalar.getRuntimeIO();
}
// If the glob's IO part is null, try to look up the global handle by name
// If the glob's IO part is null, try to look up the global handle by name.
// IMPORTANT: This fallback should only be used when the detached copy's own
// IO slot genuinely has no IO handle. For the `do { local *FH; *FH }` pattern,
// the detached copy's IO slot IS the correct place to look.
if (fh == null && runtimeGlob.globName != null) {
if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO: fallback lookup for " + runtimeGlob.globName);
System.err.flush();
}
RuntimeGlob globalGlob = GlobalVariable.getGlobalIO(runtimeGlob.globName);
if (globalGlob != null) {
RuntimeScalar globalIoScalar = globalGlob.getIO();
Expand All @@ -828,9 +861,18 @@ public static RuntimeIO getRuntimeIO(RuntimeScalar runtimeScalar) {
}
} else if (runtimeScalar.value instanceof RuntimeIO runtimeIO) {
// Direct I/O handle
if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO: found direct RuntimeIO id=" + System.identityHashCode(runtimeIO));
System.err.flush();
}
fh = runtimeIO;
}

if (ioDebug) {
System.err.println("[JPERL_IO_DEBUG] getRuntimeIO EXIT: fh=" + (fh != null ? System.identityHashCode(fh) : "null"));
System.err.flush();
}

if (fh == null) {
// Check if object is eligible for overloading `*{}`
int blessId = RuntimeScalarType.blessedId(runtimeScalar);
Expand Down
Loading
Loading