54262: track and revert hidden references in chains across locallevel scopes

This commit is contained in:
Philippe Altherr
2026-03-31 15:06:30 -07:00
committed by Bart Schaefer
parent d8d74ef682
commit 8edd5029de
3 changed files with 135 additions and 13 deletions

View File

@@ -1,5 +1,9 @@
2026-03-31 Bart Schaefer <schaefer@zsh.org>
* Philippe: 54262: Src/params.c, Test/K01nameref.ztst: track
and revert hidden references in chains that extend across
locallevel scopes
* Philippe: 54261: Src/builtin.c, Src/params.c, Src/zsh.h,
Test/K01nameref.ztst, Test/V10private.ztst: `unset -n` removes
named-reference-ness of a parameter when removing the referent.

View File

@@ -485,6 +485,24 @@ static initparam argvparam_pm = IPDEF9("", &pparams, NULL, \
static Param argvparam;
/*
* Lists of references to nested variables ("Param" instances) indexed
* by scope. Whenever the "base" scope of a named reference is set to
* refer to a variable more deeply nested than the reference itself
* ("base > level"), the "base" scope has to be updated once the
* "base" scope ends. The "scoperefs" lists keep track of these
* references. Since "Param" instances get reused when variables with
* the same name are redefined in the same scope, listed "Param"
* instances may no longer be references when the scope ends or may
* refer to a different "base" scope. A given "Param" instance may
* also be included in multiple lists at the same time or multiple
* times in the same list. Non of that is harmful as long as only
* instances that are still references referring to the ending scope
* are updated when the scope ends.
*/
static LinkList *scoperefs = NULL;
static int scoperefs_num = 0;
/* "parameter table" - hash table containing the parameters
*
* realparamtab always points to the shell's global table. paramtab is sometimes
@@ -5855,6 +5873,7 @@ static int lc_update_needed;
mod_export void
endparamscope(void)
{
LinkList refs = locallevel < scoperefs_num ? scoperefs[locallevel] : NULL;
queue_signals();
locallevel--;
/* This pops anything from a higher locallevel */
@@ -5882,6 +5901,13 @@ endparamscope(void)
clear_mbstate(); /* LC_CTYPE may have changed */
}
#endif /* USE_LOCALE */
/* Reset scope of namerefs that refer to dead variables */
for (Param pm; refs && (pm = (Param)getlinknode(refs));) {
if ((pm->node.flags & PM_NAMEREF) && !(pm->node.flags & PM_UNSET) &&
!(pm->node.flags & PM_UPPER) && pm->base > locallevel) {
setscope_base(pm, locallevel);
}
}
unqueue_signals();
}
@@ -5890,9 +5916,7 @@ static void
scanendscope(HashNode hn, UNUSED(int flags))
{
Param pm = (Param)hn;
Param hidden = NULL;
if (pm->level > locallevel) {
hidden = pm->old;
if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
/*
* Removable specials are normal in that they can be removed
@@ -5956,14 +5980,6 @@ scanendscope(HashNode hn, UNUSED(int flags))
export_param(pm);
} else
unsetparam_pm(pm, 0, 0);
pm = NULL;
}
if (hidden)
pm = hidden;
if (pm && (pm->node.flags & PM_NAMEREF) &&
pm->base >= pm->level && pm->base >= locallevel) {
/* Should never get here for a -u reference */
pm->base = locallevel;
}
}
@@ -6405,7 +6421,7 @@ setscope(Param pm)
(basepm = (Param)gethashnode2(realparamtab, refname)) &&
(basepm = (Param)loadparamnode(realparamtab, basepm, refname)) &&
(basepm != pm || !basepm->old || (basepm = basepm->old))) {
pm->base = basepm->level;
setscope_base(pm, basepm->level);
}
if (pm->base > pm->level) {
if (EMULATION(EMULATE_KSH)) {
@@ -6431,6 +6447,25 @@ setscope(Param pm)
unqueue_signals();
}
/**/
static void
setscope_base(Param pm, int base)
{
if ((pm->base = base) > pm->level) {
LinkList refs;
if (base >= scoperefs_num) {
int old_num = scoperefs_num;
int new_num = scoperefs_num = MAX(2 * base, 8);
scoperefs = zrealloc(scoperefs, new_num * sizeof(refs));
memset(scoperefs + old_num, 0, (new_num - old_num) * sizeof(refs));
}
refs = scoperefs[base];
if (!refs)
refs = scoperefs[base] = znewlinklist();
zpushnode(refs, pm);
}
}
/**/
static Param
upscope(Param pm, const Param ref)

View File

@@ -1247,8 +1247,8 @@ F:previously this could create an infinite recursion and crash
0:Transitive references with scoping changes
>f4: ref1=f4 ref2=XX ref3=f4
>f3: ref1=f3 ref2=XX ref3=f3
>g5: ref1=f3 ref2=XX ref3=g4
>g4: ref1=f3 ref2=XX ref3=g4
>g5: ref1=f3 ref2=XX ref3=f3
>g4: ref1=f3 ref2=XX ref3=f3
>f3: ref1=f3 ref2=XX ref3=f3
>f2: ref1=f1 ref2=XX ref3=f1
>f1: ref1=f1 ref2=f1 ref3=f1
@@ -1885,4 +1885,87 @@ F:converting from association/array to string should work here too
># d:reference to not-yet-defined - local - ref1
>typeset -i var=42
typeset -n ref1
typeset -n ref2
typeset -n ref3=ref2
typeset var=aaa
() {
typeset -i ref2=123 # Hides the reference ref2 in this scope and nested scopes
typeset var=bbb
() {
typeset var=ccc
ref1=var
ref3=var # From now on ref1 and ref3 should always refer to the same variable
echo A:ref1=$ref1 ref2=$ref2 ref3=$ref3
} # Both top-level references ref1 and ref2 should be rebound
echo B:ref1=$ref1 ref2=$ref2 ref3=$ref3
() {
typeset var=ddd # No reference should refer to this variable
echo C:ref1=$ref1 ref2=$ref2 ref3=$ref3
}
echo D:ref1=$ref1 ref2=$ref2 ref3=$ref3
} # Both top-level references ref1 and ref2 should be rebound
echo E:ref1=$ref1 ref2=$ref2 ref3=$ref3
() {
typeset var=eee # No reference should refer to this variable
echo F:ref1=$ref1 ref2=$ref2 ref3=$ref3
}
echo G:ref1=$ref1 ref2=$ref2 ref3=$ref3
0:hidden reference refers to a nested variable
>A:ref1=ccc ref2=123 ref3=ccc
>B:ref1=bbb ref2=123 ref3=bbb
>C:ref1=bbb ref2=123 ref3=bbb
>D:ref1=bbb ref2=123 ref3=bbb
>E:ref1=aaa ref2=aaa ref3=aaa
>F:ref1=aaa ref2=aaa ref3=aaa
>G:ref1=aaa ref2=aaa ref3=aaa
typeset ref
typeset var1=var1@scope1
typeset var2=var2@scope1
() { # enter scope 2
typeset var1=var1@scope2
typeset var2=var2@scope2
typeset -g -n ref=var1; echo A:$ref # ref added to scope 2
typeset -g -n ref=var2; echo B:$ref # ref added to scope 2
() { # enter scope 3
typeset var1=var1@scope3
typeset -g -n ref=var1; echo C:$ref # ref added to scope 3
() { # enter scope 4
typeset var1=var1@scope4
typeset var2=var2@scope4
typeset -g -n ref=var1; echo D:$ref # ref added to scope 4
typeset -g -n ref=var2; echo E:$ref # ref added to scope 4
} # leave scope 4: ref rebound to var2 in scope 2 and added to scope 2
echo F:$ref
} # leave scope 3: ref remains bound to var2 in scope 2
echo G:$ref
unset -n ref # ref is unset
echo H:$ref
} # leave scope 2: ref remains unset
echo I:$ref
0:reference refers successively to multiple variables in multiple nested scopes
>A:var1@scope2
>B:var2@scope2
>C:var1@scope3
>D:var1@scope4
>E:var2@scope4
>F:var2@scope2
>G:var2@scope2
>H:
>I:
typeset ref
() { # enter scope 2
typeset var=var
typeset -g -n ref=var; echo A:$ref # ref added to scope 2
unset -n ref
typeset -g -i16 ref=255; echo B:$ref # ref becomes an integer in base 16
} # leave scope 2: ref remains an integer in base 16
echo C:$ref
0:reference referring to a nested variable becomes an integer
>A:var
>B:16#FF
>C:16#FF
%clean