Skip to content

feat(stash): alias storage + name resolution for *Dst:: = *Src::#543

Closed
fglock wants to merge 2 commits intofix/sub-name-b-gv-introspectionfrom
fix/stash-aliasing
Closed

feat(stash): alias storage + name resolution for *Dst:: = *Src::#543
fglock wants to merge 2 commits intofix/sub-name-b-gv-introspectionfrom
fix/stash-aliasing

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 22, 2026

Summary

Implements Commits 1–3 of dev/prompts/stash-aliasing-plan.md to make *Dst:: = *Src::; actually alias the namespaces, matching Perl 5 semantics.

⚠️ Stacked on top of #541 (Sub::Name B::GV fix). Merge #541 first; GitHub will auto-retarget this PR to master when that happens.

What now works

package Src; sub hello {}
package main; no strict 'refs';
*Dst:: = *Src::;

\%Dst:: == \%Src::          # true
keys %Dst::                  # ['hello']
eval 'sub Dst::foo {}';
defined &Src::foo            # true
*{"Dst::sym"} = sub {};
defined &Src::sym            # true
${"Dst::var"} = 42;
${"Src::var"}                # 42
# chained:
*Mid:: = *Src::;
*Fin:: = *Mid::;
\%Fin:: == \%Src::           # true

Changes

  1. GlobalVariable infrastructure. New resolveAliasedFqn(fqn) plus a memoised transitive-resolution cache resolvedStashAliasCache with cycle cap. Fast path when stashAliases.isEmpty(); non-alias hits return the input string identity so callers can skip the substring+concat via ==. Invalidated on every setStashAlias/clearStashAlias/resetAllGlobals.

  2. Hash unification in RuntimeGlob.set(RuntimeGlob). When both glob names end with ::, also globalHashes.put(this.globName, getGlobalHash(value.globName)) so the two stash-view hashes share storage.

  3. NameNormalizer.normalizeVariableName applies resolveAliasedFqn after the existing cache, covering symbolic refs, eval "sub Dst::foo {}", and ${"Dst::var"}.

  4. SubroutineParser package/name split bugfix. placeholder.subName was being set from the raw parser token (may contain ::, e.g. "Dst::foo"), while packageName was derived from the alias-resolved fullName. caller() then concatenated to Src::Dst::foo. Now both halves are derived from the resolved fullName.

Caching strategy

resolveAliasedFqn is on the hot path for every global symbol access. Two layers protect it:

  • Empty-map fast path. if (stashAliases.isEmpty()) return fqn; — O(1) for programs that never use stash aliasing (i.e. almost all programs).
  • Transitive cache. Once an alias exists, each "Pkg::" maps in resolvedStashAliasCache to its terminal target. Chains collapsed once, reused forever (until a mutation clears the cache). Non-aliased packages map to their own string instance so the caller uses reference equality to shortcut the concat.

Test Impact

  • Sub-Name-0.28 t/exotic_names.t (the test that surfaced this): 1038 → 1168 passing (out of 1560) cumulative since fix: Sub::Name B::GV introspection + stash aliasing for *Dst:: = *Src:: #541.
  • Remaining 392 failures are all the "natively compiled sub" subtest that uses GV-level aliasing (${"palatable::"}{"sub"} = ${"palatable::"}{$encoded_sub}) — requires GVs with their own name field independent of the stash slot; deeper refactor, tracked in the plan doc.
  • perl5_t/t/op/stash.t + perl5_t/t/uni/stash.t: 75/105 → 75/105 — no regression.
  • make unit tests: all pass, including a new src/test/resources/unit/stash_aliasing.t covering hash identity, sub installation through alias, caller() reporting, symbolic refs, and chained aliases.

Test plan

  • make (unit tests)
  • New src/test/resources/unit/stash_aliasing.t — 5 subtests, all pass
  • perl dev/tools/perl_test_runner.pl perl5_t/t/op/stash.t perl5_t/t/uni/stash.t — 75/105, unchanged
  • jcpan -t Sub::Name — exotic_names.t 1038→1168 passing

Generated with Devin

fglock and others added 2 commits April 22, 2026 16:08
Documents the architecture, fix strategy, and staged implementation
plan for making typeglob-to-stash assignment unify the target's
namespace with the source's, as Perl 5 does. Referenced from the
PR body of this same branch; keeping it co-located with the Sub::Name
fix commit for context.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Implements Commits 1-3 of dev/prompts/stash-aliasing-plan.md:

1. Infrastructure in GlobalVariable:
   - resolveAliasedFqn(fqn): resolves the pkg prefix of an FQN through
     stashAliases. Fast path when the alias map is empty (no hashing,
     no substring).
   - resolvedStashAliasCache: memoised transitive resolution with cycle
     cap. Invalidated on setStashAlias/clearStashAlias/resetAllGlobals.
     Returns the input String identity for non-aliased packages so
     callers can fast-path with `==`.

2. Hash storage unification in RuntimeGlob.set(RuntimeGlob):
   When both names end with "::", also put the source's RuntimeStash
   into globalHashes under the destination key. This makes
   `\%Dst:: == \%Src::` true and `*Dst::{HASH} == *Src::{HASH}` true,
   matching Perl 5.

3. Route NameNormalizer.normalizeVariableName through resolveAliasedFqn:
   Applied after the existing normalisation cache so cached entries
   still participate in alias resolution. Covers symbolic refs,
   runtime-compiled subs (`sub Dst::foo {}` via eval), and qualified
   variable references.

4. Fix caller()-style package/name splitting in SubroutineParser:
   The placeholder.subName was previously set from the raw `subName`
   parameter, which may contain "::" (e.g. parsing `sub Dst::foo {}`
   arrives with subName="Dst::foo"). Combined with resolveAliasedFqn
   rewriting the full name to "Src::foo", this produced `code.subName
   = "Dst::foo"` and `code.packageName = "Src"` and caller() reported
   "Src::Dst::foo". Derive both halves from the resolved fullName
   instead.

Unit test added: src/test/resources/unit/stash_aliasing.t — covers
hash identity, sub installation through alias, caller() name,
symbolic refs, and chained aliases.

Impact on Sub-Name-0.28 t/exotic_names.t (stacked on #541):
  - Before #541: 0/1558 pass
  - After #541:  1038/1560 pass
  - With this PR: 1168/1560 pass (+130)

Remaining 392 failures are all in the "natively compiled sub" subtest
which uses GV-level aliasing:
  *palatable:: = *{"aliased::native::${pkg}::"};
  ${"palatable::"}{"sub"} = ${"palatable::"}{$encoded_sub};
That second line stores a GV named $encoded_sub under the hash key
"sub", so `sub palatable::sub{}` should install under the
$encoded_sub name. Supporting this requires first-class GV objects
with their own `name` field independent of the stash slot, which is
a larger architectural change tracked separately.

Verification:
  - make: all unit tests pass (including new stash_aliasing.t)
  - perl5_t/t/op/stash.t + perl5_t/t/uni/stash.t: 75/105, unchanged
  - Sub-Name-0.28 t/exotic_names.t: 0 -> 1168/1560 cumulative

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock force-pushed the fix/sub-name-b-gv-introspection branch from 299848a to b195dee Compare April 22, 2026 14:37
@fglock
Copy link
Copy Markdown
Owner Author

fglock commented Apr 22, 2026

Unified into #541 — stacked PR no longer needed; #541 now contains both commits (Sub::Name B::GV fix + stash aliasing).

@fglock fglock closed this Apr 22, 2026
@fglock fglock deleted the fix/stash-aliasing branch April 22, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant