xref: /freebsd/contrib/bmake/unit-tests/varmod-ifelse.mk (revision f126890ac5386406dadf7c4cfa9566cbb56537c5)
1# $NetBSD: varmod-ifelse.mk,v 1.26 2023/12/10 20:12:28 rillig Exp $
2#
3# Tests for the ${cond:?then:else} variable modifier, which evaluates either
4# the then-expression or the else-expression, depending on the condition.
5#
6# The modifier was added on 1998-04-01.
7#
8# Until 2015-10-11, the modifier always evaluated both the "then" and the
9# "else" expressions.
10
11# TODO: Implementation
12
13# The variable name of the expression is expanded and then taken as the
14# condition.  In the below example it becomes:
15#
16#	bare words == "literal"
17#
18# This confuses the parser, which expects an operator instead of the bare
19# word "expression".  If the name were expanded lazily, everything would be
20# fine since the condition would be:
21#
22#	${:Ubare words} == "literal"
23#
24# Evaluating the variable name lazily would require additional code in
25# Var_Parse and ParseVarname, it would be more useful and predictable
26# though.
27# expect+1: Malformed conditional (${${:Ubare words} == "literal":?bad:bad})
28.if ${${:Ubare words} == "literal":?bad:bad}
29.  error
30.else
31.  error
32.endif
33
34# In a variable assignment, undefined variables are not an error.
35# Because of the early expansion, the whole condition evaluates to
36# ' == ""' though, which cannot be parsed because the left-hand side looks
37# empty.
38COND:=	${${UNDEF} == "":?bad-assign:bad-assign}
39
40# In a condition, undefined variables generate a "Malformed conditional"
41# error.  That error message is wrong though.  In lint mode, the correct
42# "Undefined variable" error message is generated.
43# The difference to the ':=' variable assignment is the additional
44# "Malformed conditional" error message.
45# expect+1: Malformed conditional (${${UNDEF} == "":?bad-cond:bad-cond})
46.if ${${UNDEF} == "":?bad-cond:bad-cond}
47.  error
48.else
49.  error
50.endif
51
52# When the :? is parsed, it is greedy.  The else branch spans all the
53# text, up until the closing character '}', even if the text looks like
54# another modifier.
55.if ${1:?then:else:Q} != "then"
56.  error
57.endif
58.if ${0:?then:else:Q} != "else:Q"
59.  error
60.endif
61
62# This line generates 2 error messages.  The first comes from evaluating the
63# malformed conditional "1 == == 2", which is reported as "Bad conditional
64# expression" by ApplyModifier_IfElse.  The expression containing that
65# conditional therefore returns a parse error from Var_Parse, and this parse
66# error propagates to CondEvalExpression, where the "Malformed conditional"
67# comes from.
68# expect+1: Malformed conditional (${1 == == 2:?yes:no} != "")
69.if ${1 == == 2:?yes:no} != ""
70.  error
71.else
72.  error
73.endif
74
75# If the "Bad conditional expression" appears in a quoted string literal, the
76# error message "Malformed conditional" is not printed, leaving only the "Bad
77# conditional expression".
78#
79# XXX: The left-hand side is enclosed in quotes.  This results in Var_Parse
80# being called without VARE_UNDEFERR.  When ApplyModifier_IfElse
81# returns AMR_CLEANUP as result, Var_Parse returns varUndefined since the
82# value of the expression is still undefined.  CondParser_String is
83# then supposed to do proper error handling, but since varUndefined is local
84# to var.c, it cannot distinguish this return value from an ordinary empty
85# string.  The left-hand side of the comparison is therefore just an empty
86# string, which is obviously equal to the empty string on the right-hand side.
87#
88# XXX: The debug log for -dc shows a comparison between 1.0 and 0.0.  The
89# condition should be detected as being malformed before any comparison is
90# done since there is no well-formed comparison in the condition at all.
91.MAKEFLAGS: -dc
92.if "${1 == == 2:?yes:no}" != ""
93.  error
94.else
95# expect+1: warning: Oops, the parse error should have been propagated.
96.  warning Oops, the parse error should have been propagated.
97.endif
98.MAKEFLAGS: -d0
99
100# As of 2020-12-10, the variable "VAR" is first expanded, and the result of
101# this expansion is then taken as the condition.  To force the
102# expression in the condition to be evaluated at exactly the right point,
103# the '$' of the intended '${VAR}' escapes from the parser in form of the
104# expression ${:U\$}.  Because of this escaping, the variable "VAR" and thus
105# the condition ends up as "${VAR} == value", just as intended.
106#
107# This hack does not work for variables from .for loops since these are
108# expanded at parse time to their corresponding ${:Uvalue} expressions.
109# Making the '$' of the '${VAR}' expression indirect hides this expression
110# from the parser of the .for loop body.  See ForLoop_SubstVarLong.
111.MAKEFLAGS: -dc
112VAR=	value
113.if ${ ${:U\$}{VAR} == value:?ok:bad} != "ok"
114.  error
115.endif
116.MAKEFLAGS: -d0
117
118# On 2021-04-19, when building external/bsd/tmux with HAVE_LLVM=yes and
119# HAVE_GCC=no, the following conditional generated this error message:
120#
121#	make: Bad conditional expression 'string == "literal" && no >= 10'
122#	    in 'string == "literal" && no >= 10?yes:no'
123#
124# Despite the error message (which was not clearly marked with "error:"),
125# the build continued, for historical reasons, see main_Exit.
126#
127# The tricky detail here is that the condition that looks so obvious in the
128# form written in the makefile becomes tricky when it is actually evaluated.
129# This is because the condition is written in the place of the variable name
130# of the expression, and in an expression, the variable name is always
131# expanded first, before even looking at the modifiers.  This happens for the
132# modifier ':?' as well, so when CondEvalExpression gets to see the
133# expression, it already looks like this:
134#
135#	string == "literal" && no >= 10
136#
137# When parsing such an expression, the parser used to be strict.  It first
138# evaluated the left-hand side of the operator '&&' and then started parsing
139# the right-hand side 'no >= 10'.  The word 'no' is obviously a string
140# literal, not enclosed in quotes, which is OK, even on the left-hand side of
141# the comparison operator, but only because this is a condition in the
142# modifier ':?'.  In an ordinary directive '.if', this would be a parse error.
143# For strings, only the comparison operators '==' and '!=' are defined,
144# therefore parsing stopped at the '>', producing the 'Bad conditional
145# expression'.
146#
147# Ideally, the conditional expression would not be expanded before parsing
148# it.  This would allow to write the conditions exactly as seen below.  That
149# change has a high chance of breaking _some_ existing code and would need
150# to be thoroughly tested.
151#
152# Since cond.c 1.262 from 2021-04-20, make reports a more specific error
153# message in situations like these, pointing directly to the specific problem
154# instead of just saying that the whole condition is bad.
155STRING=		string
156NUMBER=		no		# not really a number
157# expect+1: no.
158.info ${${STRING} == "literal" && ${NUMBER} >= 10:?yes:no}.
159# expect+3: Comparison with '>=' requires both operands 'no' and '10' to be numeric
160# expect: make: Bad conditional expression 'string == "literal" || no >= 10' before '?yes:no'
161# expect+1: .
162.info ${${STRING} == "literal" || ${NUMBER} >= 10:?yes:no}.
163
164# The following situation occasionally occurs with MKINET6 or similar
165# variables.
166NUMBER=		# empty, not really a number either
167# expect: make: Bad conditional expression 'string == "literal" &&  >= 10' before '?yes:no'
168# expect+1: .
169.info ${${STRING} == "literal" && ${NUMBER} >= 10:?yes:no}.
170# expect: make: Bad conditional expression 'string == "literal" ||  >= 10' before '?yes:no'
171# expect+1: .
172.info ${${STRING} == "literal" || ${NUMBER} >= 10:?yes:no}.
173
174# CondParser_LeafToken handles [0-9-+] specially, treating them as a number.
175PLUS=		+
176ASTERISK=	*
177EMPTY=		# empty
178# "true" since "+" is not the empty string.
179# expect+1: <true>
180.info <${${PLUS}		:?true:false}>
181# "false" since the variable named "*" is not defined.
182# expect+1: <false>
183.info <${${ASTERISK}	:?true:false}>
184# syntax error since the condition is completely blank.
185# expect+1: <>
186.info <${${EMPTY}	:?true:false}>
187
188
189# Since the condition of the '?:' modifier is expanded before being parsed and
190# evaluated, it is common practice to enclose expressions in quotes, to avoid
191# producing syntactically invalid conditions such as ' == value'.  This only
192# works if the expanded values neither contain quotes nor backslashes.  For
193# strings containing quotes or backslashes, the '?:' modifier should not be
194# used.
195PRIMES=	2 3 5 7 11
196.if ${1 2 3 4 5:L:@n@$n:${ ("${PRIMES:M$n}" != "") :?prime:not_prime}@} != \
197  "1:not_prime 2:prime 3:prime 4:not_prime 5:prime"
198.  error
199.endif
200
201# When parsing the modifier ':?', there are 3 possible cases:
202#
203#	1. The whole expression is only parsed.
204#	2. The expression is parsed and the 'then' branch is evaluated.
205#	3. The expression is parsed and the 'else' branch is evaluated.
206#
207# In all of these cases, the expression must be parsed in the same way,
208# especially when one of the branches contains unbalanced '{}' braces.
209#
210# At 2020-01-01, the expressions from the 'then' and 'else' branches were
211# parsed differently, depending on whether the branch was taken or not.  When
212# the branch was taken, the parser recognized that in the modifier ':S,}},,',
213# the '}}' were ordinary characters.  When the branch was not taken, the
214# parser only counted balanced '{' and '}', ignoring any escaping or other
215# changes in the interpretation.
216#
217# In var.c 1.285 from 2020-07-20, the parsing of the expressions changed so
218# that in both cases the expression is parsed in the same way, taking the
219# unbalanced braces in the ':S' modifiers into account.  This change was not
220# on purpose, the commit message mentioned 'has the same effect', which was a
221# wrong assumption.
222#
223# In var.c 1.323 from 2020-07-26, the unintended fix from var.c 1.285 was
224# reverted, still not knowing about the difference between regular parsing and
225# balanced-mode parsing.
226#
227# In var.c 1.1028 from 2022-08-08, there was another attempt at fixing this
228# inconsistency in parsing, but since that broke parsing of the modifier ':@',
229# it was reverted in var.c 1.1029 from 2022-08-23.
230#
231# In var.c 1.1047 from 2023-02-18, the inconsistency in parsing was finally
232# fixed.  The modifier ':@' now parses the body in balanced mode, while
233# everywhere else the modifier parts have their subexpressions parsed in the
234# same way, no matter whether they are evaluated or not.
235#
236# The modifiers ':@' and ':?' are similar in that they conceptually contain
237# text to be evaluated later or conditionally, still they parse that text
238# differently.  The crucial difference is that the body of the modifier ':@'
239# is always parsed using balanced mode.  The modifier ':?', on the other hand,
240# must parse both of its branches in the same way, no matter whether they are
241# evaluated or not.  Since balanced mode and standard mode are incompatible,
242# it's impossible to use balanced mode in the modifier ':?'.
243.MAKEFLAGS: -dc
244.if 0 && ${1:?${:Uthen0:S,}},,}:${:Uelse0:S,}},,}} != "not evaluated"
245# At 2020-01-07, the expression evaluated to 'then0,,}}', even though it was
246# irrelevant as the '0' had already been evaluated to 'false'.
247.  error
248.endif
249.if 1 && ${0:?${:Uthen1:S,}},,}:${:Uelse1:S,}},,}} != "else1"
250.  error
251.endif
252.if 2 && ${1:?${:Uthen2:S,}},,}:${:Uelse2:S,}},,}} != "then2"
253# At 2020-01-07, the whole expression evaluated to 'then2,,}}' instead of the
254# expected 'then2'.  The 'then' branch of the ':?' modifier was parsed
255# normally, parsing and evaluating the ':S' modifier, thereby treating the
256# '}}' as ordinary characters and resulting in 'then2'.  The 'else' branch was
257# parsed in balanced mode, ignoring that the inner '}}' were ordinary
258# characters.  The '}}' were thus interpreted as the end of the 'else' branch
259# and the whole expression.  This left the trailing ',,}}', which together
260# with the 'then2' formed the result 'then2,,}}'.
261.  error
262.endif
263
264
265# Since the condition is taken from the variable name of the expression, not
266# from its value, it is evaluated early.  It is possible though to construct
267# conditions that are evaluated lazily, at exactly the right point.  There is
268# no way to escape a '$' directly in the variable name, but there are
269# alternative ways to bring a '$' into the condition.
270#
271#	In an indirect condition using the ':U' modifier, each '$', ':' and
272#	'}' must be escaped as '\$', '\:' and '\}', respectively, but '{' must
273#	not be escaped.
274#
275#	In an indirect condition using a separate variable, each '$' must be
276#	escaped as '$$'.
277#
278# These two forms allow the variables to contain arbitrary characters, as the
279# condition parser does not see them.
280DELAYED=	two
281# expect+1: no
282.info ${ ${:U \${DELAYED\} == "one"}:?yes:no}
283# expect+1: yes
284.info ${ ${:U \${DELAYED\} == "two"}:?yes:no}
285INDIRECT_COND1=	$${DELAYED} == "one"
286# expect+1: no
287.info ${ ${INDIRECT_COND1}:?yes:no}
288INDIRECT_COND2=	$${DELAYED} == "two"
289# expect+1: yes
290.info ${ ${INDIRECT_COND2}:?yes:no}
291
292
293.MAKEFLAGS: -d0
294
295
296# In the modifier parts for the 'then' and 'else' branches, subexpressions are
297# parsed in by inspecting the actual modifiers.  In 2008, 2015, 2020, 2022 and
298# 2023, the exact parsing algorithm switched a few times, counting balanced
299# braces instead of proper subexpressions, which meant that unbalanced braces
300# were parsed differently, depending on whether the branch was active or not.
301BRACES=	}}}
302NO=	${0:?${BRACES:S,}}},yes,}:${BRACES:S,}}},no,}}
303YES=	${1:?${BRACES:S,}}},yes,}:${BRACES:S,}}},no,}}
304BOTH=	<${YES}> <${NO}>
305.if ${BOTH} != "<yes> <no>"
306.  error
307.endif
308