xref: /freebsd/contrib/bmake/unit-tests/directive-for-empty.mk (revision 22d7dd834bc5cd189810e414701e3ad1e98102e4)
1# $NetBSD: directive-for-empty.mk,v 1.2 2023/06/01 20:56:35 rillig Exp $
2#
3# Tests for .for loops containing conditions of the form 'empty(var:...)'.
4#
5# When a .for loop is expanded, variable expressions in the body of the loop
6# are replaced with expressions containing the variable values.  This
7# replacement is a bit naive but covers most of the practical cases.  The one
8# popular exception is the condition 'empty(var:Modifiers)', which does not
9# look like a variable expression and is thus not replaced.
10#
11# See also:
12#	https://gnats.netbsd.org/43821
13
14
15# In the body of the .for loop, the expression '${i:M*2*}' is replaced with
16# '${:U11:M*2*}', '${:U12:M*2*}', '${:U13:M*2*}', one after another.  This
17# replacement creates the impression that .for variables were real variables,
18# when in fact they aren't.
19.for i in 11 12 13
20.  if ${i:M*2*}
21# expect+1: 2
22.info 2
23.  endif
24.endfor
25
26
27# In conditions, the function call to 'empty' does not look like a variable
28# expression, therefore it is not replaced.  Since there is no global variable
29# named 'i', this expression makes for a leaky abstraction.  If the .for
30# variables were real variables, calling 'empty' would work on them as well.
31.for i in 11 12 13
32# Asking for an empty iteration variable does not make sense as the .for loop
33# splits the iteration items into words, and such a word cannot be empty.
34.  if empty(i)
35# expect+3: Missing argument for ".error"
36# expect+2: Missing argument for ".error"
37# expect+1: Missing argument for ".error"
38.    error			# due to the leaky abstraction
39.  endif
40# The typical way of using 'empty' with variables from .for loops is pattern
41# matching using the modifiers ':M' or ':N'.
42.  if !empty(i:M*2*)
43.    if ${i} != "12"
44.      error
45.    endif
46.  endif
47.endfor
48
49
50# The idea of replacing every occurrences of 'empty(i' in the body of a .for
51# loop would be naive and require many special cases, as there are many cases
52# that need to be considered when deciding whether the token 'empty' is a
53# function call or not, as demonstrated by the following examples.  For
54# variable expressions like '${i:Modifiers}', this is simpler as a single
55# dollar almost always starts a variable expression.  For counterexamples and
56# edge cases, see directive-for-escape.mk.  Adding another such tricky detail
57# is out of the question.
58.MAKEFLAGS: -df
59.for i in value
60# The identifier 'empty' can only be used in conditions such as .if, .ifdef or
61# .elif.  In other lines the string 'empty(' must be preserved.
62CPPFLAGS+=	-Dmessage="empty(i)"
63# There may be whitespace between 'empty' and '('.
64.if ! empty (i)
65.  error
66.endif
67# Even in conditions, the string 'empty(' is not always a function call, it
68# can occur in a string literal as well.
69.if "empty\(i)" != "empty(i)"
70.  error
71.endif
72# In comments like 'empty(i)', the text must be preserved as well.
73#
74# Conditions, including function calls to 'empty', can not only occur in
75# condition directives, they can also occur in the modifier ':?', see
76# varmod-ifelse.mk.
77CPPFLAGS+=	-Dmacro="${empty(i):?empty:not-empty}"
78.endfor
79.MAKEFLAGS: -d0
80
81
82# An idea to work around the above problems is to collect the variables from
83# the .for loops in a separate scope.  To match the current behavior, there
84# has to be one scope per included file.  There may be .for loops using the
85# same variable name in files that include each other:
86#
87# outer.mk:	.for i in outer
88#		.  info $i		# outer
89#		.  include "inner.mk"
90# inner.mk:	.    info $i		# (undefined)
91#		.    for i in inner
92#		.      info $i		# inner
93#		.    endfor
94#		.    info $i		# (undefined)
95# outer.mk:	.  info $i		# outer
96#		.endfor
97#
98# This might be regarded another leaky abstraction, but it is in fact useful
99# that variables from .for loops can only affect expressions in the current
100# file.  If variables from .for loops were implemented as global variables,
101# they might interact between files.
102#
103# To emulate this exact behavior for the function 'empty', each file in the
104# stack of included files needs its own scope that is independent from the
105# other files.
106#
107# Another tricky detail are nested .for loops in a single file that use the
108# same variable name.  These are generally avoided by developers, as they
109# would be difficult to understand for humans as well.  Technically, they are
110# possible though.  Assuming there are two nested .for loops, both using the
111# variable 'i'.  When the inner .for loop ends, the inner 'i' needs to be
112# removed from the scope, which would need to make the outer 'i' visible
113# again.  This would suggest to use one variable scope per .for loop.
114#
115# Using a separate scope has the benefit that Var_Parse already allows for
116# a custom scope to be passed as parameter.  This would have another side
117# effect though.  There are several modifiers that actually modify variables,
118# and these modifications happen in the scope that is passed to Var_Parse.
119# This would mean that the combination of a .for variable and the modifiers
120# '::=', '::+=', '::?=', '::!=' and ':_' would lead to different behavior than
121# before.
122
123# TODO: Add code that demonstrates the current interaction between variables
124#  from .for loops and the modifiers mentioned above.
125