summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilippe Altherr <philippe.altherr@gmail.com>2026-03-31 15:06:30 -0700
committerBart Schaefer <schaefer@zsh.org>2026-03-31 18:32:26 -0700
commit8edd5029de35ccb988902381511c4c3db6ee0604 (patch)
tree4baa422d4138156ca48a5c6339ecf52e6e3e82dd
parent54261: `unset -n` removes named-reference type as well as removes referent (diff)
downloadzsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar.gz
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar.bz2
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar.lz
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar.xz
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.tar.zst
zsh-8edd5029de35ccb988902381511c4c3db6ee0604.zip
54262: track and revert hidden references in chains across locallevel scopes
-rw-r--r--ChangeLog4
-rw-r--r--Src/params.c57
-rw-r--r--Test/K01nameref.ztst87
3 files changed, 135 insertions, 13 deletions
diff --git a/ChangeLog b/ChangeLog
index 3067a3e6f..0b7f5ec6b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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.
diff --git a/Src/params.c b/Src/params.c
index afc67eb14..4f5454abb 100644
--- a/Src/params.c
+++ b/Src/params.c
@@ -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)) {
@@ -6432,6 +6448,25 @@ setscope(Param pm)
}
/**/
+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)
{
diff --git a/Test/K01nameref.ztst b/Test/K01nameref.ztst
index fb27e7261..82ccbfb89 100644
--- a/Test/K01nameref.ztst
+++ b/Test/K01nameref.ztst
@@ -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