Module Name:    src
Committed By:   rillig
Date:           Mon May 23 22:33:56 UTC 2022

Modified Files:
        src/distrib/sets/lists/tests: mi
        src/usr.bin/make/unit-tests: Makefile
Added Files:
        src/usr.bin/make/unit-tests: directive-for-empty.exp
            directive-for-empty.mk

Log Message:
tests/make: document and demonstrate .for i containing .if empty(i)

PR bin/43821 describes the inconsistency that in a '.for i' loop, the
condition '.if ${i:M*.c}' works since 2009 while the seemingly
equivalent condition '.if !empty(i:M*.c)' does not access the variable
'i' from the .for loop but instead the global 'i'.

Resolving this situation in a backwards-compatible and non-surprising
way is hard, as make has grown several features during the last 20 years
that interact in various edge cases.  For now, document the most obvious
pitfalls.


To generate a diff of this commit:
cvs rdiff -u -r1.1206 -r1.1207 src/distrib/sets/lists/tests/mi
cvs rdiff -u -r1.315 -r1.316 src/usr.bin/make/unit-tests/Makefile
cvs rdiff -u -r0 -r1.1 src/usr.bin/make/unit-tests/directive-for-empty.exp \
    src/usr.bin/make/unit-tests/directive-for-empty.mk

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: src/distrib/sets/lists/tests/mi
diff -u src/distrib/sets/lists/tests/mi:1.1206 src/distrib/sets/lists/tests/mi:1.1207
--- src/distrib/sets/lists/tests/mi:1.1206	Sun May 22 17:55:08 2022
+++ src/distrib/sets/lists/tests/mi	Mon May 23 22:33:56 2022
@@ -1,4 +1,4 @@
-# $NetBSD: mi,v 1.1206 2022/05/22 17:55:08 rillig Exp $
+# $NetBSD: mi,v 1.1207 2022/05/23 22:33:56 rillig Exp $
 #
 # Note: don't delete entries from here - mark them as "obsolete" instead.
 #
@@ -5630,6 +5630,8 @@
 ./usr/tests/usr.bin/make/unit-tests/directive-export-literal.mk			tests-usr.bin-tests	compattestfile,atf
 ./usr/tests/usr.bin/make/unit-tests/directive-export.exp			tests-usr.bin-tests	compattestfile,atf
 ./usr/tests/usr.bin/make/unit-tests/directive-export.mk				tests-usr.bin-tests	compattestfile,atf
+./usr/tests/usr.bin/make/unit-tests/directive-for-empty.exp			tests-usr.bin-tests	compattestfile,atf
+./usr/tests/usr.bin/make/unit-tests/directive-for-empty.mk			tests-usr.bin-tests	compattestfile,atf
 ./usr/tests/usr.bin/make/unit-tests/directive-for-errors.exp			tests-usr.bin-tests	compattestfile,atf
 ./usr/tests/usr.bin/make/unit-tests/directive-for-errors.mk			tests-usr.bin-tests	compattestfile,atf
 ./usr/tests/usr.bin/make/unit-tests/directive-for-escape.exp			tests-usr.bin-tests	compattestfile,atf

Index: src/usr.bin/make/unit-tests/Makefile
diff -u src/usr.bin/make/unit-tests/Makefile:1.315 src/usr.bin/make/unit-tests/Makefile:1.316
--- src/usr.bin/make/unit-tests/Makefile:1.315	Sun May  8 10:20:49 2022
+++ src/usr.bin/make/unit-tests/Makefile	Mon May 23 22:33:56 2022
@@ -1,4 +1,4 @@
-# $NetBSD: Makefile,v 1.315 2022/05/08 10:20:49 rillig Exp $
+# $NetBSD: Makefile,v 1.316 2022/05/23 22:33:56 rillig Exp $
 #
 # Unit tests for make(1)
 #
@@ -168,6 +168,7 @@ TESTS+=		directive-export-impl
 TESTS+=		directive-export-gmake
 TESTS+=		directive-export-literal
 TESTS+=		directive-for
+TESTS+=		directive-for-empty
 TESTS+=		directive-for-errors
 TESTS+=		directive-for-escape
 TESTS+=		directive-for-generating-endif

Added files:

Index: src/usr.bin/make/unit-tests/directive-for-empty.exp
diff -u /dev/null src/usr.bin/make/unit-tests/directive-for-empty.exp:1.1
--- /dev/null	Mon May 23 22:33:56 2022
+++ src/usr.bin/make/unit-tests/directive-for-empty.exp	Mon May 23 22:33:56 2022
@@ -0,0 +1,27 @@
+make: "directive-for-empty.mk" line 21: 2
+make: "directive-for-empty.mk" line 34: Missing argument for ".error"
+make: "directive-for-empty.mk" line 34: Missing argument for ".error"
+make: "directive-for-empty.mk" line 34: Missing argument for ".error"
+For: end for 1
+For: loop body:
+# The identifier 'empty' can only be used in conditions such as .if, .ifdef or
+# .elif.  In other lines the string 'empty(' must be preserved.
+CPPFLAGS+=	-Dmessage="empty(i)"
+# There may be whitespace between 'empty' and '('.
+.if ! empty (i)
+.  error
+.endif
+# Even in conditions, the string 'empty(' is not always a function call, it
+# can occur in a string literal as well.
+.if "empty\(i)" != "empty(i)"
+.  error
+.endif
+# In comments like 'empty(i)', the text must be preserved as well.
+#
+# Conditions, including function calls to 'empty', can not only occur in
+# condition directives, they can also occur in the modifier ':?', see
+# varmod-ifelse.mk.
+CPPFLAGS+=	-Dmacro="${empty(i):?empty:not-empty}"
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
Index: src/usr.bin/make/unit-tests/directive-for-empty.mk
diff -u /dev/null src/usr.bin/make/unit-tests/directive-for-empty.mk:1.1
--- /dev/null	Mon May 23 22:33:56 2022
+++ src/usr.bin/make/unit-tests/directive-for-empty.mk	Mon May 23 22:33:56 2022
@@ -0,0 +1,120 @@
+# $NetBSD: directive-for-empty.mk,v 1.1 2022/05/23 22:33:56 rillig Exp $
+#
+# Tests for .for loops containing conditions of the form 'empty(var:...)'.
+#
+# When a .for loop is expanded, variable expressions in the body of the loop
+# are replaced with expressions containing the variable values.  This
+# replacement is a bit naive but covers most of the practical cases.  The one
+# popular exception is the condition 'empty(var:Modifiers)', which does not
+# look like a variable expression and is thus not replaced.
+#
+# See also:
+#	https://gnats.netbsd.org/43821
+
+
+# In the body of the .for loop, the expression '${i:M*2*}' is replaced with
+# '${:U11:M*2*}', '${:U12:M*2*}', '${:U13:M*2*}', one after another.  This
+# replacement creates the impression that .for variables were real variables,
+# when in fact they aren't.
+.for i in 11 12 13
+.  if ${i:M*2*}
+.info 2
+.  endif
+.endfor
+
+
+# In conditions, the function call to 'empty' does not look like a variable
+# expression, therefore it is not replaced.  Since there is no global variable
+# named 'i', this expression makes for a leaky abstraction.  If the .for
+# variables were real variables, calling 'empty' would work on them as well.
+.for i in 11 12 13
+# Asking for an empty iteration variable does not make sense as the .for loop
+# splits the iteration items into words, and such a word cannot be empty.
+.  if empty(i)
+.    error			# due to the leaky abstraction
+.  endif
+# The typical way of using 'empty' with variables from .for loops is pattern
+# matching using the modifiers ':M' or ':N'.
+.  if !empty(i:M*2*)
+.    if ${i} != "12"
+.      error
+.    endif
+.  endif
+.endfor
+
+
+# The idea of replacing every occurrences of 'empty(i' in the body of a .for
+# loop would be naive and require many special cases, as there are many cases
+# that need to be considered when deciding whether the token 'empty' is a
+# function call or not, as demonstrated by the following examples.  For
+# variable expressions like '${i:Modifiers}', this is simpler as a single
+# dollar almost always starts a variable expression.  For counterexamples and
+# edge cases, see directive-for-escape.mk.  Adding another such tricky detail
+# is out of the question.
+.MAKEFLAGS: -df
+.for i in value
+# The identifier 'empty' can only be used in conditions such as .if, .ifdef or
+# .elif.  In other lines the string 'empty(' must be preserved.
+CPPFLAGS+=	-Dmessage="empty(i)"
+# There may be whitespace between 'empty' and '('.
+.if ! empty (i)
+.  error
+.endif
+# Even in conditions, the string 'empty(' is not always a function call, it
+# can occur in a string literal as well.
+.if "empty\(i)" != "empty(i)"
+.  error
+.endif
+# In comments like 'empty(i)', the text must be preserved as well.
+#
+# Conditions, including function calls to 'empty', can not only occur in
+# condition directives, they can also occur in the modifier ':?', see
+# varmod-ifelse.mk.
+CPPFLAGS+=	-Dmacro="${empty(i):?empty:not-empty}"
+.endfor
+.MAKEFLAGS: -d0
+
+
+# An idea to work around the above problems is to collect the variables from
+# the .for loops in a separate scope.  To match the current behavior, there
+# has to be one scope per included file.  There may be .for loops using the
+# same variable name in files that include each other:
+#
+# outer.mk:	.for i in outer
+#		.  info $i		# outer
+#		.  include "inner.mk"
+# inner.mk:	.    info $i		# (undefined)
+#		.    for i in inner
+#		.      info $i		# inner
+#		.    endfor
+#		.    info $i		# (undefined)
+# outer.mk:	.  info $i		# outer
+#		.endfor
+#
+# This might be regarded another leaky abstraction, but it is in fact useful
+# that variables from .for loops can only affect expressions in the current
+# file.  If variables from .for loops were implemented as global variables,
+# they might interact between files.
+#
+# To emulate this exact behavior for the function 'empty', each file in the
+# stack of included files needs its own scope that is independent from the
+# other files.
+#
+# Another tricky detail are nested .for loops in a single file that use the
+# same variable name.  These are generally avoided by developers, as they
+# would be difficult to understand for humans as well.  Technically, they are
+# possible though.  Assuming there are two nested .for loops, both using the
+# variable 'i'.  When the inner .for loop ends, the inner 'i' needs to be
+# removed from the scope, which would need to make the outer 'i' visible
+# again.  This would suggest to use one variable scope per .for loop.
+#
+# Using a separate scope has the benefit that Var_Parse already allows for
+# a custom scope to be passed as parameter.  This would have another side
+# effect though.  There are several modifiers that actually modify variables,
+# and these modifications happen in the scope that is passed to Var_Parse.
+# This would mean that the combination of a .for variable and the modifiers
+# '::=', '::+=', '::?=', '::!=' and ':_' would lead to different behavior than
+# before.
+
+# TODO: Add code that demonstrates the current interaction between variables
+#  from .for loops and the modifiers mentioned above.

Reply via email to