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
52 changes: 30 additions & 22 deletions dev/design/cpan_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,38 +581,46 @@ DateTime.pm
| Issue | Module Affected | Status |
|-------|-----------------|--------|
| ~~`${ $stash{NAME} }` dereference~~ | ~~Module::Implementation~~ | **FIXED** |
| Package::Stash::PP doesn't return true | Package::Stash | **BUG - needs investigation** |
| evalContext null in parallel tests | Test2::API INIT blocks | **FIXED** |
| Bare block return value at file level | Package::Stash::PP | **PARTIAL - TODO tests added** |

### Proposed Solutions
### evalContext Issue Resolution (2026-03-19)

#### Option A: Fix Package::Stash::PP (Recommended)
The JUnit parallel test failures ("ctx is null" errors) were caused by stale INIT blocks referencing cleared evalContext entries.

Investigate why Package::Stash::PP.pm doesn't return a true value.
**Root cause:**
1. Test A loads Test2::API which has `INIT { eval '...' }`
2. The INIT block's compiled bytecode references a specific evalTag
3. `resetAll()` clears evalContext but INIT blocks persisted
4. Test B triggers the stale INIT block → evalContext.get() returns null → NPE

**Files to investigate:**
- `~/.perlonjava/lib/Package/Stash/PP.pm` - Check for compilation errors
**Fix applied:**
1. Added null check in `RuntimeCode.evalStringHelper()` with descriptive error
2. Clear special blocks (INIT/END/CHECK) in `GlobalVariable.resetAllGlobals()`
3. Updated JUnit test harness to skip TODO tests (`# TODO` in TAP output)

#### Option B: Create Bundled DateTime.pm
### Bare Block Return Value Issue

Create a simplified `DateTime.pm` in `src/main/perl/lib/` that:
1. Skips heavy dependencies (namespace::autoclean, Specio)
2. Uses our Java XS implementation or DateTime::PP
3. Provides core DateTime functionality
**Problem:** Files ending with a bare block `{ ... }` return `undef` instead of the block's last expression value.

### Investigation Commands
**Example:**
```perl
# File ends with:
{ 42; }
# do "file" returns undef in PerlOnJava, 42 in Perl
```

```bash
# Test symbol table dereference (NOW FIXED)
./jperl -e 'package Foo; our $VERSION = "1.0"; print ${ $Foo::{VERSION} }, "\n"'
# Output: 1.0 (correct)
**Attempted fix:** Change context for bare blocks in RUNTIME context (EmitBlock.java/EmitStatement.java)

# Test DateTime - still blocked by namespace::autoclean
./jperl -MDateTime -e 'print DateTime->now->ymd, "\n"'
# Error: Can't locate namespace/autoclean.pm
```
**Result:** Causes ASM stack frame verification failures because changing context for the block affects bytecode generation for ALL statements inside it, not just the return value capture.

**Current status:** Tests for this feature are marked as TODO. The fix requires a more sophisticated approach that captures the return value without changing how interior statements compile.

**Files changed:**
- `src/test/resources/unit/bare_block_return.t` - TODO tests for bare block return values

### Next Steps

1. **Install namespace::autoclean dependencies** via jcpan to test further
2. **If blocked by Package::Stash::PP**, investigate why modules ending with `sub foo { }` return undef
1. **Investigate bare block return value fix** - Need to capture last expression value without changing interior statement compilation
2. **Test Package::Stash::PP loading** - May need explicit `1;` at end of file
3. **Alternative**: Create bundled DateTime.pm wrapper that skips heavy dependencies
12 changes: 11 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,21 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) {

ByteCodeSourceMapper.setDebugInfoLineNumber(emitterVisitor.ctx, element.getIndex());

// Check if this block should store its result in a register (for bare block expressions)
Object resultRegObj = node.getAnnotation("resultRegister");
int resultReg = (resultRegObj instanceof Integer) ? (Integer) resultRegObj : -1;

// Emit the statement with current context
if (i == lastNonNullIndex) {
// Special case for the last element
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("Last element: " + element);
element.accept(emitterVisitor);
if (resultReg >= 0) {
// Visit in SCALAR context to get a value, store it, then pop
element.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
mv.visitVarInsn(Opcodes.ASTORE, resultReg);
} else {
element.accept(emitterVisitor);
}
} else {
// General case for all other elements
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("Element: " + element);
Expand Down
30 changes: 28 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,29 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) {
isUnlabeledTarget);

// Visit the loop body
node.body.accept(voidVisitor);
// For simple blocks (bare blocks like { ... }) in non-void context,
// use register spilling to capture the result: allocate a local variable,
// tell the block to store its last element's value there, then load it after.
// This ensures consistent stack state across all code paths.
// Only apply for SCALAR/LIST contexts, not RUNTIME (which is for subroutine bodies).
boolean needsReturnValue = node.isSimpleBlock
&& (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR
|| emitterVisitor.ctx.contextType == RuntimeContextType.LIST);
if (needsReturnValue) {
// Allocate a local variable for the result
int resultReg = emitterVisitor.ctx.symbolTable.allocateLocalVariable();
// Initialize it to undef
EmitOperator.emitUndef(mv);
mv.visitVarInsn(Opcodes.ASTORE, resultReg);
// Tell the block to store its last element's value in this register
node.body.setAnnotation("resultRegister", resultReg);
// Visit body in VOID context (consistent stack state)
node.body.accept(voidVisitor);
// Load the result
mv.visitVarInsn(Opcodes.ALOAD, resultReg);
} else {
node.body.accept(voidVisitor);
}

} else {
// Within a `while` modifier, next/redo/last labels are not active
Expand Down Expand Up @@ -228,7 +250,11 @@ public static void emitFor3(EmitterVisitor emitterVisitor, For3Node node) {
}

// If the context is not VOID, push "undef" to the stack
if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) {
// Exception: for simple blocks in SCALAR/LIST context, we already loaded the result from the register
boolean simpleBlockHandled = node.isSimpleBlock
&& (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR
|| emitterVisitor.ctx.contextType == RuntimeContextType.LIST);
if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID && !simpleBlockHandled) {
EmitOperator.emitUndef(emitterVisitor.ctx.mv);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ public static void resetAllGlobals() {

RuntimeCode.clearCaches();

// Clear special blocks (INIT, END, CHECK, UNITCHECK) to prevent stale code references.
// When the classloader is replaced, old INIT blocks may reference evalTags that no longer
// exist in the cleared evalContext, causing "ctx is null" errors.
SpecialBlock.initBlocks.elements.clear();
SpecialBlock.endBlocks.elements.clear();
SpecialBlock.checkBlocks.elements.clear();

// Method resolution caches can grow across test scripts.
InheritanceResolver.invalidateCache();

Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,19 @@ public static Class<?> evalStringHelper(RuntimeScalar code, String evalTag, Obje
// Retrieve the eval context that was saved at program compile-time
EmitterContext ctx = RuntimeCode.evalContext.get(evalTag);

// Handle missing eval context - this can happen when compiled code (e.g., INIT blocks
// with eval) is executed after the runtime has been reset. In JUnit parallel tests,
// PerlLanguageProvider.resetAll() clears evalContext between tests, but compiled
// bytecode (which references specific evalTags) survives and may be re-executed.
if (ctx == null) {
throw new RuntimeException(
"Eval context not found for tag: " + evalTag +
". This can happen when eval is called from code that was compiled " +
"in a previous session (e.g., INIT blocks in cached modules). " +
"The module may need to be reloaded. " +
"If this occurs in tests, ensure module caches are cleared along with eval contexts.");
}

// Save the current scope so we can restore it after eval compilation.
// This is critical because eval may be called from code compiled with different
// warning/feature flags than the caller, and we must not leak the eval's scope.
Expand Down Expand Up @@ -818,6 +831,19 @@ public static RuntimeList evalStringWithInterpreter(
// Retrieve the eval context that was saved at program compile-time
EmitterContext ctx = RuntimeCode.evalContext.get(evalTag);

// Handle missing eval context - this can happen when compiled code (e.g., INIT blocks
// with eval) is executed after the runtime has been reset. In JUnit parallel tests,
// PerlLanguageProvider.resetAll() clears evalContext between tests, but compiled
// bytecode (which references specific evalTags) survives and may be re-executed.
if (ctx == null) {
throw new RuntimeException(
"Eval context not found for tag: " + evalTag +
". This can happen when eval is called from code that was compiled " +
"in a previous session (e.g., INIT blocks in cached modules). " +
"The module may need to be reloaded. " +
"If this occurs in tests, ensure module caches are cleared along with eval contexts.");
}

// Save the current scope so we can restore it after eval execution.
// This is critical because eval may be called from code compiled with different
// warning/feature flags than the caller, and we must not leak the eval's scope.
Expand Down
18 changes: 18 additions & 0 deletions src/main/perl/lib/ExtUtils/MakeMaker.pm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ require ExtUtils::MM;
# Installation directory (configurable via environment)
our $INSTALL_BASE = $ENV{PERLONJAVA_LIB};

# Parse command-line arguments like INSTALL_BASE=/path
# This is called by Makefile.PL scripts that use MY->parse_args(@ARGV)
sub parse_args {
my ($self, @args) = @_;
$self = {} unless ref $self;
foreach (@args) {
next unless m/(.*?)=(.*)/;
my ($name, $value) = ($1, $2);
# Expand ~ in paths
if ($value =~ m/^~/) {
my $home = $ENV{HOME} || $ENV{USERPROFILE} || '.';
$value =~ s/^~/$home/;
}
$self->{ARGS}{uc $name} = $self->{uc $name} = $value;
}
return $self;
}

# Find the default lib directory
sub _default_install_base {
# Check if running from JAR
Expand Down
35 changes: 35 additions & 0 deletions src/main/perl/lib/Package/Stash/Conflicts.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package Package::Stash::Conflicts;
use strict;
use warnings;

our $VERSION = '0.40';

# PerlOnJava stub - conflict checking not needed
#
# The original module uses Dist::CheckConflicts to verify there are no
# conflicting versions of Package::Stash with old versions of:
# - Class::MOP 1.08
# - MooseX::Method::Signatures 0.36
# - MooseX::Role::WithOverloading 0.08
# - namespace::clean 0.18
#
# In PerlOnJava's environment, these old conflicting versions don't exist,
# so we provide a no-op stub.

sub check_conflicts { }

1;

__END__

=head1 NAME

Package::Stash::Conflicts - PerlOnJava stub for conflict checking

=head1 DESCRIPTION

This is a stub implementation for PerlOnJava. The original module checks
for conflicting versions of Package::Stash, but this is not needed in
PerlOnJava's environment.

=cut
3 changes: 2 additions & 1 deletion src/test/java/org/perlonjava/PerlScriptExecutionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ private void executeTest(String filename) {
lineNumber++;

// Check for test failures - works with both Test::More TAP format and simple tests
if (line.trim().startsWith("not ok")) {
// Skip TODO tests (they are expected to fail) - TAP format: "not ok ... # TODO ..."
if (line.trim().startsWith("not ok") && !line.contains("# TODO")) {
errorFound = true;
fail("Test failure in " + filename + " at line " + lineNumber + ": " + line);
break;
Expand Down
Loading
Loading