diff --git a/dev/modules/anyevent_fixes.md b/dev/modules/anyevent_fixes.md index 29289f6fb..e7438384a 100644 --- a/dev/modules/anyevent_fixes.md +++ b/dev/modules/anyevent_fixes.md @@ -4,6 +4,53 @@ This document tracks the work needed to make `./jcpan -t AnyEvent` pass all 83 test programs. The project policy requires all tests to pass, including low-priority ones. +## Discussion — jcpan noise & signals (2026-05-09, PR #700) + +PR [#700](https://github.com/fglock/PerlOnJava/pull/700) addresses **spurious CPAN-time warnings** when installing/testing AnyEvent (and similar dists), not the functional gaps below. + +### What that PR fixes + +| Topic | Problem | Approach | +|-------|---------|------------| +| `Subroutine … redefined` / `Constant subroutine … redefined` | `warnings`-helper logic treated `globalWarningsEnabled` like “almost every category on”, unlike Perl’s `$^W` + `${^WARNING_BITS}` | `ckWarnForScope('redefine')` for non-constant redefine; constant redefine warns unless `no warnings 'redefine'` (even when `$^W` is 0); detect prior constant via prototype **`()`** | +| `"my" … masks earlier declaration` | Wrong category (`redefine`) + unconditional warn in one parser path | Gate with `ckWarnForScope(..., "shadow")` | +| `Warning: AutoSplit had to create top-level blib/lib/auto unexpectedly` | PerlOnJava MakeMaker only `mkdir -p blib/arch` before `pm_to_blib`; AutoSplit then creates `blib/lib/auto` | Emit `mkdir -p blib/lib/auto` before staging (matches typical EU::MM ordering) | + +### `t/02_signals.t` — `Bail out! No signal caught.` + +**What the test does** (`t/02_signals.t`): registers an **`INT`** watcher (`AnyEvent->signal`), arms a **5s bailout** timer, prints **`ok 2`**, then **`kill 'INT', $$`**. The handler should **`syswrite`** one byte on an internal pipe; **`AnyEvent::Loop`** **`select`** sees the fd; the **`io`** callback runs **`_signal_exec`**, which invokes AE callbacks → **`ok 3`**. If the pipe never wakes the loop, the timer prints bailout. + +**Failure modes on PerlOnJava** + +1. **Forward subroutine reference / GV stub (primary, reproducible)** + `AnyEvent::Base::signal` is built inside `eval q{ ... }` and registers **`AE::io(..., \&_signal_exec)` before** **`*_signal_exec = sub { ... }`**. On Perl 5, **` \&_signal_exec`** taken early still calls the body installed later (same GV). On **`jperl`**, invoking that early coderef **does not** run the assigned sub: + + ```perl + package AnyEvent::Base; + eval q{ + my $cb = \&_signal_exec; + *_signal_exec = sub { print "ok\n" }; + $cb->(); + }; + ``` + + Stock **`perl`** prints **`ok`**; **`jperl`** prints nothing. The event loop’s stored **`io`** callback can therefore be a no-op / wrong resolution, matching runtime errors such as **`Undefined subroutine &AnyEvent::Base::_signal_exec`** when **`AnyEvent::Loop`** invokes **`$_->[2]()`** on the watcher. + + **Fix direction:** **`RuntimeCode` / GV / coderef identity** — ensure **` \&pkg::name`** created before **`*pkg::name = sub { ... }`** shares the callable cell with Perl 5 (stub upgrade when the glob is filled). + +2. **POSIX signals on the JVM (secondary)** + Even with (1) fixed, **`kill 'INT', $$`** must run Perl’s **`%SIG`** handler in a process-compatible way. Expect further gaps vs Unix Perl. + +**Workarounds today** + +- Treat **`t/02_signals.t`** / **`t/03_child.t`** (fork) as **expected gaps** until **`fork`** and signal/coderef semantics match Perl. +- Run signal-heavy upstream suites with **`perl`**, not **`jperl`**, where policy allows (same theme as **`AGENTS.md`** fork note). +- Optional: **distroprefs** patch to early-exit **`t/02_signals.t`** on **`jperl`** (maintenance cost). + +**Earlier note on `weaken`:** an older hypothesis tied **`t/02_signals`** to **`weaken`** + **`select`** watchers. The **` \&_signal_exec`** stub issue is a better match for **`Undefined … _signal_exec`** and should be verified first after any GV fix. + +--- + ## Status | Date | Failed | Passed | Subtests running | Subtests failed | @@ -165,10 +212,13 @@ minimal reproduction in `AnyEvent::Socket::parse_address` and fix. ### H — `t/09_multi.t` and `t/02_signals.t`: signal delivery / Ctrl+C -Exit 130 is SIGINT. The test's timer/signal infrastructure is probably -reaching a deadlock and the outer harness sends SIGINT. Likely related -to AnyEvent::Base using `pipe` + signals, which now compiles but may -not actually wake up the select loop. +**Update (2026-05-09):** `t/02_signals.t` fails with **`Bail out! No signal caught.`** +(the 5s timer). Root cause analysis: **`AnyEvent::Base`** installs **` \&_signal_exec`** +before **`*_signal_exec = sub { ... }`**; **`jperl`** does not upgrade the early coderef +when the glob is filled — see **Discussion — jcpan noise & signals** above. Fix **`GV`/stub +coderef parity** first; then re-evaluate JVM **`kill`** / **`%SIG`**. + +`Exit 130` / harness SIGINT may still appear when tests hang once the stub bug is fixed. ### I — `t/13_weaken.t`: 3 subtests — weaken semantics @@ -209,6 +259,10 @@ Low priority, narrow after A–I land. ### Current status +- **2026-05-09:** Document updated with PR [#700](https://github.com/fglock/PerlOnJava/pull/700) + discussion (warnings/`${^WARNING_BITS}`, AutoSplit **`blib/lib/auto`**, constant redefine) + and clarified **`t/02_signals.t`** analysis (forward **` \&_signal_exec`** stub vs **`weaken`**). + Tier 1 fixes landed. Remaining work: - **Quick wins likely**: B (lvalue vec), G (inet_pton), J (single diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index e5dfc328d..932a77dba 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -10,7 +10,6 @@ import org.perlonjava.frontend.astnode.*; import org.perlonjava.frontend.semantic.SymbolTable; import org.perlonjava.runtime.perlmodule.Strict; -import org.perlonjava.runtime.perlmodule.Warnings; import org.perlonjava.runtime.operators.WarnDie; import org.perlonjava.runtime.runtimetypes.*; @@ -1423,8 +1422,8 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) { String var = sigil + name; if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("MY " + operator + " " + sigil + name); - // Check for redeclaration warnings - if (Warnings.warningManager.isWarningEnabled("redefine")) { + // Check for redeclaration warnings (Perl category "shadow", not "redefine") + if (WarningFlags.ckWarnForScope(emitterVisitor.ctx.symbolTable, "shadow")) { // Skip warning for state variables that were already hoisted by EmitBlock. // The hoisted declaration pre-allocates the JVM local slot, so the original // declaration sees it as a duplicate. This is not a real user-level redeclaration. diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 48e3b7a25..1e4583541 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -357,8 +357,9 @@ && isGlobalOnlyVariable(name)) { "\"our\" variable " + var + " redeclared")); } } else { - // For 'my'/'local', warn if redeclared in the same scope - if (ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) { + // For 'my'/'local', warn if redeclared in the same scope (warnings 'shadow') + if (WarningFlags.ckWarnForScope(ctx.symbolTable, "shadow") + && ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) { System.err.println( ctx.errorUtil.errorMessage(node.getIndex(), "\"" + operator + "\" variable " + var + " masks earlier declaration in same scope")); diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 484e5acb7..9b92b381d 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -1140,8 +1140,8 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S || existingCode.compilerSupplier != null; if (isRedefinition) { oldPrototype = existingCode.prototype; - // A constant sub has empty prototype "()" - detect for "Constant subroutine" warning - isConstantSub = "".equals(oldPrototype); + // Previous sub was compile-time constant iff prototype is "()". (Perl stores "()", not "") + isConstantSub = "()".equals(oldPrototype) || "".equals(oldPrototype); // Java-registered methods (via registerMethod) have isStatic=true and methodHandle set isBuiltinSub = existingCode.isStatic && existingCode.methodHandle != null; } @@ -1157,7 +1157,6 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S } // Prototype mismatch is a default warning (always on unless explicitly disabled) - boolean dollarW = GlobalVariable.getGlobalVariable("main::" + Character.toString('W' - 'A' + 1)).getBoolean(); { // Perl format: "sub NAME: none vs (new)" or "sub NAME (old) vs none" // When prototype is null, display as ": none"; when defined, display as " (proto)" @@ -1171,14 +1170,16 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S } } - // "Constant subroutine X redefined" is a default warning (always on) - // "Subroutine X redefined" requires -w or use warnings 'redefine' + // "Subroutine X redefined": ckWARN('redefine') — $^W or lexical 'redefine' in ${^WARNING_BITS}. + // "Constant subroutine X redefined": still emitted when $^W is 0 (e.g. eval under local $^W=0); + // only suppressed by lexical no warnings 'redefine' / no warnings. See perl5_t/t/comp/redef.t. if (isConstantSub) { - String msg = "Constant subroutine " + subName + " redefined" + location; - org.perlonjava.runtime.operators.WarnDie.warn( - new RuntimeScalar(msg), new RuntimeScalar("")); - } else if (!Warnings.warningManager.isWarningDisabled("redefine") - && (dollarW || Warnings.warningManager.isWarningEnabled("redefine"))) { + if (!Warnings.warningManager.isWarningDisabled("redefine")) { + String msg = "Constant subroutine " + subName + " redefined" + location; + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar(msg), new RuntimeScalar("")); + } + } else if (WarningFlags.ckWarnForScope(parser.ctx.symbolTable, "redefine")) { String msg = "Subroutine " + subName + " redefined" + location; org.perlonjava.runtime.operators.WarnDie.warn( new RuntimeScalar(msg), new RuntimeScalar("")); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java b/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java index a3fefb76f..aa00b6669 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/WarningFlags.java @@ -347,6 +347,31 @@ public static String toWarningBitsString(BitSet enabled, BitSet fatal, return new String(bytes, StandardCharsets.ISO_8859_1); } + /** + * Perl compile-time {@code ckWARN}-style check: honor {@code no warnings 'category'}, + * then {@code $^W (-w)}, then the category's enabled bit in the scope's + * {@code ${^WARNING_BITS}} string. + * + *

Do not use {@link #warningManager}{@code .isWarningEnabled()} for compile-time + * diagnostics: when {@link #globalWarningsEnabled} is true (after a bare {@code use warnings} + * anywhere in the process), that helper incorrectly treats almost every category as on unless + * explicitly disabled, which breaks modules that set warning state only via + * {@code ${^WARNING_BITS}} (e.g. AnyEvent's generated {@code AnyEvent::common_sense}). + */ + public static boolean ckWarnForScope(ScopedSymbolTable scope, String category) { + if (scope != null && scope.isWarningCategoryDisabled(category)) { + return false; + } + if (GlobalVariable.getGlobalVariable("main::" + Character.toString('W' - 'A' + 1)) + .getBoolean()) { + return true; + } + if (scope == null) { + return false; + } + return isEnabledInBits(scope.getWarningBitsString(), category); + } + /** * Checks if a category is enabled in a warning bits string. * diff --git a/src/main/perl/lib/ExtUtils/MakeMaker.pm b/src/main/perl/lib/ExtUtils/MakeMaker.pm index 249978c4c..bd667ad00 100644 --- a/src/main/perl/lib/ExtUtils/MakeMaker.pm +++ b/src/main/perl/lib/ExtUtils/MakeMaker.pm @@ -743,8 +743,12 @@ $depend_rules_str # ExtUtils::MakeMaker). Files are NOT copied to INSTALLSITELIB here; # that happens in the 'install' target. # Also create blib/arch so that "use blib" / "-Mblib" works (blib.pm requires both). +# Pre-create blib/lib/auto before AutoSplit runs (each staged .pm may call autosplit into it). +# Without this, AutoSplit::autosplit_file creates the dir and prints a warning — stock +# EU::MakeMaker typically creates blib dirs earlier via blibdirs / pm_to_blib ordering. pm_to_blib::$pm_deps_str \t\@mkdir -p blib/arch +\t\@mkdir -p blib/lib/auto $blib_cmds_str # pure_all is an alias target some postambles (File::ShareDir::Install,