From f96713425e08b54fbfdfd4299bb9341ee8b82b6c Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 29 Apr 2026 22:16:07 +0200 Subject: [PATCH 1/2] fix: support `new Class or ...` and add no-op `ppd` Makefile target Two unrelated fixes uncovered while running `./jcpan -t Email::Stuff`: 1. ExtUtils/MakeMaker.pm: emit a no-op `ppd::` rule. Some Makefile.PLs (notably MailTools' postamble) declare `all:: ppd`, which previously broke MailTools' build with `make: *** No rule to make target 'ppd'`. Real ExtUtils::MakeMaker generates a Win32 PPM .ppd descriptor; we don't need PPM, but the target must exist so postambles work. 2. SubroutineParser: indirect-object syntax `new Class` followed by an infix operator (`or`, `and`, `||`, `&&`, `==`, ...) or a statement terminator (`;`, `)`, `}`, `]`, `,`, `?`, `:`) was being backtracked, making the call collapse to a bare `new` identifier and producing a confusing `syntax error ... near "or print ..."`. We now parse it as a zero-argument `Class->new()` call and let the outer parser consume the trailing operator. This fixes idioms like: my $msg = new Mail::Send or print "not "; my $m = new Mail::Mailer or warn; which appear verbatim in MailTools' t/mailer.t and t/send.t. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../frontend/parser/SubroutineParser.java | 56 +++++++++++++++++++ src/main/perl/lib/ExtUtils/MakeMaker.pm | 9 ++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index d1668c52c..d1a7ee803 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -294,6 +294,62 @@ && isValidIndirectMethod(subName, parser) arguments, currentIndex2), currentIndex2); } + // Followed by an infix operator (e.g. `new Foo or die`, + // `new Foo && bar`) or a statement terminator: parse as a + // zero-arg indirect-object call `Class->method()` and let + // the outer parser handle the operator. + // + // Be conservative about which operators trigger this: + // limit to logical/comparison operators (idiomatic after a + // method call) and statement terminators. Arithmetic + // operators (`+`, `-`, `*`, …) are excluded because the + // identifier parser converts a trailing `'` into a `::` + // package separator (so `eval'1+2'` becomes packageName + // `eval::1` followed by `+`); blindly accepting `+` here + // would mis-parse that idiom as `eval::1->f()` and leave + // the rest of the expression dangling. + boolean isSafeInfix = + token.text.equals("or") + || token.text.equals("and") + || token.text.equals("xor") + || token.text.equals("not") + || token.text.equals("||") + || token.text.equals("&&") + || token.text.equals("//") + || token.text.equals("==") + || token.text.equals("!=") + || token.text.equals("<=>") + || token.text.equals("eq") + || token.text.equals("ne") + || token.text.equals("cmp") + || token.text.equals("<") + || token.text.equals(">") + || token.text.equals("<=") + || token.text.equals(">=") + || token.text.equals("lt") + || token.text.equals("gt") + || token.text.equals("le") + || token.text.equals("ge"); + boolean isTerminator = + token.text.equals(";") + || token.text.equals(")") + || token.text.equals("}") + || token.text.equals("]") + || token.text.equals(",") + || token.text.equals("?") + || token.text.equals(":") + || token.type == LexerTokenType.EOF; + if (isSafeInfix || isTerminator) { + return new BinaryOperatorNode( + "->", + new IdentifierNode(packageName, currentIndex2), + new BinaryOperatorNode("(", + new OperatorNode("&", + new IdentifierNode(subName, currentIndex2), + currentIndex), + new ListNode(currentIndex), currentIndex2), + currentIndex2); + } } // backtrack diff --git a/src/main/perl/lib/ExtUtils/MakeMaker.pm b/src/main/perl/lib/ExtUtils/MakeMaker.pm index 479db49f5..08522c6db 100644 --- a/src/main/perl/lib/ExtUtils/MakeMaker.pm +++ b/src/main/perl/lib/ExtUtils/MakeMaker.pm @@ -739,7 +739,14 @@ realclean:: clean distclean:: clean \t\$(RM_RF) $makefile ${makefile}.old -.PHONY: all pm_to_blib pure_all pl_files blib_scripts config test install clean realclean distclean install_scripts +# ppd: real ExtUtils::MakeMaker generates a Win32 PPM .ppd descriptor here. +# PerlOnJava has no PPM, but some Makefile.PLs (e.g. MailTools) add +# `all:: ppd` in their postamble. Provide a no-op target so make doesn't +# fail with "No rule to make target ppd". +ppd:: +\t\@true + +.PHONY: all pm_to_blib pure_all pl_files blib_scripts config test install clean realclean distclean install_scripts ppd MAKEFILE # Call MY::postamble if it exists (File::ShareDir::Install uses this) From fd6a580a228071b23599af960639bae74b9b6968 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 29 Apr 2026 22:54:14 +0200 Subject: [PATCH 2/2] Email::Stuff: 3 more fixes (send override, base.pm require, \L\u in s///) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuation of fix/email-stuff-build. Three additional real PerlOnJava bugs found while running ./jcpan -t Email::Stuff: 1. ParserTables.OVERRIDABLE_OP: add `send`. Email::Send exports a sub named `send` that real Perl honours via typeglob assignment. PerlOnJava was rejecting `send(Test => $msg)` with "Not enough arguments for send" because it always enforced the socket builtin's prototype `*$$;$`. Adding `send` to OVERRIDABLE_OP makes the imported sub win, unblocking 5 Email::Send test files. 2. Base.importBase: tighten "already loaded" detection. `use base 'IO::Handle'` was skipping `require IO::Handle` because the Java backend pre-registers a few bridge stubs (IO::Handle::_sync etc.) in the global code-ref map, which made isPackageLoaded() return true. Realigned with real Perl's base.pm: only @ISA or $VERSION counts as loaded; otherwise require. If require fails with "Can't locate" / "not found" AND the package has code refs in its stash, accept it (preserves the DBIC eval-package fix from before). Fixes Mail::Mailer's `$class->SUPER::new` which was failing because IO::Handle::new was never defined. 3. StringDoubleQuoted.applyCaseModifier: nested \L\u ordering. `s/\b(\w+)/\L\u$1/g` on "spickett" was producing "spickett" instead of "Spickett". The outer \L was being applied AFTER the inner \u, so the AST was lc(ucfirst($1)) — which lowercases the freshly-uppercased first char. Real Perl applies case modifiers per-character left-to- right; for `\L\u` the first char gets \u, the rest get \L, equivalent to ucfirst(lc($1)). Fix: when a single-char modifier (\u/\l) is applied inside a region modifier (\L/\U/\F/\Q), pre-wrap the segment with the outer's case function before wrapping with the single-char function, and remove the segment from the outer's tracking so it isn't re-wrapped. Fixes 6 subtests in MailTools/t/extract.t (Mail::Address->name uses `s/\b(\w+)/\L\u$1/igo` to title-case extracted names). Status with these fixes: MailTools make test: PASS (109/109; was crashing) Email::Send make test: 89/90 (1 subtest fails: chained shebang) Email::Stuff: still blocked by the chained shebang cascade — needs a native binary launcher for jperl. Documented in dev/modules/email_stuff.md as item 6. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/modules/email_stuff.md | 205 ++++++++++++++++++ .../org/perlonjava/core/Configuration.java | 4 +- .../frontend/parser/ParserTables.java | 1 + .../frontend/parser/StringDoubleQuoted.java | 45 +++- .../perlonjava/runtime/perlmodule/Base.java | 43 ++-- 5 files changed, 276 insertions(+), 22 deletions(-) create mode 100644 dev/modules/email_stuff.md diff --git a/dev/modules/email_stuff.md b/dev/modules/email_stuff.md new file mode 100644 index 000000000..309b53a4d --- /dev/null +++ b/dev/modules/email_stuff.md @@ -0,0 +1,205 @@ +# Email::Stuff — `./jcpan -t Email::Stuff` + +## Status: PARTIALLY WORKING (4 PerlOnJava bugs fixed; 1 macOS-specific blocker remains) + +```bash +./jcpan -t Email::Stuff +``` + +System Perl baseline: 60/61 tests pass; the single remaining failure is an +upstream `Email::MIME` quoting style change (`name=README` vs. +`name="README"`) and is unrelated to PerlOnJava. + +PerlOnJava current status: + +| Distribution | make | make test | Notes | +|---------------------|------|-----------|-------| +| `MailTools` | OK | **PASS** (109/109) | All tests pass | +| `Return::Value` | OK | PASS (98/98) | | +| `Email::Send` | OK | FAIL (89/90; 1 sub) | `t/sendmail.t` chained shebang on macOS | +| `Email::Send::Test` | OK | (not run; same dist as Email::Send) | | +| `File::Type` | OK | PASS (58/58) | | +| `prefork` | OK | PASS | | +| `Email::Stuff` | OK | FAIL | cascade — `Email::Send` not in @INC | + +The single remaining failure is the macOS-specific chained-shebang blocker; +on Linux the chain works and the entire test suite would pass. + +## Dependency chain + +``` +Email::Stuff (RJBS/Email-Stuff-2.105) +├── Email::Send (RJBS/Email-Send-2.202) +│ ├── Mail::Internet (MARKOV/MailTools-2.22) [build_requires] +│ └── Return::Value (RJBS/Return-Value-1.666005) +├── Email::Send::Test (bundled with Email::Send) +└── File::Type (PMISON/File-Type-0.22) +``` + +`Mail::Internet`, `Email::Send`, and `Email::Stuff` are all old (~2008–2014) +and rely on idioms that are now-discouraged but still valid Perl. + +## Issues fixed (this branch — `fix/email-stuff-build`) + +### 1. `MakeMaker.pm`: missing `ppd::` target +**File:** `src/main/perl/lib/ExtUtils/MakeMaker.pm` + +`MailTools/Makefile.PL`'s `MY::postamble` adds `all:: ppd` (real +ExtUtils::MakeMaker generates a Win32 PPM `.ppd` descriptor here). +PerlOnJava never emitted a `ppd` rule, so `make` died with +`*** No rule to make target 'ppd'`, blocking the entire chain. + +Added a no-op `ppd::` target plus `.PHONY` entry. + +### 2. `SubroutineParser`: `new Class or ...` syntax error +**File:** `src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java` + +`my $x = new Foo or print "not "` produced +`syntax error ... near "or print "`. The indirect-object branch saw an +infix operator after the class name and backtracked past the class, +collapsing the call to a bare `new` identifier and confusing the outer +parser. + +Now: when `new Class` is followed by an `INFIX_OP` (`or`, `and`, `||`, +`&&`, …) or a statement terminator (`;`, `)`, `}`, `]`, `,`, `?`, `:`, EOF), +parse it as a zero-argument `Class->new()` and let the outer parser +handle the operator. `->` and `=>` are excluded. + +This idiom appears verbatim in `MailTools/t/mailer.t`, `t/send.t`, and +`Mail::Mailer::new`. + +### 3. `send` not on `OVERRIDABLE_OP` +**File:** `src/main/java/org/perlonjava/frontend/parser/ParserTables.java` + +`Email::Send` exports a sub named `send` and is used as +`use Email::Send 'Test'; send(Test => $msg);`. PerlOnJava was hard-coding +the prototype of the *socket* `send` builtin (`*$$;$`) and rejecting the +imported sub's call with `Not enough arguments for send` / +`Too many arguments for send`. + +Real Perl honours typeglob assignment from Exporter as an override +(this is exactly how `CORE::GLOBAL::send` is supposed to work). Added +`send` to `ParserTables.OVERRIDABLE_OP`. This unblocks 5 Email::Send +test files (`t/abstract-msg.t`, `t/all-mailers.t`, `t/classic.t`, +`t/errors.t`, `t/without.t`). + +### 4. `base.pm`: spurious "package already loaded" detection +**File:** `src/main/java/org/perlonjava/runtime/perlmodule/Base.java` + +`Mail::Mailer::new` does `$class->SUPER::new` where the calling-package +`@ISA` chain leads to `IO::Handle`. PerlOnJava's `Base.importBase` +considered `IO::Handle` "already loaded" because the Java backend +pre-registers a handful of bridge stubs (`IO::Handle::_sync`, etc.) in +the global code-ref map — so `use base 'IO::Handle'` skipped the +`require IO::Handle` and `IO::Handle::new` was never defined. + +Realigned with real Perl's `base.pm` logic: only `@ISA` or `$VERSION` +counts as "loaded"; otherwise attempt `require`. If the require +fails with a "Can't locate ... .pm in @INC" / "not found" error AND +the package nevertheless has code refs (Java bridge stubs OR +eval-created classes like DBIC's `t/inflate/hri.t`), accept the +in-memory package — preserving the DBIC fix from earlier. + +### 5. `\L\u$1` in regex replacement / interpolated strings +**File:** `src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java` + +`s/\b(\w+)/\L\u$1/g` on `spickett@tiac.net` produced `spickett@tiac.net` +(no change) instead of `Spickett@Tiac.Net`. The case-modifier stack +was wrapping the inner single-char modifier first and the outer region +modifier on top — yielding `lc(ucfirst($1))` (which lowercases the +freshly-uppercased first char). Real Perl applies modifiers +per-character left-to-right; for `\L\u$1` the first char gets `\u` +(wins over `\L`), the rest get `\L`, equivalent to +`ucfirst(lc($1))`. + +Fixed `applyCaseModifier`: when applying a single-char modifier +(`\u`/`\l`) inside a region modifier (`\L`/`\U`/`\F`/`\Q`), pre-wrap +the segment with the outer's case function first, then wrap with the +single-char function, and remove those segments from the outer's +tracking so they aren't re-wrapped. + +This fixes 6 subtests in `MailTools/t/extract.t` (used by `Mail::Address->name()` +which case-folds extracted names with `s/\b(\w+)/\L\u$1/igo`). + +## Remaining blocker: chained-shebang on macOS + +### 6. `Email::Send/t/sendmail.t` +**Symptom (1 subtest of 11 fails):** +``` +t/temp.../executable: line 4: syntax error near unexpected token `;' +t/temp.../executable: line 4: `my $input = join '', ;' +# Failed test 'cannot check sendmail log contents' at t/sendmail.t line 120. +``` + +The test writes a fake `sendmail` shell script with `#!$^X\n` (where +`$^X` is the path to `jperl`, which is itself a `#!/bin/bash` wrapper +script), `chmod 0755`s it, and execs it. macOS does **not** support +multi-level shebang (verified locally: a `#!/bin/bash` interpreter +script for a wrapper for our binary causes the kernel to return ENOEXEC, +and the calling shell falls back to interpreting the original file as +bash). Linux behaviour is the same — both fall back, but bash's fallback +is to run the script as bash code, which makes `my $input = …` a +syntax error. + +This is a `jperl`-as-script-launcher issue. Real fixes would be: +(a) replace the bash wrapper with a tiny native binary launcher + (`jpackage`, hand-written C, or rust); +(b) install a `binfmt_misc` rule on Linux only (still doesn't help macOS); +(c) intercept inside `Email::Send::Sendmail::send` (won't do — modifying + tests / installed module code is forbidden per AGENTS.md). + +For now, **out of scope** for this branch — Email::Stuff itself does +*not* exec the temp sendmail script; only the upstream `t/sendmail.t` +does. Once a native launcher exists, this test will pass and so will +the cascade. + +The cascade is unfortunate: CPAN.pm marks `Email::Send` as +`make_test => NO` after the single subtest failure, so it does not add +its `blib/lib` to `@INC` for `Email::Stuff`'s tests, which then fail +with `Can't locate Email/Send.pm in @INC`. + +### Future-proofing options for item 6 + +1. **Native binary launcher** — most robust. Add a small C or Rust + `jperl-bin` that does `execve("java", ["-jar", JAR_PATH, …])`. The + bash wrapper can stay as a convenience for users; `$^X` points at + the binary instead. + +2. **Detect bash-fallback inside the wrapper** — not possible; bash + never re-invokes our wrapper when the kernel returns ENOEXEC. + +3. **Patch `cpan_random_tester.pl`-style harness** to add a + shebang-rewrite filter — invasive, doesn't help running tests + manually. + +## Notes on test ordering + +`./jcpan -t` runs each prerequisite's `make test` and only adds its +`blib/lib` to the next module's `@INC` if the test passed. That's why +**any** `Email::Send` test failure cascades into `Can't locate +Email/Send.pm in @INC` when `Email::Stuff`'s tests run. + +## Progress tracking + +### Completed +- [x] System-perl baseline confirmed (60/61). +- [x] Item 1 — `ppd` Makefile target. +- [x] Item 2 — `new Class or ...` parse fix. +- [x] Item 3 — `send` override. +- [x] Item 4 — `use base 'IO::Handle'` actually `require`s now. +- [x] Item 5 — `\L\u$1` case modifier ordering. + +### Open +- [ ] Item 6 — chained shebang in `t/sendmail.t` (needs native launcher; + out of scope for this branch). + +### Net effect +Without item 6 fixed: +- `MailTools` `make test`: was crashing → now PASS (109/109). +- `Email::Send` `make test`: was crashing on most files → now 89/90. +- `Email::Stuff` `make test`: blocked by item 6 cascade. + +With item 6 fixed (Linux/once we have a native launcher): +- All distributions in the chain expected to PASS, mirroring the + system-perl 60/61 baseline. + diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 566786e29..3db562a92 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -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 = "2c91dd8bb"; + public static final String gitCommitId = "cfdf262ba"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -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 30 2026 10:33:02"; + public static final String buildTimestamp = "Apr 30 2026 10:41:56"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/ParserTables.java b/src/main/java/org/perlonjava/frontend/parser/ParserTables.java index f3ad5459a..82a4dc8a8 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParserTables.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParserTables.java @@ -35,6 +35,7 @@ public class ParserTables { "kill", "oct", "open", "readline", "readpipe", "rename", "require", + "send", "sleep", "stat", "system", "time", diff --git a/src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java b/src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java index 1446b489b..8634ed1e5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java +++ b/src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java @@ -287,6 +287,35 @@ private void applyCaseModifier(CaseModifier modifier) { // Create case-modified node var contentNode = createJoinNode(modifier.segments); + + // Single-char modifiers (backslash-u, backslash-l) inside a region modifier + // (backslash-L/U/F) need the region's case function applied FIRST, + // then the single-char on top. Otherwise a `\L` region followed + // by an inner single-char modifier becomes lc(ucfirst(...)) — + // which lowercases the freshly-uppercased + // first char and produces the wrong result. Real Perl applies + // the modifiers per-character left-to-right: at the first char + // both `\L` and the single-char are active and the single-char + // (the more recent one) wins; for the rest only `\L` is active. + // Equivalent expression: ucfirst(lc($1)). + if (modifier.isSingleChar && !caseModifiers.isEmpty()) { + CaseModifier outer = caseModifiers.peek(); + String outerOp = switch (outer.type) { + case "U" -> "uc"; + case "L" -> "lc"; + case "F" -> "fc"; + case "Q" -> "quotemeta"; + default -> null; + }; + // Only apply the pre-wrap if the outer modifier is a region + // type (\L/\U/\F/\Q) AND it actually tracks these same + // segments — otherwise we'd double-wrap segments owned by + // a different modifier. + if (outerOp != null && outer.segments.containsAll(modifier.segments)) { + contentNode = new OperatorNode(outerOp, contentNode, parser.tokenIndex); + } + } + var caseModifiedNode = new OperatorNode(operator, contentNode, parser.tokenIndex); // Replace segments with case-modified node @@ -298,11 +327,21 @@ private void applyCaseModifier(CaseModifier modifier) { segments.add(firstIndex, caseModifiedNode); // Update parent modifiers to reference the new node instead of the old segments - // This maintains proper nesting when modifiers are nested + // This maintains proper nesting when modifiers are nested. + // + // For single-char modifiers that already pre-wrapped with the + // outer's operator above, REMOVE the segments from the outer's + // tracking entirely — we don't want the outer to wrap again. for (CaseModifier parent : caseModifiers) { - if (parent.segments.removeAll(modifier.segments)) { - parent.segments.add(caseModifiedNode); + boolean removed = parent.segments.removeAll(modifier.segments); + if (!removed) continue; + if (modifier.isSingleChar && parent == caseModifiers.peek()) { + // pre-wrapped by outer's op above; do not re-add so + // the outer modifier won't wrap with lc/uc/fc again + // for these segments. (Other ancestors still wrap.) + continue; } + parent.segments.add(caseModifiedNode); } } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java index 329f04d88..2e8ed6759 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java @@ -96,20 +96,19 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { continue; } - // Check if the base class is already "loaded" in the Perl sense. - // Match Perl 5 base.pm semantics: a package counts as loaded if it has - // - $VERSION set, OR - // - @ISA populated, OR - // - any CODE refs in its stash - // (Perl's base.pm uses: !defined($VERSION) && !@ISA → then require.) - // Without this, packages that were populated programmatically (e.g. DBIC - // schema classes built from result_source metadata, or eval-created - // packages) would be spuriously require()d and fail because there is - // no corresponding .pm file. Fixes DBIC t/inflate/hri.t which does: - // eval "package DBICTest::CDSubclass; use base '$orig_resclass'"; - // where $orig_resclass is DBICTest::CD (defined in memory, no file). - boolean baseIsLoaded = GlobalVariable.isPackageLoaded(baseClassName) - || !GlobalVariable.getGlobalArray(baseClassName + "::ISA").elements.isEmpty() + // Match Perl 5 base.pm semantics: require the base class unless it + // already has $VERSION set OR @ISA populated. + // unless (defined ${"$base\::VERSION"} || @{"$base\::ISA"}) { + // require $base; + // } + // We add a graceful fallback for packages that were populated + // programmatically (no .pm file exists): if `require` fails with a + // "not found" error, but the package's stash has any code refs + // (Java-backend bridge stubs OR eval-created subs like in DBIC's + // t/inflate/hri.t which does + // eval "package DBICTest::CDSubclass; use base '$orig_resclass'"; + // ), accept the existing in-memory package instead of erroring. + boolean baseIsLoaded = !GlobalVariable.getGlobalArray(baseClassName + "::ISA").elements.isEmpty() || GlobalVariable.existsGlobalVariable(baseClassName + "::VERSION"); if (!baseIsLoaded) { // Require the base class file @@ -117,9 +116,19 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { try { RuntimeScalar ret = ModuleOperators.require(new RuntimeScalar(filename)); } catch (Exception e) { - if (e.getMessage().contains("not found")) { - System.err.println("Base class package \"" + baseClassName + "\" is empty."); - throw new PerlCompilerException("Base class package \"" + baseClassName + "\" is empty."); + String msg = e.getMessage(); + boolean notFound = msg != null + && (msg.contains("not found") + || msg.contains("Can't locate")); + if (notFound) { + // No .pm file. Fall back to in-memory check — the + // package may have been built up by other code (e.g. + // Java bridge stubs for IO::Handle::_sync, or DBIC's + // eval-created classes). + if (!GlobalVariable.isPackageLoaded(baseClassName)) { + System.err.println("Base class package \"" + baseClassName + "\" is empty."); + throw new PerlCompilerException("Base class package \"" + baseClassName + "\" is empty."); + } } else { throw e; }