Skip to content
Merged
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/frontend/parser/FileHandle.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ else if (token.type == LexerTokenType.IDENTIFIER) {
if (fileHandle == null
&& name.matches("^[A-Z_][A-Z0-9_]*$")
&& !isDoubleUnderscoreMagicBareword(name)) {
GlobalVariable.getGlobalIO(normalizeBarewordHandle(parser, name));
GlobalVariable.vivifyGlobalIO(normalizeBarewordHandle(parser, name));
fileHandle = parseBarewordHandle(parser, name);
}
}
Expand Down Expand Up @@ -331,4 +331,4 @@ public static String normalizeBarewordHandle(Parser parser, String name) {
private static boolean isDoubleUnderscoreMagicBareword(String name) {
return name.length() >= 4 && name.startsWith("__") && name.endsWith("__");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ static Node parseDiamondOperator(Parser parser, LexerToken token) {
parser.tokenIndex++; // consume NUMBER
TokenUtils.consume(parser); // consume '>'
String digitName = operand.text;
GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, digitName));
GlobalVariable.vivifyGlobalIO(FileHandle.normalizeBarewordHandle(parser, digitName));
Node globRef = FileHandle.parseBarewordHandle(parser, digitName);
if (globRef != null) {
BinaryOperatorNode readlineNode = new BinaryOperatorNode("readline",
Expand Down Expand Up @@ -710,7 +710,7 @@ public static OperatorNode parseSelect(Parser parser, LexerToken token, int curr
// select FILEHANDLE
if (listNode1.elements.getFirst() instanceof IdentifierNode identifierNode) {
// Autovivify the filehandle IO slot so parseBarewordHandle succeeds
GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, identifierNode.name));
GlobalVariable.vivifyGlobalIO(FileHandle.normalizeBarewordHandle(parser, identifierNode.name));
Node handle = FileHandle.parseBarewordHandle(parser, identifierNode.name);
if (handle != null) {
// handle is Bareword
Expand Down Expand Up @@ -979,7 +979,7 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex)
if (name.matches("^[A-Z_][A-Z0-9_]*$")) {
TokenUtils.consume(parser);
// autovivify filehandle and convert to globref
GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, name));
GlobalVariable.vivifyGlobalIO(FileHandle.normalizeBarewordHandle(parser, name));
Node fh = FileHandle.parseBarewordHandle(parser, name);
Node operand = fh != null ? fh : new IdentifierNode(name, parser.tokenIndex);
if (paren) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ private static void handleScalarArgument(Parser parser, ListNode args, boolean i
}
// Not a known filehandle, but still allow bareword (will be treated as filename string)
// Autovivify the filehandle in case it's used later
GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, idNode.name));
GlobalVariable.vivifyGlobalIO(FileHandle.normalizeBarewordHandle(parser, idNode.name));
}
}
Node scalarArg = ParserNodeUtils.toScalarContext(arg);
Expand Down Expand Up @@ -740,7 +740,7 @@ private static int handleTypeGlobArgument(Parser parser, ListNode args, boolean
// Builtin bareword filehandle - create a typeglob reference

// autovivify the bareword handle
GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, idNode.name));
GlobalVariable.vivifyGlobalIO(FileHandle.normalizeBarewordHandle(parser, idNode.name));

Node typeglobRef = FileHandle.parseBarewordHandle(parser, idNode.name);
args.elements.add(typeglobRef == null ? expr : typeglobRef);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/frontend/parser/TokenUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public static LexerToken consume(Parser parser, LexerTokenType type) {
LexerToken token = consume(parser);
if (token.type != type) {
throw new PerlCompilerException(
parser.tokenIndex, "Expected token " + type + " but got " + token, parser.ctx.errorUtil);
parser.tokenIndex, "syntax error", parser.ctx.errorUtil);
}
return token;
}
Expand All @@ -155,7 +155,7 @@ public static void consume(Parser parser, LexerTokenType type, String text) {
if (token.type != type || !token.text.equals(text)) {
throw new PerlCompilerException(
parser.tokenIndex,
"Expected token " + type + " with text " + text + " but got " + token,
"syntax error",
parser.ctx.errorUtil);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
targetGlob = GlobalVariable.getGlobalIO(filehandleName);
}
if (targetGlob != null) {
targetGlob.setIO(oneFh);
targetGlob.openIO(oneFh);
// If args[0] is a writable scalar (not readonly), update it to point
// at the glob. We must NOT call set() on a readonly scalar (e.g. when
// args[0] is a numeric literal like in `open 0`).
Expand Down Expand Up @@ -851,7 +851,7 @@ else if (secondArg.type == RuntimeScalarType.GLOB || secondArg.type == RuntimeSc
" ioHandleId=" + (fh != null && fh.ioHandle != null ? System.identityHashCode(fh.ioHandle) : 0));
System.err.flush();
}
targetGlob.setIO(fh);
targetGlob.openIO(fh);
} else {
// Create a new anonymous GLOB and assign it to the lvalue
RuntimeScalar newGlob = new RuntimeScalar();
Expand Down Expand Up @@ -1540,7 +1540,7 @@ public static RuntimeScalar sysopen(int ctx, RuntimeBase... args) {
}

if (targetGlob != null) {
targetGlob.setIO(fh);
targetGlob.openIO(fh);
} else {
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
Expand Down
37 changes: 30 additions & 7 deletions src/main/java/org/perlonjava/runtime/operators/TieOperators.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.perlonjava.runtime.operators;

import org.perlonjava.runtime.mro.InheritanceResolver;
import org.perlonjava.runtime.runtimetypes.*;

import java.util.Arrays;

import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable;
import static org.perlonjava.runtime.runtimetypes.RuntimeArray.TIED_ARRAY;
import static org.perlonjava.runtime.runtimetypes.RuntimeHash.TIED_HASH;
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue;
Expand Down Expand Up @@ -64,13 +66,15 @@ public static RuntimeScalar tie(int ctx, RuntimeBase... scalars) {
// (not a string). This matches Perl's tie() behavior where `tie *$obj, $obj`
// passes the blessed object as $_[0] to TIEHANDLE.
RuntimeScalar invocant = blessId != 0 ? classArg : new RuntimeScalar(className);
RuntimeScalar self = RuntimeCode.call(
invocant,
new RuntimeScalar(method),
null,
args,
RuntimeContextType.SCALAR
).getFirst();
RuntimeScalar self = blessId != 0
? RuntimeCode.call(
invocant,
new RuntimeScalar(method),
null,
args,
RuntimeContextType.SCALAR
).getFirst()
: callTieConstructor(className, method, args);

switch (variable.type) {
case REFERENCE -> {
Expand Down Expand Up @@ -132,6 +136,25 @@ public static RuntimeScalar tie(int ctx, RuntimeBase... scalars) {
return self;
}

private static RuntimeScalar callTieConstructor(String className, String methodName, RuntimeArray args) {
args.elements.addFirst(new RuntimeScalar(className));

RuntimeScalar method = InheritanceResolver.findMethodInHierarchy(methodName, className, null, 0);
if (method == null) {
throw new PerlCompilerException("Can't locate object method \"" + methodName
+ "\" via package \"" + className + "\" (perhaps you forgot to load \""
+ className + "\"?)");
}

String autoloadVariableName = ((RuntimeCode) method.value).autoloadVariableName;
if (autoloadVariableName != null && !methodName.equals("AUTOLOAD")) {
getGlobalVariable(autoloadVariableName).set(
NameNormalizer.normalizeVariableName(methodName, className));
}

return RuntimeCode.apply(method, args, RuntimeContextType.SCALAR).getFirst();
}

/**
* Implements Perl's untie() builtin function.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,14 @@ yield switch (inner.type) {
case ARRAYREFERENCE -> "ARRAY";
case HASHREFERENCE -> "HASH";
case CODE -> "CODE";
case GLOB, GLOBREFERENCE -> "GLOB";
case GLOB -> {
if (scalar.value instanceof RuntimeIO) yield "IO";
yield null;
}
case GLOBREFERENCE -> {
if (scalar.value instanceof RuntimeIO) yield "IO";
yield "GLOB";
}
case FORMAT -> "FORMAT";
case REGEX -> "REGEXP";
default -> null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,21 @@ public static RuntimeGlob getGlobalIO(String key) {
return glob;
}

/**
* Vivifies the IO slot for a bareword filehandle seen at compile time.
* Generic glob creation must not define {@code *name{IO}}, but Perl creates
* a PVIO object when it parses a bareword filehandle argument such as
* {@code open FH, ...}. BEGIN blocks can observe that placeholder before
* the runtime open call installs the real handle.
*/
public static RuntimeGlob vivifyGlobalIO(String key) {
RuntimeGlob glob = getGlobalIO(key);
if (glob.IO == null || glob.IO.type == RuntimeScalarType.UNDEF || glob.IO.value == null) {
glob.setIO(new RuntimeIO());
}
return glob;
}

/**
* Peek at a glob entry without vivifying it. Returns null if no glob has
* been registered under this name. Used by anon-sub naming lookups
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,26 @@ && followGlobalCodeCaptures(code, target, seen, todo)) {
return false;
}

public static boolean hasLiveStrongScalarReferent(RuntimeBase target) {
if (target == null) return false;
for (Object liveVar : MyVarCleanupStack.snapshotLiveVars()) {
if (liveVar instanceof RuntimeScalar sc
&& !WeakRefRegistry.isweak(sc)
&& !sc.scopeExited
&& sc.value == target) {
return true;
}
}
for (RuntimeScalar sc : ScalarRefRegistry.snapshot()) {
if (sc == null) continue;
if (WeakRefRegistry.isweak(sc)) continue;
if (sc.scopeExited) continue;
if (!MyVarCleanupStack.isLive(sc)) continue;
if (sc.value == target) return true;
}
return false;
}

public static boolean isReachableFromGlobalCodeCaptures(RuntimeBase target) {
if (target == null) return false;
Set<RuntimeBase> seen = Collections.newSetFromMap(new IdentityHashMap<>());
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,22 @@ public RuntimeGlob setIO(RuntimeIO io) {
return this;
}

public RuntimeGlob openIO(RuntimeIO io) {
if (this.IO != null
&& this.IO.type != RuntimeScalarType.TIED_SCALAR
&& this.IO.value instanceof RuntimeIO existingIO
&& existingIO != io) {
RuntimeIO oldSelected = existingIO == RuntimeIO.selectedHandle ? existingIO : null;
existingIO.replaceStateFrom(io);
existingIO.globName = this.globName;
if (oldSelected != null) {
RuntimeIO.selectedHandle = existingIO;
}
return this;
}
return setIO(io);
}

/**
* Counts the number of elements in the typeglob.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,30 @@ public RuntimeIO(DirectoryIO directoryIO) {
this.directoryIO = directoryIO;
}

/**
* Replace this handle's runtime state with another handle while preserving
* object identity. Perl keeps the PVIO object stable across {@code open FH}
* on an already-vivified bareword handle, so values captured by
* {@code *FH{IO}} before the open see the newly opened stream.
*/
public void replaceStateFrom(RuntimeIO other) {
if (other == null || other == this) {
return;
}

Integer fd = ioToFileno.remove(other);
if (fd != null) {
ioToFileno.put(this, fd);
filenoToIO.put(fd, this);
}

this.currentLineNumber = other.currentLineNumber;
this.ioHandle = other.ioHandle;
this.directoryIO = other.directoryIO;
this.needFlush = other.needFlush;
this.autoFlush = other.autoFlush;
}

/**
* Checks if this handle is in byte mode (no encoding layers).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,7 @@ public RuntimeGlob globDeref() {
// PCS (Proxy Constant Subroutine) creation should only happen via direct
// stash hash assignment ($stash->{name} = \$scalar), handled by RuntimeStashEntry.set().
if (value instanceof RuntimeStashEntry stashEntry) {
yield new RuntimeGlob(stashEntry.globName);
yield GlobalVariable.getGlobalIO(stashEntry.globName);
}
yield (RuntimeGlob) value;
}
Expand Down Expand Up @@ -2404,7 +2404,7 @@ public RuntimeGlob globDerefNonStrict(String packageName) {
// When glob-dereferencing a stash entry, return a plain RuntimeGlob.
// This prevents *{$stash->{name}} = \$scalar from creating PCS constant subs.
if (value instanceof RuntimeStashEntry stashEntry) {
yield new RuntimeGlob(stashEntry.globName);
yield GlobalVariable.getGlobalIO(stashEntry.globName);
}
yield (RuntimeGlob) value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,12 @@ public RuntimeGlob createDetachedCopy() {
*/
@Override
public RuntimeGlob globDeref() {
RuntimeGlob pure = new RuntimeGlob(this.globName);
pure.IO = this.IO;
return pure;
return GlobalVariable.getGlobalIO(this.globName);
}

@Override
public RuntimeGlob globDerefNonStrict(String packageName) {
RuntimeGlob pure = new RuntimeGlob(this.globName);
pure.IO = this.IO;
return pure;
return GlobalVariable.getGlobalIO(this.globName);
}

// Note on Stash Operations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,13 @@ public static void weaken(RuntimeScalar ref) {
ref.refCountOwned = false;
base.refCount = WEAKLY_TRACKED;
}
boolean shouldSweepLiveCodeRef = weakenedLiveCodeRef
&& codeRefHasCountedOwners(base)
&& !ModuleInitGuard.inModuleInit();
if (base instanceof RuntimeCode code
&& code.refCount >= 0
&& weakRefsExist
&& ((weakenedLiveCodeRef && !ModuleInitGuard.inModuleInit())
&& (shouldSweepLiveCodeRef
|| (code.hadStashRef
&& code.stashRefCount <= 0
&& !isInstalledGlobalCodeRef(code)))) {
Expand All @@ -205,6 +208,10 @@ public static void weaken(RuntimeScalar ref) {
}
}

private static boolean codeRefHasCountedOwners(RuntimeBase base) {
return base.refCount > 0 || base.activeOwnerCount() > 0;
}

/**
* Check if a RuntimeScalar is a weak reference.
*/
Expand Down Expand Up @@ -295,6 +302,7 @@ private static boolean isInstalledGlobalCodeRef(RuntimeCode code) {

private static boolean shouldKeepCodeWeakRefs(RuntimeCode code) {
if (code.stashRefCount > 0 || isInstalledGlobalCodeRef(code)) return true;
if (ReachabilityWalker.hasLiveStrongScalarReferent(code)) return true;
return RuntimeCode.isActiveCode(code);
}

Expand Down
8 changes: 8 additions & 0 deletions src/test/resources/unit/parser_syntax_error.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use strict;
use warnings;
use Test::More;

eval q{sub parser_syntax_error_probe { if }};
like($@, qr/\Asyntax error at /, 'unexpected token reports Perl syntax error');

done_testing();
22 changes: 22 additions & 0 deletions src/test/resources/unit/refcount/weaken_closure_argument.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env perl
use strict;
use warnings;

use Scalar::Util qw(isweak weaken);
use Test::More tests => 3;

sub run_callback {
my $callback = shift;
return weaken_and_call($callback);
}

sub weaken_and_call {
weaken(my $weak_callback = shift);

ok(isweak($weak_callback), 'closure argument copy is weak');
ok(defined $weak_callback, 'weak closure argument copy survives while caller holds it');
is($weak_callback->(), 'captured value', 'weak closure argument remains callable');
}

my $captured = 'captured value';
run_callback(sub { $captured });
Loading
Loading