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
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,23 @@ public void visit(BlockNode node) {
// Visit each statement in the block
int numStatements = node.elements.size();

// Bare/labeled blocks marked as `isLoop=true` (synthesized by the parser
// for things like `given { ... }`, `eval { ... }`, etc.) act as a single-
// iteration loop target so that `last/next/redo` inside the block stay
// inside the block instead of escaping to the enclosing real loop.
// The JVM backend handles this via EmitBlock; the interpreter backend
// must do the same here.
LoopInfo blockLoopInfo = null;
int blockLoopStartPc = -1;
if (node.isLoop) {
blockLoopStartPc = bytecode.size();
// For a bare block, `node.labelName` is null and the block is a
// valid target for unlabeled last/next/redo (matches JVM
// EmitBlock's pushLoopLabels(... isBareBlock, isBareBlock)).
blockLoopInfo = new LoopInfo(node.labelName, blockLoopStartPc, true);
loopStack.push(blockLoopInfo);
}

int lastMeaningfulIndex = -1;
for (int i = numStatements - 1; i >= 0; i--) {
Node elem = node.elements.get(i);
Expand Down Expand Up @@ -1170,6 +1187,23 @@ public void visit(BlockNode node) {
emitReg(outerResultReg);
}

// Patch last/next/redo PCs for blocks marked isLoop=true.
// last/next jump to here (end of body, before exit-scope cleanup so locals are restored).
// redo jumps back to the start of the body.
if (blockLoopInfo != null) {
int blockEndPc = bytecode.size();
for (int pc : blockLoopInfo.breakPcs) {
patchJump(pc, blockEndPc);
}
for (int pc : blockLoopInfo.nextPcs) {
patchJump(pc, blockEndPc);
}
for (int pc : blockLoopInfo.redoPcs) {
patchJump(pc, blockLoopStartPc);
}
loopStack.pop();
}

if (regexSaveReg >= 0) {
emit(Opcodes.RESTORE_REGEX_STATE);
emitReg(regexSaveReg);
Expand Down
4 changes: 2 additions & 2 deletions 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 = "846d4d854";
public static final String gitCommitId = "46b0ccb80";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -48,7 +48,7 @@ public final class Configuration {
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String buildTimestamp = "Apr 28 2026 17:18:57";
public static final String buildTimestamp = "Apr 28 2026 18:01:32";

// Prevent instantiation
private Configuration() {
Expand Down
58 changes: 43 additions & 15 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -558,28 +558,56 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
RuntimeScalar fileHandle = (RuntimeScalar) args[0];
if (args.length < 2) {
// 1-argument open: open FILEHANDLE
// Uses $_ as the filename (with embedded mode prefix parsed from it)
String fileName = getGlobalVariable("main::_").toString();
// Per Perl semantics, the global scalar variable of the same name as the
// filehandle holds the filename (which may include a leading mode prefix).
// For example: `open MYFH` reads filename from $main::MYFH, `open 0` reads
// from $main::0 (which is the script name $0).
String filehandleName = null;
RuntimeGlob existingGlob = null;
if ((fileHandle.type == RuntimeScalarType.GLOB || fileHandle.type == RuntimeScalarType.GLOBREFERENCE) && fileHandle.value instanceof RuntimeGlob glob) {
existingGlob = glob;
filehandleName = glob.globName;
} else {
// Otherwise, derive the name from the scalar value. This covers both
// bareword-as-string ("MYFH") and constants like `open 0` where the
// filehandle is named "0".
String name = fileHandle.toString();
if (name != null && !name.isEmpty()) {
filehandleName = name.contains("::") ? name : ("main::" + name);
}
}

// Resolve the filename from the global scalar of the same name.
String fileName = filehandleName != null
? getGlobalVariable(filehandleName).toString()
: "";
RuntimeIO oneFh = RuntimeIO.open(fileName);
if (oneFh == null) {
return scalarUndef;
}
// Assign the IO handle to the filehandle glob (reuse the existing assignment logic below)
RuntimeGlob targetGlob = null;
if ((fileHandle.type == RuntimeScalarType.GLOB || fileHandle.type == RuntimeScalarType.GLOBREFERENCE) && fileHandle.value instanceof RuntimeGlob glob) {
targetGlob = glob;
} else if ((fileHandle.type == RuntimeScalarType.STRING || fileHandle.type == RuntimeScalarType.BYTE_STRING) && fileHandle.value instanceof String name) {
if (!name.isEmpty() && name.matches("^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$")) {
String fullName = name.contains("::") ? name : ("main::" + name);
targetGlob = GlobalVariable.getGlobalIO(fullName);
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
newGlob.value = targetGlob;
fileHandle.set(newGlob);
}

RuntimeGlob targetGlob = existingGlob;
if (targetGlob == null && filehandleName != null) {
targetGlob = GlobalVariable.getGlobalIO(filehandleName);
}
if (targetGlob != null) {
targetGlob.setIO(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`).
if (!(fileHandle instanceof RuntimeScalarReadOnly)
&& fileHandle.type != RuntimeScalarType.GLOB
&& fileHandle.type != RuntimeScalarType.GLOBREFERENCE) {
try {
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
newGlob.value = targetGlob;
fileHandle.set(newGlob);
} catch (RuntimeException ignored) {
// Read-only / unsettable scalar - the IO has already been
// registered on the global glob, so callers can find it by name.
}
}
} else {
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
Expand Down
62 changes: 62 additions & 0 deletions src/main/perl/lib/POSIX.pm
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ our @EXPORT_OK = qw(

# Constants - access (for access() function)
F_OK R_OK W_OK X_OK

# Constants - termios (termios_h)
BRKINT
CS5 CS6 CS7 CS8 CSIZE CSTOPB CREAD PARENB PARODD HUPCL CLOCAL
ECHO ECHOE ECHOK ECHONL
ICANON IEXTEN ISIG
ICRNL INPCK ISTRIP IXON IXOFF IGNBRK IGNCR IGNPAR INLCR IXANY PARMRK
OPOST
TCSADRAIN TCSAFLUSH TCSANOW
VEOF VEOL VERASE VINTR VKILL VMIN VQUIT VSTART VSTOP VSUSP VTIME

# Constants - sysconf (subset, used by POE etc.)
_SC_ARG_MAX _SC_CHILD_MAX _SC_CLK_TCK _SC_NGROUPS_MAX _SC_OPEN_MAX
_SC_JOB_CONTROL _SC_SAVED_IDS _SC_VERSION _SC_PAGESIZE _SC_PAGE_SIZE
_SC_NPROCESSORS_CONF _SC_NPROCESSORS_ONLN
);

our %EXPORT_TAGS = (
Expand Down Expand Up @@ -575,6 +590,53 @@ for my $const (qw(
*{$const} = eval "sub () { POSIX::_const_$const() }";
}

# sysconf() variable name constants and stub implementation.
# Real POSIX sysconf() returns system-dependent runtime limits. PerlOnJava
# does not implement true sysconf(), but many CPAN modules (POE, Proc::Daemon,
# etc.) call sysconf(_SC_OPEN_MAX) etc. for sensible defaults. Provide the
# common _SC_* names as constants and have sysconf() return reasonable values.
BEGIN {
my %sc = (
_SC_ARG_MAX => 0,
_SC_CHILD_MAX => 1,
_SC_CLK_TCK => 2,
_SC_NGROUPS_MAX => 3,
_SC_OPEN_MAX => 4,
_SC_JOB_CONTROL => 5,
_SC_SAVED_IDS => 6,
_SC_VERSION => 7,
_SC_PAGESIZE => 8,
_SC_PAGE_SIZE => 8, # alias of _SC_PAGESIZE
_SC_NPROCESSORS_CONF => 9,
_SC_NPROCESSORS_ONLN => 10,
);
no strict 'refs';
for my $name (keys %sc) {
my $value = $sc{$name};
*{"POSIX::$name"} = sub () { $value };
}
}

sub sysconf {
my $name = shift;
return undef unless defined $name;
if ($name == 0) { return 4096 * 1024; } # _SC_ARG_MAX
elsif ($name == 1) { return 1024; } # _SC_CHILD_MAX
elsif ($name == 2) { return 100; } # _SC_CLK_TCK
elsif ($name == 3) { return 16; } # _SC_NGROUPS_MAX
elsif ($name == 4) { return 1024; } # _SC_OPEN_MAX
elsif ($name == 5) { return 1; } # _SC_JOB_CONTROL
elsif ($name == 6) { return 1; } # _SC_SAVED_IDS
elsif ($name == 7) { return 200809; } # _SC_VERSION
elsif ($name == 8) { return 4096; } # _SC_PAGESIZE
elsif ($name == 9 || $name == 10) { # _SC_NPROCESSORS_*
my $n = eval { 0 + (`getconf _NPROCESSORS_ONLN 2>/dev/null` || 1) };
$n = 1 if !$n || $n < 1;
return $n;
}
return undef;
}

# Locale category constants - defined directly since XS _const_ may not exist
BEGIN {
my %lc = (
Expand Down
Loading