Skip to content

Commit ff1da2b

Browse files
authored
Merge pull request #617 from fglock/feature/anon-sub-local-anon-naming
fix(naming): honor local *PKG::__ANON__ = 'name' in caller()/Sub::Util
2 parents 517239c + c3effde commit ff1da2b

7 files changed

Lines changed: 272 additions & 2 deletions

File tree

dev/modules/anon_sub_naming.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Anonymous subroutine naming via `*__ANON__`
2+
3+
## Problem
4+
5+
`local *__ANON__ = 'name'` is a Perl idiom for giving an anonymous
6+
subroutine a temporary name visible to `caller()`, `Carp`, and
7+
`Sub::Util::subname()`. It is used by `SUPER.pm`, `Try::Tiny`,
8+
`namespace::clean`, and several Moose internals to make stack traces
9+
and SUPER-dispatch work correctly when subs are installed under
10+
unusual names (or not installed at all).
11+
12+
In PerlOnJava the idiom is silently lost:
13+
14+
```perl
15+
my $s = sub { local *__ANON__ = 'myname'; (caller(0))[3] };
16+
$s->();
17+
# real perl: main::myname
18+
# jperl: main::__ANON__
19+
```
20+
21+
The root cause: `caller()` and `Sub::Util::subname` read the cached
22+
`RuntimeCode.subName`, which for anonymous subs is always `null` (and
23+
falls back to `"__ANON__"`). Real perl resolves the name dynamically
24+
through `CvGV(cv)->NAME`, so a `local`-scoped alias of the package's
25+
`*__ANON__` glob is observed as a rename.
26+
27+
This blocks `SUPER` (3 of 6 tests fail in `t/bugs.t`), which in turn
28+
blocks `Test::MockModule`, which in turn blocks `DBIx::Retry` and
29+
others.
30+
31+
## Goal
32+
33+
Make `caller()` and `Sub::Util::subname` honor `local *PKG::__ANON__
34+
= 'name'` for the dynamic scope of the local, without regressing
35+
existing behavior or `Sub::Name`/`Sub::Util::set_subname`.
36+
37+
## Design — pragmatic glob indirection (Option A)
38+
39+
### Data model
40+
41+
1. `RuntimeGlob` gains an optional field
42+
```java
43+
public String nameOverride; // null by default
44+
```
45+
This represents the dynamic "name override" of the glob — i.e. the
46+
string most recently assigned via `*foo = $string`.
47+
48+
2. `RuntimeCode` does **not** need a new field. Anonymous subs
49+
already carry `packageName` (the CvSTASH equivalent), and that's
50+
enough to locate the relevant `*PKG::__ANON__` glob via
51+
`GlobalVariable.globalIORefs.get(packageName + "::__ANON__")`.
52+
53+
### Write path: `*PKG::FOO = $string`
54+
55+
In `RuntimeGlob.set(RuntimeScalar value)`, the scalar-value cases
56+
(STRING / BYTE_STRING / INTEGER / DOUBLE / BOOLEAN / VSTRING /
57+
DUALVAR) currently store `value` into the SCALAR slot. We add:
58+
59+
```java
60+
RuntimeGlob current = GlobalVariable.globalIORefs.getOrDefault(
61+
this.globName, this);
62+
current.nameOverride = value.toString();
63+
```
64+
65+
We update `current` rather than `this` because `local *FOO` swaps in
66+
a new RuntimeGlob in `globalIORefs`; the lvalue captured before
67+
`local` still references the old RuntimeGlob, but the override must
68+
be visible to readers that look up the *current* glob by name. The
69+
existing SCALAR-slot write already follows this "look up by name"
70+
pattern (see `getGlobalVariable(this.globName)` in `set(STRING)`).
71+
72+
The override is **only** set by glob-as-scalar assignment; plain
73+
`$PKG::__ANON__ = $x` continues to write the SCALAR slot without
74+
touching `nameOverride`. This matches real Perl's distinction
75+
between glob assignment (which does the stash-alias trick) and
76+
scalar assignment.
77+
78+
### Local-scope handling
79+
80+
`RuntimeGlob.dynamicSaveState()` already creates a fresh
81+
`RuntimeGlob` for the local scope and installs it in `globalIORefs`.
82+
A fresh glob has `nameOverride == null`, so the local scope starts
83+
clean. `dynamicRestoreState()` restores the original glob, whose
84+
`nameOverride` was never mutated, so no extra save/restore is
85+
needed.
86+
87+
### Read path: `caller()` and `Sub::Util::subname`
88+
89+
For anonymous subs (where `code.subName` is null/empty and
90+
`code.explicitlyRenamed` is false), consult the override:
91+
92+
```java
93+
String name = null;
94+
if (!code.explicitlyRenamed
95+
&& (code.subName == null || code.subName.isEmpty())
96+
&& code.packageName != null) {
97+
RuntimeGlob anonGlob = GlobalVariable.globalIORefs.get(
98+
code.packageName + "::__ANON__");
99+
if (anonGlob != null && anonGlob.nameOverride != null
100+
&& !anonGlob.nameOverride.isEmpty()) {
101+
name = code.packageName + "::" + anonGlob.nameOverride;
102+
}
103+
}
104+
if (name == null) {
105+
// existing fallback: "Pkg::__ANON__" or stack-trace info
106+
}
107+
```
108+
109+
Lookup order:
110+
111+
1. `code.explicitlyRenamed` (`Sub::Name`, `Sub::Util::set_subname`)
112+
wins outright — this matches real Perl, where a CV whose `CvGV`
113+
has been repointed by `Sub::Name` is no longer affected by
114+
`local *__ANON__` higher up.
115+
2. Anonymous-sub override via `*PKG::__ANON__`'s `nameOverride`.
116+
3. Fallback: `Pkg::__ANON__` (current behavior).
117+
118+
### Interaction with `Sub::Name` / `Sub::Util::set_subname`
119+
120+
`Sub::Name::subname` and `Sub::Util::set_subname` mutate
121+
`RuntimeCode.subName`/`packageName` and set
122+
`explicitlyRenamed = true`. The new lookup explicitly checks
123+
`explicitlyRenamed` first, so:
124+
125+
- Sub::Name on a sub that's also under `local *__ANON__`: the
126+
Sub::Name name wins (matches real perl).
127+
- Plain anon sub under `local *__ANON__`: the override wins.
128+
- Plain anon sub outside any local: falls back to
129+
`Pkg::__ANON__` as today.
130+
131+
`B::CV->GV->NAME` and the `_is_renamed` shim in `Sub::Name` are not
132+
touched in this change. A future cleanup could fold the
133+
`explicitlyRenamed` mechanism into the same glob-indirection model
134+
(repointing the anon-glob link on `set_subname`), letting us delete
135+
`_is_renamed`. That's left for follow-up.
136+
137+
## Tests
138+
139+
### Regression baseline (must keep passing)
140+
141+
Captured in `dev/modules/anon_sub_naming_baseline.txt`. Highlights:
142+
143+
| Idiom | Expected name |
144+
|----------------------------------------|------------------|
145+
| `Sub::Name::subname('My::r', $s)` | `My::r` |
146+
| `Sub::Util::set_subname('O::n', $s)` | `O::n` |
147+
| Plain `sub { ... }` in `main` | `main::__ANON__` |
148+
| Plain `sub { ... }` in `Foo::Bar` | `Foo::Bar::__ANON__` |
149+
| `B::svref_2object(set_subname'd)->GV->NAME` | `n` |
150+
151+
### New behavior (must start passing)
152+
153+
| Idiom | Expected name |
154+
|------------------------------------------------|---------------|
155+
| `local *__ANON__ = 'myname'` in `sub { caller }` | `main::myname` |
156+
| `local *Foo::__ANON__ = 'x'` in `Foo` package | `Foo::x` |
157+
| Sub::Name'd sub also under `local *__ANON__` | Sub::Name's name (unchanged) |
158+
| Carp longmess from sub under `local *__ANON__` | reflects override |
159+
| SUPER.pm `t/bugs.t` | 6/6 pass |
160+
161+
### End-to-end
162+
163+
`./jcpan -i SUPER` should pass tests; `./jcpan -i Test::MockModule`
164+
should follow; `./jcpan -t DBIx::Retry` should at least get past
165+
the "Test::MockModule not found" stage.
166+
167+
## Out of scope
168+
169+
- Making `*foo = "string"` do the full Perl stash-alias dance for
170+
arbitrary glob names. We only honor the override for naming
171+
purposes; the SCALAR slot semantics of glob-string assignment are
172+
unchanged.
173+
- Rebuilding `Sub::Name` on top of glob indirection.
174+
- `B::CV->GV->NAME` reflecting the override dynamically (currently
175+
always reports `__ANON__` for anon subs; not consulted by SUPER).
176+
177+
## Status
178+
179+
- [x] Design
180+
- [x] Baseline captured (`anon_sub_naming_baseline.txt`)
181+
- [x] Implementation
182+
- `RuntimeGlob.nameOverride` field
183+
- `RuntimeGlob.set(scalar)` records override on the live glob via
184+
`peekGlobalIO(globName)`
185+
- `GlobalVariable.peekGlobalIO(name)` non-vivifying lookup
186+
- `RuntimeCode.callerWithSub` consults override for both
187+
innermost (via `currentSub`) and deeper (via stack-trace
188+
`Pkg::__ANON__` frame) anon frames
189+
- `SubUtil.subname` consults override
190+
- [x] SUPER `t/bugs.t` passes 6/6
191+
- [x] `Test::MockModule` installs and tests pass 103/103
192+
- [x] `DBIx::Retry` test chain unblocked (17 subtests run, 1
193+
remaining unrelated DBD::ExampleP failure)
194+
- [x] Sub::Name baseline diff is empty (no regressions)
195+
- [x] `make` passes
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
1a (before rename) caller: main::__ANON__
2+
1b (before rename) Sub::Util::subname: main::__ANON__
3+
2a (after Sub::Name) caller: My::renamed
4+
2b (after Sub::Name) Sub::Util::subname: My::renamed
5+
3a (set_subname) caller: Other::name
6+
3b (set_subname) Sub::Util::subname: Other::name
7+
4a (plain) caller: main::__ANON__
8+
4b (plain) Sub::Util::subname: main::__ANON__
9+
5a (Foo::Bar) caller: Foo::Bar::__ANON__
10+
5b (Foo::Bar) Sub::Util::subname: Foo::Bar::__ANON__
11+
6 (Carp w/ Sub::Name):
12+
7 (B GV NAME after set_subname): name
13+
7 (B GV STASH NAME): Other

src/main/java/org/perlonjava/core/Configuration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class Configuration {
3333
* Automatically populated by Gradle/Maven during build.
3434
* DO NOT EDIT MANUALLY - this value is replaced at build time.
3535
*/
36-
public static final String gitCommitId = "bebebd07e";
36+
public static final String gitCommitId = "82c10284e";
3737

3838
/**
3939
* Git commit date of the build (ISO format: YYYY-MM-DD).
@@ -48,7 +48,7 @@ public final class Configuration {
4848
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
4949
* DO NOT EDIT MANUALLY - this value is replaced at build time.
5050
*/
51-
public static final String buildTimestamp = "Apr 29 2026 12:11:44";
51+
public static final String buildTimestamp = "Apr 29 2026 13:31:56";
5252

5353
// Prevent instantiation
5454
private Configuration() {

src/main/java/org/perlonjava/runtime/perlmodule/SubUtil.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,15 @@ public static RuntimeList subname(RuntimeArray args, int ctx) {
123123
if (sub == null || sub.isEmpty()) {
124124
// Anonymous sub: real Perl returns "Package::__ANON__" where Package
125125
// is the compile-time package (CvSTASH).
126+
// Honor `local *PKG::__ANON__ = 'name'` by consulting the package's
127+
// *__ANON__ glob's nameOverride. See dev/modules/anon_sub_naming.md.
126128
if (pkg != null && !pkg.isEmpty()) {
129+
org.perlonjava.runtime.runtimetypes.RuntimeGlob anonGlob =
130+
GlobalVariable.peekGlobalIO(pkg + "::__ANON__");
131+
if (anonGlob != null && anonGlob.nameOverride != null
132+
&& !anonGlob.nameOverride.isEmpty()) {
133+
return new RuntimeScalar(pkg + "::" + anonGlob.nameOverride).getList();
134+
}
127135
return new RuntimeScalar(pkg + "::__ANON__").getList();
128136
}
129137
return new RuntimeScalar("__ANON__").getList();

src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,17 @@ public static RuntimeGlob getGlobalIO(String key) {
900900
return glob;
901901
}
902902

903+
/**
904+
* Peek at a glob entry without vivifying it. Returns null if no glob has
905+
* been registered under this name. Used by anon-sub naming lookups
906+
* (see dev/modules/anon_sub_naming.md) to read *PKG::__ANON__'s
907+
* nameOverride without creating an empty glob as a side effect.
908+
*/
909+
public static RuntimeGlob peekGlobalIO(String key) {
910+
String resolvedKey = resolveStashHashRedirect(key);
911+
return globalIORefs.get(resolvedKey);
912+
}
913+
903914
/**
904915
* Retrieves a detached copy of a global IO reference, wrapped in a RuntimeScalar.
905916
*

src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,16 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar
22672267
if (code.subName != null && !code.subName.isEmpty()) {
22682268
String codePkg = code.packageName != null ? code.packageName : "main";
22692269
subName = codePkg + "::" + code.subName;
2270+
} else if (!code.explicitlyRenamed && code.packageName != null) {
2271+
// Anonymous sub: honor `local *PKG::__ANON__ = 'name'`
2272+
// by reading the package's *__ANON__ glob's nameOverride.
2273+
// See dev/modules/anon_sub_naming.md.
2274+
RuntimeGlob anonGlob = GlobalVariable.peekGlobalIO(
2275+
code.packageName + "::__ANON__");
2276+
if (anonGlob != null && anonGlob.nameOverride != null
2277+
&& !anonGlob.nameOverride.isEmpty()) {
2278+
subName = code.packageName + "::" + anonGlob.nameOverride;
2279+
}
22702280
}
22712281
}
22722282

@@ -2287,6 +2297,22 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar
22872297
}
22882298
}
22892299

2300+
// Honor `local *PKG::__ANON__ = 'name'` for any anonymous-sub
2301+
// frame, not just the innermost one. After both fallbacks,
2302+
// an anon frame ends up as "Pkg::__ANON__"; if the package's
2303+
// *__ANON__ glob currently has a name override active, swap
2304+
// it in. See dev/modules/anon_sub_naming.md.
2305+
if (subName != null && subName.endsWith("::__ANON__")) {
2306+
String anonPkg = subName.substring(0,
2307+
subName.length() - "::__ANON__".length());
2308+
RuntimeGlob anonGlob = GlobalVariable.peekGlobalIO(
2309+
anonPkg + "::__ANON__");
2310+
if (anonGlob != null && anonGlob.nameOverride != null
2311+
&& !anonGlob.nameOverride.isEmpty()) {
2312+
subName = anonPkg + "::" + anonGlob.nameOverride;
2313+
}
2314+
}
2315+
22902316
if (subName != null && !subName.isEmpty()) {
22912317
res.add(new RuntimeScalar(subName)); // subroutine
22922318
} else {

src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference
2727
RuntimeHash hashSlot;
2828
// Local code slot for detached globs (from stash delete)
2929
public RuntimeScalar codeSlot;
30+
// Dynamic name override set by `*foo = $string` glob-as-scalar assignment.
31+
// Used to honor the `local *PKG::__ANON__ = 'name'` idiom (see SUPER.pm,
32+
// Try::Tiny, namespace::clean) — caller()/Sub::Util::subname report this
33+
// string in place of __ANON__ for anonymous subs whose CvSTASH is PKG.
34+
// See dev/modules/anon_sub_naming.md.
35+
public String nameOverride;
3036

3137
/**
3238
* Tracks how many RuntimeScalar variables hold a GLOBREFERENCE to this glob.
@@ -327,6 +333,17 @@ public RuntimeScalar set(RuntimeScalar value) {
327333
} else {
328334
currentScalar.set(value);
329335
}
336+
// Record the dynamic name override for `local *PKG::__ANON__ =
337+
// 'name'`. We update the *current* glob in globalIORefs (which
338+
// is the freshly-created glob during a local scope), so readers
339+
// that look up by name see the override regardless of whether
340+
// they reached the lvalue before or after `local` swapped the
341+
// glob. See dev/modules/anon_sub_naming.md.
342+
if (this.globName != null) {
343+
RuntimeGlob currentGlob = GlobalVariable.peekGlobalIO(this.globName);
344+
if (currentGlob == null) currentGlob = this;
345+
currentGlob.nameOverride = value.toString();
346+
}
330347
return value;
331348
case FORMAT:
332349
// Handle format assignments to typeglobs

0 commit comments

Comments
 (0)