xref: /freebsd/contrib/bmake/unit-tests/varmod-ifelse.mk (revision 9f23cbd6cae82fd77edfad7173432fa8dccd0a95)
1# $NetBSD: varmod-ifelse.mk,v 1.22 2023/06/01 20:56:35 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#	variable expression == "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#	${:Uvariable expression} == "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 (${${:Uvariable expression} == "literal":?bad:bad})
28.if ${${:Uvariable expression} == "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 variable 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 variable 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 "name" is first expanded, and the result of
101# this expansion is then taken as the condition.  To force the variable
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 "name" 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+2: .
160# expect+1: Comparison with '>=' requires both operands 'no' and '10' to be numeric
161.info ${${STRING} == "literal" || ${NUMBER} >= 10:?yes:no}.
162
163# The following situation occasionally occurs with MKINET6 or similar
164# variables.
165NUMBER=		# empty, not really a number either
166# expect+1: .
167.info ${${STRING} == "literal" && ${NUMBER} >= 10:?yes:no}.
168# expect+1: .
169.info ${${STRING} == "literal" || ${NUMBER} >= 10:?yes:no}.
170
171# CondParser_LeafToken handles [0-9-+] specially, treating them as a number.
172PLUS=		+
173ASTERISK=	*
174EMPTY=		# empty
175# "true" since "+" is not the empty string.
176# expect+1: true
177.info ${${PLUS}		:?true:false}
178# "false" since the variable named "*" is not defined.
179# expect+1: false
180.info ${${ASTERISK}	:?true:false}
181# syntax error since the condition is completely blank.
182.info ${${EMPTY}	:?true:false}
183
184
185# Since the condition of the '?:' modifier is expanded before being parsed and
186# evaluated, it is common practice to enclose expressions in quotes, to avoid
187# producing syntactically invalid conditions such as ' == value'.  This only
188# works if the expanded values neither contain quotes nor backslashes.  For
189# strings containing quotes or backslashes, the '?:' modifier should not be
190# used.
191PRIMES=	2 3 5 7 11
192.if ${1 2 3 4 5:L:@n@$n:${ ("${PRIMES:M$n}" != "") :?prime:not_prime}@} != \
193  "1:not_prime 2:prime 3:prime 4:not_prime 5:prime"
194.  error
195.endif
196
197# When parsing the modifier ':?', there are 3 possible cases:
198#
199#	1. The whole expression is only parsed.
200#	2. The expression is parsed and the 'then' branch is evaluated.
201#	3. The expression is parsed and the 'else' branch is evaluated.
202#
203# In all of these cases, the expression must be parsed in the same way,
204# especially when one of the branches contains unbalanced '{}' braces.
205#
206# At 2020-01-01, the expressions from the 'then' and 'else' branches were
207# parsed differently, depending on whether the branch was taken or not.  When
208# the branch was taken, the parser recognized that in the modifier ':S,}},,',
209# the '}}' were ordinary characters.  When the branch was not taken, the
210# parser only counted balanced '{' and '}', ignoring any escaping or other
211# changes in the interpretation.
212#
213# In var.c 1.285 from 2020-07-20, the parsing of the expressions changed so
214# that in both cases the expression is parsed in the same way, taking the
215# unbalanced braces in the ':S' modifiers into account.  This change was not
216# on purpose, the commit message mentioned 'has the same effect', which was a
217# wrong assumption.
218#
219# In var.c 1.323 from 2020-07-26, the unintended fix from var.c 1.285 was
220# reverted, still not knowing about the difference between regular parsing and
221# balanced-mode parsing.
222#
223# In var.c 1.1028 from 2022-08-08, there was another attempt at fixing this
224# inconsistency in parsing, but since that broke parsing of the modifier ':@',
225# it was reverted in var.c 1.1029 from 2022-08-23.
226#
227# In var.c 1.1047 from 2023-02-18, the inconsistency in parsing was finally
228# fixed.  The modifier ':@' now parses the body in balanced mode, while
229# everywhere else the modifier parts have their subexpressions parsed in the
230# same way, no matter whether they are evaluated or not.
231#
232# The modifiers ':@' and ':?' are similar in that they conceptually contain
233# text to be evaluated later or conditionally, still they parse that text
234# differently.  The crucial difference is that the body of the modifier ':@'
235# is always parsed using balanced mode.  The modifier ':?', on the other hand,
236# must parse both of its branches in the same way, no matter whether they are
237# evaluated or not.  Since balanced mode and standard mode are incompatible,
238# it's impossible to use balanced mode in the modifier ':?'.
239.MAKEFLAGS: -dc
240.if 0 && ${1:?${:Uthen0:S,}},,}:${:Uelse0:S,}},,}} != "not evaluated"
241# At 2020-01-07, the expression evaluated to 'then0,,}}', even though it was
242# irrelevant as the '0' had already been evaluated to 'false'.
243.  error
244.endif
245.if 1 && ${0:?${:Uthen1:S,}},,}:${:Uelse1:S,}},,}} != "else1"
246.  error
247.endif
248.if 2 && ${1:?${:Uthen2:S,}},,}:${:Uelse2:S,}},,}} != "then2"
249# At 2020-01-07, the whole expression evaluated to 'then2,,}}' instead of the
250# expected 'then2'.  The 'then' branch of the ':?' modifier was parsed
251# normally, parsing and evaluating the ':S' modifier, thereby treating the
252# '}}' as ordinary characters and resulting in 'then2'.  The 'else' branch was
253# parsed in balanced mode, ignoring that the inner '}}' were ordinary
254# characters.  The '}}' were thus interpreted as the end of the 'else' branch
255# and the whole expression.  This left the trailing ',,}}', which together
256# with the 'then2' formed the result 'then2,,}}'.
257.  error
258.endif
259.MAKEFLAGS: -d0
260