make(1): add more detailed debug logging for variable modifiers

Before a modifier is applied to a variable, it is not yet parsed,
therefore it is only possible to log a rough estimate of the modifier.
But after applying it, the parsing position has advanced, and the full
modifier can be logged.

In addition, to fully understand how the modifiers work, it's not enough
to just know the variable names and values, there are also some flags
that influence how the modifiers behave.  The most influential is
VARE_WANTRES.

Thanks to sjg for the extensive review and valuable feedback on the
first drafts.
This commit is contained in:
rillig 2020-08-08 13:50:23 +00:00
parent 2b496df1e1
commit 619278f158
6 changed files with 285 additions and 64 deletions

View File

@ -1,4 +1,4 @@
# $NetBSD: Makefile,v 1.82 2020/08/07 05:13:04 rillig Exp $
# $NetBSD: Makefile,v 1.83 2020/08/08 13:50:23 rillig Exp $
# @(#)Makefile 5.2 (Berkeley) 12/28/90
PROG= make
@ -7,6 +7,7 @@ SRCS+= buf.c
SRCS+= compat.c
SRCS+= cond.c
SRCS+= dir.c
SRCS+= enum.c
SRCS+= for.c
SRCS+= hash.c
SRCS+= job.c
@ -26,6 +27,7 @@ SRCS+= util.c
HDRS= buf.h
HDRS+= config.h
HDRS+= dir.h
HDRS+= enum.h
HDRS+= hash.h
HDRS+= job.h
HDRS+= lst.h

83
usr.bin/make/enum.c Normal file
View File

@ -0,0 +1,83 @@
/* $NetBSD: enum.c,v 1.1 2020/08/08 13:50:23 rillig Exp $ */
/*
Copyright (c) 2020 Roland Illig <rillig@NetBSD.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef MAKE_NATIVE
static char rcsid[] = "$NetBSD: enum.c,v 1.1 2020/08/08 13:50:23 rillig Exp $";
#else
#include <sys/cdefs.h>
#ifndef lint
__RCSID("$NetBSD: enum.c,v 1.1 2020/08/08 13:50:23 rillig Exp $");
#endif
#endif
#include <assert.h>
#include <string.h>
#include "enum.h"
/* Convert a bitset into a string representation showing the names of the
* individual bits, or optionally shortcuts for groups of bits. */
const char *
Enum_ToString(char *buf, size_t buf_size, int value,
const EnumToStringSpec *spec)
{
const char *buf_start = buf;
const char *sep = "";
size_t sep_len = 0;
for (; spec->es_value != 0; spec++) {
size_t name_len;
if ((value & spec->es_value) != spec->es_value)
continue;
value &= ~spec->es_value;
assert(buf_size >= sep_len + 1);
memcpy(buf, sep, sep_len);
buf += sep_len;
buf_size -= sep_len;
name_len = strlen(spec->es_name);
assert(buf_size >= name_len + 1);
memcpy(buf, spec->es_name, name_len);
buf += name_len;
buf_size -= name_len;
sep = ENUM__SEP;
sep_len = sizeof ENUM__SEP - 1;
}
assert(value == 0);
if (buf == buf_start)
return "none";
assert(buf_size >= 1);
buf[0] = '\0';
return buf_start;
}

93
usr.bin/make/enum.h Normal file
View File

@ -0,0 +1,93 @@
/* $NetBSD: enum.h,v 1.1 2020/08/08 13:50:23 rillig Exp $ */
/*
Copyright (c) 2020 Roland Illig <rillig@NetBSD.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef MAKE_ENUM_H
#define MAKE_ENUM_H
/*
* Generate string representation for bitmasks.
*/
typedef struct {
int es_value;
const char *es_name;
} EnumToStringSpec;
const char *Enum_ToString(char *, size_t, int, const EnumToStringSpec *);
#define ENUM__SEP "|"
#define ENUM__JOIN_1(v1) \
#v1
#define ENUM__JOIN_2(v1, v2) \
#v1 ENUM__SEP ENUM__JOIN_1(v2)
#define ENUM__JOIN_3(v1, v2, v3) \
#v1 ENUM__SEP ENUM__JOIN_2(v2, v3)
#define ENUM__JOIN_4(v1, v2, v3, v4) \
#v1 ENUM__SEP ENUM__JOIN_3(v2, v3, v4)
#define ENUM__JOIN_5(v1, v2, v3, v4, v5) \
#v1 ENUM__SEP ENUM__JOIN_4(v2, v3, v4, v5)
#define ENUM__JOIN_6(v1, v2, v3, v4, v5, v6) \
#v1 ENUM__SEP ENUM__JOIN_5(v2, v3, v4, v5, v6)
#define ENUM__JOIN_7(v1, v2, v3, v4, v5, v6, v7) \
#v1 ENUM__SEP ENUM__JOIN_6(v2, v3, v4, v5, v6, v7)
#define ENUM__RTTI(typnam, specs, joined) \
static const EnumToStringSpec typnam ## _ ## ToStringSpecs[] = specs; \
static const size_t typnam ## _ ## ToStringSize = sizeof joined
#define ENUM__SPEC(v) { v, #v }
#define ENUM__SPEC_3(v1, v2, v3) { \
ENUM__SPEC(v1), \
ENUM__SPEC(v2), \
ENUM__SPEC(v3), \
{ 0, "" } }
#define ENUM__SPEC_7(v1, v2, v3, v4, v5, v6, v7) { \
ENUM__SPEC(v1), \
ENUM__SPEC(v2), \
ENUM__SPEC(v3), \
ENUM__SPEC(v4), \
ENUM__SPEC(v5), \
ENUM__SPEC(v6), \
ENUM__SPEC(v7), \
{ 0, "" } }
#define ENUM_RTTI_3(typnam, v1, v2, v3) \
ENUM__RTTI(typnam, \
ENUM__SPEC_3(v1, v2, v3), \
ENUM__JOIN_3(v1, v2, v3))
#define ENUM_RTTI_7(typnam, v1, v2, v3, v4, v5, v6, v7) \
ENUM__RTTI(typnam, \
ENUM__SPEC_7(v1, v2, v3, v4, v5, v6, v7), \
ENUM__JOIN_7(v1, v2, v3, v4, v5, v6, v7))
#endif

View File

@ -2,64 +2,64 @@ Global:RELEVANT = yes (load-time part)
Global:COUNTER =
Global:NEXT = ${COUNTER::=${COUNTER} a}${COUNTER:[#]}
Global:A =
Applying[COUNTER] :: to ""
Applying ${COUNTER::...} to "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: " a"
Global:COUNTER = a
Result[COUNTER] of :: is ""
Applying[COUNTER] :[ to " a"
Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Applying ${COUNTER:[...} to " a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: "#"
Result[COUNTER] of :[ is "1"
Result of ${COUNTER:[#]} is "1" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Global:A = ${COUNTER::= a a}1
Global:B =
Applying[COUNTER] :: to " a"
Applying ${COUNTER::...} to " a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: " a a"
Global:COUNTER = a a
Result[COUNTER] of :: is ""
Applying[COUNTER] :[ to " a a"
Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Applying ${COUNTER:[...} to " a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: "#"
Result[COUNTER] of :[ is "2"
Result of ${COUNTER:[#]} is "2" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Global:B = ${COUNTER::= a a a}2
Global:C =
Applying[COUNTER] :: to " a a"
Applying ${COUNTER::...} to " a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: " a a a"
Global:COUNTER = a a a
Result[COUNTER] of :: is ""
Applying[COUNTER] :[ to " a a a"
Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Applying ${COUNTER:[...} to " a a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Modifier part: "#"
Result[COUNTER] of :[ is "3"
Result of ${COUNTER:[#]} is "3" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
Global:C = ${COUNTER::= a a a a}3
Global:RELEVANT = no
Global:RELEVANT = yes (run-time part)
Result[RELEVANT] of :: is ""
Applying[COUNTER] :: to " a a a"
Result of ${RELEVANT::=yes (run-time part)} is "" (eflags = VARE_WANTRES, vflags = none)
Applying ${COUNTER::...} to " a a a" (eflags = VARE_WANTRES, vflags = none)
Modifier part: " a a"
Global:COUNTER = a a
Result[COUNTER] of :: is ""
Applying[A] :Q to "1"
Result of ${COUNTER::= a a} is "" (eflags = VARE_WANTRES, vflags = none)
Applying ${A:Q} to "1" (eflags = VARE_WANTRES, vflags = none)
QuoteMeta: [1]
Result[A] of :Q is "1"
Applying[COUNTER] :: to " a a"
Result of ${A:Q} is "1" (eflags = VARE_WANTRES, vflags = none)
Applying ${COUNTER::...} to " a a" (eflags = VARE_WANTRES, vflags = none)
Modifier part: " a a a"
Global:COUNTER = a a a
Result[COUNTER] of :: is ""
Applying[B] :Q to "2"
Result of ${COUNTER::= a a a} is "" (eflags = VARE_WANTRES, vflags = none)
Applying ${B:Q} to "2" (eflags = VARE_WANTRES, vflags = none)
QuoteMeta: [2]
Result[B] of :Q is "2"
Applying[COUNTER] :: to " a a a"
Result of ${B:Q} is "2" (eflags = VARE_WANTRES, vflags = none)
Applying ${COUNTER::...} to " a a a" (eflags = VARE_WANTRES, vflags = none)
Modifier part: " a a a a"
Global:COUNTER = a a a a
Result[COUNTER] of :: is ""
Applying[C] :Q to "3"
Result of ${COUNTER::= a a a a} is "" (eflags = VARE_WANTRES, vflags = none)
Applying ${C:Q} to "3" (eflags = VARE_WANTRES, vflags = none)
QuoteMeta: [3]
Result[C] of :Q is "3"
Applying[COUNTER] :[ to " a a a a"
Result of ${C:Q} is "3" (eflags = VARE_WANTRES, vflags = none)
Applying ${COUNTER:[...} to " a a a a" (eflags = VARE_WANTRES, vflags = none)
Modifier part: "#"
Result[COUNTER] of :[ is "4"
Applying[COUNTER] :Q to "4"
Result of ${COUNTER:[#]} is "4" (eflags = VARE_WANTRES, vflags = none)
Applying ${COUNTER:Q} to "4" (eflags = VARE_WANTRES, vflags = none)
QuoteMeta: [4]
Result[COUNTER] of :Q is "4"
Result of ${COUNTER:Q} is "4" (eflags = VARE_WANTRES, vflags = none)
A=1 B=2 C=3 COUNTER=4
Applying[RELEVANT] :: to "yes (run-time part)"
Applying ${RELEVANT::...} to "yes (run-time part)" (eflags = VARE_WANTRES, vflags = none)
Modifier part: "no"
Global:RELEVANT = no
exit status 0

View File

@ -3,59 +3,59 @@ Global:VAR = added
Global:VAR = overwritten
Global:delete VAR
Global:delete VAR (not found)
Applying[] :U to ""
Result[] of :U is ""
Applying ${:U} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
Result of ${:U} is "" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Var_Set("${:U}", "empty name", ...) name expands to empty string - ignored
Applying[] :U to ""
Result[] of :U is ""
Applying ${:U} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
Result of ${:U} is "" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Var_Append("${:U}", "empty name", ...) name expands to empty string - ignored
Global:FROM_CMDLINE = overwritten ignored!
Global:VAR = 1
Global:VAR = 1 2
Global:VAR = 1 2 3
Applying[VAR] :M to "1 2 3"
Applying ${VAR:M...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Pattern[VAR] for [1 2 3] is [[2]]
ModifyWords: split "1 2 3" into 3 words
VarMatch [1] [[2]]
VarMatch [2] [[2]]
VarMatch [3] [[2]]
Result[VAR] of :M is "2"
Applying[VAR] :N to "1 2 3"
Result of ${VAR:M[2]} is "2" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:N...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Pattern[VAR] for [1 2 3] is [[2]]
ModifyWords: split "1 2 3" into 3 words
Result[VAR] of :N is "1 3"
Applying[VAR] :S to "1 2 3"
Result of ${VAR:N[2]} is "1 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:S...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Modifier part: "2"
Modifier part: "two"
ModifyWords: split "1 2 3" into 3 words
Result[VAR] of :S is "1 two 3"
Applying[VAR] :Q to "1 2 3"
Result of ${VAR:S,2,two,} is "1 two 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:Q} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
QuoteMeta: [1\ 2\ 3]
Result[VAR] of :Q is "1\ 2\ 3"
Applying[VAR] :t to "1 2 3"
Result[VAR] of :t is "1 2 3"
Applying[VAR] :t to "1 2 3"
Result[VAR] of :t is "1 2 3"
Applying[VAR] :Q to "1 2 3"
Result of ${VAR:Q} is "1\ 2\ 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:t...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Result of ${VAR:tu} is "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:t...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Result of ${VAR:tl} is "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${VAR:Q} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
QuoteMeta: [1\ 2\ 3]
Result[VAR] of :Q is "1\ 2\ 3"
Applying[] :U to ""
Result[] of :U is "value"
Applying[] :U to ""
Result[] of :U is "M*e"
Result of ${VAR:Q} is "1\ 2\ 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
Applying ${:U...} to "" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK)
Result of ${:Uvalue} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Applying ${:U...} to "" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK)
Result of ${:UM*e} is "M*e" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Indirect modifier "M*e" from "${:UM*e}"
Applying[] :M to "value"
Applying ${:M...} to "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Pattern[] for [value] is [*e]
ModifyWords: split "value" into 1 words
VarMatch [value] [*e]
Result[] of :M is "value"
Applying[] :M to "value"
Result of ${:M*e} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Applying ${:M...} to "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Pattern[] for [value] is [valu[e]]
ModifyWords: split "value" into 1 words
VarMatch [value] [valu[e]]
Result[] of :M is "value"
Applying[] :U to ""
Result[] of :U is "VAR"
Result of ${:Mvalu[e]} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Applying ${:U...} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
Result of ${:UVAR} is "VAR" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
Global:delete VAR
Global:RELEVANT = no
exit status 0

View File

@ -1,4 +1,4 @@
/* $NetBSD: var.c,v 1.429 2020/08/08 13:31:24 rillig Exp $ */
/* $NetBSD: var.c,v 1.430 2020/08/08 13:50:23 rillig Exp $ */
/*
* Copyright (c) 1988, 1989, 1990, 1993
@ -69,14 +69,14 @@
*/
#ifndef MAKE_NATIVE
static char rcsid[] = "$NetBSD: var.c,v 1.429 2020/08/08 13:31:24 rillig Exp $";
static char rcsid[] = "$NetBSD: var.c,v 1.430 2020/08/08 13:50:23 rillig Exp $";
#else
#include <sys/cdefs.h>
#ifndef lint
#if 0
static char sccsid[] = "@(#)var.c 8.3 (Berkeley) 3/19/94";
#else
__RCSID("$NetBSD: var.c,v 1.429 2020/08/08 13:31:24 rillig Exp $");
__RCSID("$NetBSD: var.c,v 1.430 2020/08/08 13:50:23 rillig Exp $");
#endif
#endif /* not lint */
#endif
@ -131,6 +131,7 @@ __RCSID("$NetBSD: var.c,v 1.429 2020/08/08 13:31:24 rillig Exp $");
#include <stdlib.h>
#include <time.h>
#include "enum.h"
#include "make.h"
#include "buf.h"
#include "dir.h"
@ -145,6 +146,11 @@ __RCSID("$NetBSD: var.c,v 1.429 2020/08/08 13:31:24 rillig Exp $");
#define VAR_DEBUG(fmt, ...) VAR_DEBUG_IF(TRUE, fmt, __VA_ARGS__)
ENUM_RTTI_3(VarEvalFlags,
VARE_UNDEFERR,
VARE_WANTRES,
VARE_ASSIGN);
/*
* This lets us tell if we have replaced the original environ
* (which we cannot free).
@ -218,6 +224,15 @@ typedef enum {
VAR_FROM_CMD = 0x40 /* Variable came from command line */
} VarFlags;
ENUM_RTTI_7(VarFlags,
VAR_IN_USE,
VAR_FROM_ENV,
VAR_JUNK,
VAR_KEEP,
VAR_EXPORTED,
VAR_REEXPORT,
VAR_FROM_CMD);
typedef struct Var {
char *name; /* the variable's name; it is allocated for
* environment variables and aliased to the
@ -3019,10 +3034,27 @@ ApplyModifiers(
continue;
}
apply_mods:
VAR_DEBUG("Applying[%s] :%c to \"%s\"\n", st.v->name, *p, st.val);
st.newVal = var_Error; /* default value, in case of errors */
res = AMR_BAD; /* just a safe fallback */
mod = p;
if (DEBUG(VAR)) {
char vflags_str[VarFlags_ToStringSize];
char eflags_str[VarEvalFlags_ToStringSize];
Boolean is_single_char = mod[0] != '\0' &&
(mod[1] == endc || mod[1] == ':');
/* At this point, only the first character of the modifier can
* be used since the end of the modifier is not yet known. */
VAR_DEBUG("Applying ${%s:%c%s} to \"%s\" "
"(eflags = %s, vflags = %s)\n",
st.v->name, mod[0], is_single_char ? "" : "...", st.val,
Enum_ToString(eflags_str, sizeof eflags_str, st.eflags,
VarEvalFlags_ToStringSpecs),
Enum_ToString(vflags_str, sizeof vflags_str, st.v->flags,
VarFlags_ToStringSpecs));
}
switch (*mod) {
case ':':
res = ApplyModifier_Assign(&p, &st);
@ -3177,7 +3209,18 @@ ApplyModifiers(
if (res == AMR_BAD)
goto bad_modifier;
VAR_DEBUG("Result[%s] of :%c is \"%s\"\n", st.v->name, *mod, st.newVal);
if (DEBUG(VAR)) {
char eflags_str[VarEvalFlags_ToStringSize];
char vflags_str[VarFlags_ToStringSize];
VAR_DEBUG("Result of ${%s:%.*s} is \"%s\" "
"(eflags = %s, vflags = %s)\n",
st.v->name, (int)(p - mod), mod, st.newVal,
Enum_ToString(eflags_str, sizeof eflags_str, st.eflags,
VarEvalFlags_ToStringSpecs),
Enum_ToString(vflags_str, sizeof vflags_str, st.v->flags,
VarFlags_ToStringSpecs));
}
if (st.newVal != st.val) {
if (*freePtr) {