Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
Re: A solution to fix hidden references in reference chains
- X-seq: zsh-workers 54047
- From: Philippe Altherr <philippe.altherr@xxxxxxxxx>
- To: Zsh hackers list <zsh-workers@xxxxxxx>
- Subject: Re: A solution to fix hidden references in reference chains
- Date: Sat, 8 Nov 2025 17:45:30 +0100
- Archived-at: <https://zsh.org/workers/54047>
- In-reply-to: <CAGdYchvU3=Zzk8q-V36EEf9WDHd6Y9Mjgg=mkKFVRn0BbGuAcA@mail.gmail.com>
- List-id: <zsh-workers.zsh.org>
- References: <CAGdYchu3rxH9MBanWDLTuWcaUNh+g1suQD1bRmC+Pbcj+QJY1A@mail.gmail.com> <CAGdYchvU3=Zzk8q-V36EEf9WDHd6Y9Mjgg=mkKFVRn0BbGuAcA@mail.gmail.com>
Here is an updated patch. I slightly changed the code and rebased onto the latest head.
Philippe
Whenever the "base" field of a "Param" is set, if "base" is strictly greater than the "level" field, we know the "Param" refers to a nested variable. We also know that its "base" will have to be updated when the "base" scope is exited. We can therefore add the "Param" to a list linked to the "base" scope.
Whenever a scope is exited, first check whether any "Param"s were added to its list. If yes, the "base" fields of these are updated (if the "Param"s are still named references). Then call a "scanendscope" that doesn't do anything special for named references. Or do it in the opposite order?
This assumes that "Params" are only ever freed by "scanendscope". If that's not the case, things get more complicated as one would also sometimes have to remove "Param"s from the scope lists.
I could try to prepare a patch that implements this but I would first need advice on the preferred way to implement the lists of "Param"s (linked list, new "Param" field, ...) and where/how to store them (global array, under special names in the parameter table, ...).
As a reminder, this would fix "ref2" in
this test such that the output for "ref3" is the same as the one for "ref1".
Philippe
diff --git a/Src/params.c b/Src/params.c
index 5a0434e40..5e9387339 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
@@ -5803,6 +5821,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 */
@@ -5830,6 +5849,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_UPPER) &&
+ pm->base > locallevel) {
+ setscope_base(pm, locallevel);
+ }
+ }
unqueue_signals();
}
@@ -5838,9 +5864,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
@@ -5903,14 +5927,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;
}
}
@@ -6393,7 +6409,7 @@ setscope(Param pm)
(basepm = (Param)gethashnode2(realparamtab, refname)) &&
(basepm = (Param)loadparamnode(realparamtab, basepm, refname)) &&
(!(basepm->node.flags & PM_NEWREF) || (basepm = basepm->old))) {
- pm->base = basepm->level;
+ setscope_base(pm, basepm->level);
}
if (pm->base > pm->level) {
if (EMULATION(EMULATE_KSH)) {
@@ -6442,6 +6458,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, int reflevel)
diff --git a/Test/K01nameref.ztst b/Test/K01nameref.ztst
index de9707c5e..accb68e07 100644
--- a/Test/K01nameref.ztst
+++ b/Test/K01nameref.ztst
@@ -1170,8 +1170,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
Messages sorted by:
Reverse Date,
Date,
Thread,
Author