diff --git a/dev/modules/anon_sub_naming.md b/dev/modules/anon_sub_naming.md new file mode 100644 index 000000000..457bb42e3 --- /dev/null +++ b/dev/modules/anon_sub_naming.md @@ -0,0 +1,195 @@ +# Anonymous subroutine naming via `*__ANON__` + +## Problem + +`local *__ANON__ = 'name'` is a Perl idiom for giving an anonymous +subroutine a temporary name visible to `caller()`, `Carp`, and +`Sub::Util::subname()`. It is used by `SUPER.pm`, `Try::Tiny`, +`namespace::clean`, and several Moose internals to make stack traces +and SUPER-dispatch work correctly when subs are installed under +unusual names (or not installed at all). + +In PerlOnJava the idiom is silently lost: + +```perl +my $s = sub { local *__ANON__ = 'myname'; (caller(0))[3] }; +$s->(); +# real perl: main::myname +# jperl: main::__ANON__ +``` + +The root cause: `caller()` and `Sub::Util::subname` read the cached +`RuntimeCode.subName`, which for anonymous subs is always `null` (and +falls back to `"__ANON__"`). Real perl resolves the name dynamically +through `CvGV(cv)->NAME`, so a `local`-scoped alias of the package's +`*__ANON__` glob is observed as a rename. + +This blocks `SUPER` (3 of 6 tests fail in `t/bugs.t`), which in turn +blocks `Test::MockModule`, which in turn blocks `DBIx::Retry` and +others. + +## Goal + +Make `caller()` and `Sub::Util::subname` honor `local *PKG::__ANON__ += 'name'` for the dynamic scope of the local, without regressing +existing behavior or `Sub::Name`/`Sub::Util::set_subname`. + +## Design — pragmatic glob indirection (Option A) + +### Data model + +1. `RuntimeGlob` gains an optional field + ```java + public String nameOverride; // null by default + ``` + This represents the dynamic "name override" of the glob — i.e. the + string most recently assigned via `*foo = $string`. + +2. `RuntimeCode` does **not** need a new field. Anonymous subs + already carry `packageName` (the CvSTASH equivalent), and that's + enough to locate the relevant `*PKG::__ANON__` glob via + `GlobalVariable.globalIORefs.get(packageName + "::__ANON__")`. + +### Write path: `*PKG::FOO = $string` + +In `RuntimeGlob.set(RuntimeScalar value)`, the scalar-value cases +(STRING / BYTE_STRING / INTEGER / DOUBLE / BOOLEAN / VSTRING / +DUALVAR) currently store `value` into the SCALAR slot. We add: + +```java +RuntimeGlob current = GlobalVariable.globalIORefs.getOrDefault( + this.globName, this); +current.nameOverride = value.toString(); +``` + +We update `current` rather than `this` because `local *FOO` swaps in +a new RuntimeGlob in `globalIORefs`; the lvalue captured before +`local` still references the old RuntimeGlob, but the override must +be visible to readers that look up the *current* glob by name. The +existing SCALAR-slot write already follows this "look up by name" +pattern (see `getGlobalVariable(this.globName)` in `set(STRING)`). + +The override is **only** set by glob-as-scalar assignment; plain +`$PKG::__ANON__ = $x` continues to write the SCALAR slot without +touching `nameOverride`. This matches real Perl's distinction +between glob assignment (which does the stash-alias trick) and +scalar assignment. + +### Local-scope handling + +`RuntimeGlob.dynamicSaveState()` already creates a fresh +`RuntimeGlob` for the local scope and installs it in `globalIORefs`. +A fresh glob has `nameOverride == null`, so the local scope starts +clean. `dynamicRestoreState()` restores the original glob, whose +`nameOverride` was never mutated, so no extra save/restore is +needed. + +### Read path: `caller()` and `Sub::Util::subname` + +For anonymous subs (where `code.subName` is null/empty and +`code.explicitlyRenamed` is false), consult the override: + +```java +String name = null; +if (!code.explicitlyRenamed + && (code.subName == null || code.subName.isEmpty()) + && code.packageName != null) { + RuntimeGlob anonGlob = GlobalVariable.globalIORefs.get( + code.packageName + "::__ANON__"); + if (anonGlob != null && anonGlob.nameOverride != null + && !anonGlob.nameOverride.isEmpty()) { + name = code.packageName + "::" + anonGlob.nameOverride; + } +} +if (name == null) { + // existing fallback: "Pkg::__ANON__" or stack-trace info +} +``` + +Lookup order: + +1. `code.explicitlyRenamed` (`Sub::Name`, `Sub::Util::set_subname`) + wins outright — this matches real Perl, where a CV whose `CvGV` + has been repointed by `Sub::Name` is no longer affected by + `local *__ANON__` higher up. +2. Anonymous-sub override via `*PKG::__ANON__`'s `nameOverride`. +3. Fallback: `Pkg::__ANON__` (current behavior). + +### Interaction with `Sub::Name` / `Sub::Util::set_subname` + +`Sub::Name::subname` and `Sub::Util::set_subname` mutate +`RuntimeCode.subName`/`packageName` and set +`explicitlyRenamed = true`. The new lookup explicitly checks +`explicitlyRenamed` first, so: + +- Sub::Name on a sub that's also under `local *__ANON__`: the + Sub::Name name wins (matches real perl). +- Plain anon sub under `local *__ANON__`: the override wins. +- Plain anon sub outside any local: falls back to + `Pkg::__ANON__` as today. + +`B::CV->GV->NAME` and the `_is_renamed` shim in `Sub::Name` are not +touched in this change. A future cleanup could fold the +`explicitlyRenamed` mechanism into the same glob-indirection model +(repointing the anon-glob link on `set_subname`), letting us delete +`_is_renamed`. That's left for follow-up. + +## Tests + +### Regression baseline (must keep passing) + +Captured in `dev/modules/anon_sub_naming_baseline.txt`. Highlights: + +| Idiom | Expected name | +|----------------------------------------|------------------| +| `Sub::Name::subname('My::r', $s)` | `My::r` | +| `Sub::Util::set_subname('O::n', $s)` | `O::n` | +| Plain `sub { ... }` in `main` | `main::__ANON__` | +| Plain `sub { ... }` in `Foo::Bar` | `Foo::Bar::__ANON__` | +| `B::svref_2object(set_subname'd)->GV->NAME` | `n` | + +### New behavior (must start passing) + +| Idiom | Expected name | +|------------------------------------------------|---------------| +| `local *__ANON__ = 'myname'` in `sub { caller }` | `main::myname` | +| `local *Foo::__ANON__ = 'x'` in `Foo` package | `Foo::x` | +| Sub::Name'd sub also under `local *__ANON__` | Sub::Name's name (unchanged) | +| Carp longmess from sub under `local *__ANON__` | reflects override | +| SUPER.pm `t/bugs.t` | 6/6 pass | + +### End-to-end + +`./jcpan -i SUPER` should pass tests; `./jcpan -i Test::MockModule` +should follow; `./jcpan -t DBIx::Retry` should at least get past +the "Test::MockModule not found" stage. + +## Out of scope + +- Making `*foo = "string"` do the full Perl stash-alias dance for + arbitrary glob names. We only honor the override for naming + purposes; the SCALAR slot semantics of glob-string assignment are + unchanged. +- Rebuilding `Sub::Name` on top of glob indirection. +- `B::CV->GV->NAME` reflecting the override dynamically (currently + always reports `__ANON__` for anon subs; not consulted by SUPER). + +## Status + +- [x] Design +- [x] Baseline captured (`anon_sub_naming_baseline.txt`) +- [x] Implementation + - `RuntimeGlob.nameOverride` field + - `RuntimeGlob.set(scalar)` records override on the live glob via + `peekGlobalIO(globName)` + - `GlobalVariable.peekGlobalIO(name)` non-vivifying lookup + - `RuntimeCode.callerWithSub` consults override for both + innermost (via `currentSub`) and deeper (via stack-trace + `Pkg::__ANON__` frame) anon frames + - `SubUtil.subname` consults override +- [x] SUPER `t/bugs.t` passes 6/6 +- [x] `Test::MockModule` installs and tests pass 103/103 +- [x] `DBIx::Retry` test chain unblocked (17 subtests run, 1 + remaining unrelated DBD::ExampleP failure) +- [x] Sub::Name baseline diff is empty (no regressions) +- [x] `make` passes diff --git a/dev/modules/anon_sub_naming_baseline.txt b/dev/modules/anon_sub_naming_baseline.txt new file mode 100644 index 000000000..d0aa3e53b --- /dev/null +++ b/dev/modules/anon_sub_naming_baseline.txt @@ -0,0 +1,13 @@ +1a (before rename) caller: main::__ANON__ +1b (before rename) Sub::Util::subname: main::__ANON__ +2a (after Sub::Name) caller: My::renamed +2b (after Sub::Name) Sub::Util::subname: My::renamed +3a (set_subname) caller: Other::name +3b (set_subname) Sub::Util::subname: Other::name +4a (plain) caller: main::__ANON__ +4b (plain) Sub::Util::subname: main::__ANON__ +5a (Foo::Bar) caller: Foo::Bar::__ANON__ +5b (Foo::Bar) Sub::Util::subname: Foo::Bar::__ANON__ +6 (Carp w/ Sub::Name): +7 (B GV NAME after set_subname): name +7 (B GV STASH NAME): Other diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 4fd48602a..1d4acc845 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 = "bebebd07e"; + public static final String gitCommitId = "82c10284e"; /** * 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 29 2026 12:11:44"; + public static final String buildTimestamp = "Apr 29 2026 13:31:56"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/SubUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/SubUtil.java index 583966958..318f60d7e 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/SubUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/SubUtil.java @@ -123,7 +123,15 @@ public static RuntimeList subname(RuntimeArray args, int ctx) { if (sub == null || sub.isEmpty()) { // Anonymous sub: real Perl returns "Package::__ANON__" where Package // is the compile-time package (CvSTASH). + // Honor `local *PKG::__ANON__ = 'name'` by consulting the package's + // *__ANON__ glob's nameOverride. See dev/modules/anon_sub_naming.md. if (pkg != null && !pkg.isEmpty()) { + org.perlonjava.runtime.runtimetypes.RuntimeGlob anonGlob = + GlobalVariable.peekGlobalIO(pkg + "::__ANON__"); + if (anonGlob != null && anonGlob.nameOverride != null + && !anonGlob.nameOverride.isEmpty()) { + return new RuntimeScalar(pkg + "::" + anonGlob.nameOverride).getList(); + } return new RuntimeScalar(pkg + "::__ANON__").getList(); } return new RuntimeScalar("__ANON__").getList(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index a69fb7f9a..b4ed3ee85 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -900,6 +900,17 @@ public static RuntimeGlob getGlobalIO(String key) { 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 + * (see dev/modules/anon_sub_naming.md) to read *PKG::__ANON__'s + * nameOverride without creating an empty glob as a side effect. + */ + public static RuntimeGlob peekGlobalIO(String key) { + String resolvedKey = resolveStashHashRedirect(key); + return globalIORefs.get(resolvedKey); + } + /** * Retrieves a detached copy of a global IO reference, wrapped in a RuntimeScalar. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index cb1f96c98..034bfef79 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -2267,6 +2267,16 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar if (code.subName != null && !code.subName.isEmpty()) { String codePkg = code.packageName != null ? code.packageName : "main"; subName = codePkg + "::" + code.subName; + } else if (!code.explicitlyRenamed && code.packageName != null) { + // Anonymous sub: honor `local *PKG::__ANON__ = 'name'` + // by reading the package's *__ANON__ glob's nameOverride. + // See dev/modules/anon_sub_naming.md. + RuntimeGlob anonGlob = GlobalVariable.peekGlobalIO( + code.packageName + "::__ANON__"); + if (anonGlob != null && anonGlob.nameOverride != null + && !anonGlob.nameOverride.isEmpty()) { + subName = code.packageName + "::" + anonGlob.nameOverride; + } } } @@ -2287,6 +2297,22 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar } } + // Honor `local *PKG::__ANON__ = 'name'` for any anonymous-sub + // frame, not just the innermost one. After both fallbacks, + // an anon frame ends up as "Pkg::__ANON__"; if the package's + // *__ANON__ glob currently has a name override active, swap + // it in. See dev/modules/anon_sub_naming.md. + if (subName != null && subName.endsWith("::__ANON__")) { + String anonPkg = subName.substring(0, + subName.length() - "::__ANON__".length()); + RuntimeGlob anonGlob = GlobalVariable.peekGlobalIO( + anonPkg + "::__ANON__"); + if (anonGlob != null && anonGlob.nameOverride != null + && !anonGlob.nameOverride.isEmpty()) { + subName = anonPkg + "::" + anonGlob.nameOverride; + } + } + if (subName != null && !subName.isEmpty()) { res.add(new RuntimeScalar(subName)); // subroutine } else { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 1faf744e3..091f9dc66 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -27,6 +27,12 @@ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference RuntimeHash hashSlot; // Local code slot for detached globs (from stash delete) public RuntimeScalar codeSlot; + // Dynamic name override set by `*foo = $string` glob-as-scalar assignment. + // Used to honor the `local *PKG::__ANON__ = 'name'` idiom (see SUPER.pm, + // Try::Tiny, namespace::clean) — caller()/Sub::Util::subname report this + // string in place of __ANON__ for anonymous subs whose CvSTASH is PKG. + // See dev/modules/anon_sub_naming.md. + public String nameOverride; /** * Tracks how many RuntimeScalar variables hold a GLOBREFERENCE to this glob. @@ -327,6 +333,17 @@ public RuntimeScalar set(RuntimeScalar value) { } else { currentScalar.set(value); } + // Record the dynamic name override for `local *PKG::__ANON__ = + // 'name'`. We update the *current* glob in globalIORefs (which + // is the freshly-created glob during a local scope), so readers + // that look up by name see the override regardless of whether + // they reached the lvalue before or after `local` swapped the + // glob. See dev/modules/anon_sub_naming.md. + if (this.globName != null) { + RuntimeGlob currentGlob = GlobalVariable.peekGlobalIO(this.globName); + if (currentGlob == null) currentGlob = this; + currentGlob.nameOverride = value.toString(); + } return value; case FORMAT: // Handle format assignments to typeglobs