Gentoo Archives: gentoo-dev

From: Alexis Ballier <aballier@g.o>
To: gentoo-dev@l.g.o
Subject: Re: [gentoo-dev] [RFC] Forced/automatic USE flag constraints (codename: ENFORCED_USE)
Date: Fri, 09 Jun 2017 11:41:28
Message-Id: 20170609134110.418ae6ac@gentoo.org
In Reply to: Re: [gentoo-dev] [RFC] Forced/automatic USE flag constraints (codename: ENFORCED_USE) by "Michał Górny"
1 On Fri, 09 Jun 2017 11:19:20 +0200
2 Michał Górny <mgorny@g.o> wrote:
3
4 > On śro, 2017-06-07 at 11:56 +0200, Alexis Ballier wrote:
5 > > On Wed, 07 Jun 2017 11:27:59 +0200
6 > > Michał Górny <mgorny@g.o> wrote:
7 > >
8 > > > On śro, 2017-06-07 at 10:17 +0200, Alexis Ballier wrote:
9 > > > > > Also, do I presume correctly that for all supported cases
10 > > > > > (i.e. those which your nsolve does not reject), solve and
11 > > > > > nsolve are compatible?
12 > > > >
13 > > > > Not sure what you mean here. nsolve does not solve anything, it
14 > > > > just validates REQUIRED_USE so that it is guaranteed it can be
15 > > > > solved.
16 > > >
17 > > > What I mean is whether you can guarantee that:
18 > > >
19 > > > a. for every X that nsolve(X) == ok, solve() will be able to find
20 > > > a valid solution,
21 > >
22 > > yes
23 > >
24 > > > b. for every X that solve() can solve reliably, nsolve(X) == ok.
25 > >
26 > > no and that's not really possible
27 >
28 > I thought so. To expand it a little, could you confirm whether I
29 > correctly presume that:
30 >
31 > a. for all 'good' results, the plain iterative solve() should be able
32 > to find a solution with a single iteration?
33
34 yes that's the point of it
35
36 > b. for all 'need toposort' results, the solve() should be able to find
37 > a solution with n>=1 iterations?
38
39 yes; though n is only bounded by the # of clauses (expanded as
40 implications) while it can be 1 if reorderer properly; I wouldn't bother
41 doing several iterations and just reject that at repoman side since it's
42 easily solved
43
44 > c. all of 'circular' results have at least one USE flag combination
45 > that can not be solved by solve()?
46
47 In theory no as that would imply your 1st b. In practice, I've only
48 seen cases like that.
49
50
51
52 > > > > We first need to define properly & formally how to solve requse
53 > > > > constraints if we want ppl to be able to rely on it (or rather
54 > > > > write requse that give the expected result).
55 > > > >
56 > > > > The way I see it, REQUIRED_USE ast looks like:
57 > > > > (assuming ^^ is already expanded to || + ??)
58 > > > >
59 > > > > clause =
60 > > > > AllOf(list of clauses)
61 > > > > | AnyOf(list of clauses)
62 > > > > | AtMostOne(list of clauses)
63 > > > > | Implication(useflag, clause)
64 > > > > | useflag
65 > > > >
66 > > > > Now, portage already has the function 'eval(input, clause)'. We
67 > > > > need to define 'trueify(input, clause)' that modifies input so
68 > > > > that 'eval(input, clause)' is always true afterwards. Since
69 > > > > this is SAT, there is no hope to make this work for all
70 > > > > clauses. From the discussions here, a good algorithm would be:
71 > > > >
72 > > > > trueify(input, clause) = match clause with
73 > > > > AllOf(l) -> for i in l: trueify(input, i)
74 > > > > > AnyOf(l) -> if not eval(input, clause): trueify(input,
75 > > > > > l[0]) AtMostOne(l) -> f = (lambda x,y: pass)
76 > > > >
77 > > > > for i in l:
78 > > > > f(input, i)
79 > > > > if eval(input, i): f = falsify
80 > > > > > Implication(useflag, consequence) ->
81 > > > >
82 > > > > if input[useflag]: trueify(input,
83 > > > > consequence)
84 > > > > > useflag -> input[useflag] = True
85 > > > >
86 > > > >
87 > > > > Now you see that for the AtMostOne case we need its dual, the
88 > > > > 'falsify(input, clause)' function:
89 > > > >
90 > > > > falsify(input, clause) = match clause with
91 > > > > AllOf(l) -> falsify(input, l[0])
92 > > >
93 > > > That's a debatable case. My solve() actually 'falsifies' all
94 > > > the subexpressions which might be more reliable.
95 > >
96 > > Best way to debate this is probably to write the implication
97 > > translation and feed that to nsolve from a few test cases.
98 >
99 > Exactly my thought. Since both algorithms should be kept in sync, it
100 > probably makes sense to figure out the translation first and use
101 > whatever makes it consistent without hacking on the translation hard.
102 > I'll try to figure it out on paper today.
103 >
104 > > Intuition is that falsifying all of them adds more pressure on the
105 > > solver and you might end up failing to solve it for no good reason,
106 > > so falsifying only one of them seems safer.
107 >
108 > In either case, it's purely a matter of contract. You can't predict
109 > which one will be more correct, and I think it's hard to even predict
110 > which one developers will consider less surprising.
111
112 Not sure what you mean by "more correct" but we've seen some
113 translations are easier to solve than others, while being logically
114 equivalent.
115
116 As for what developers would expect, I think that'd be some kind of
117 continuity of the solver: for 2 "close" inputs the solver gives "close"
118 outputs (whatever close means). One way to go towards that is the least
119 effort / least change from the solver part, which is why I always did
120 the bare minimum of changes to the input in the algorithm I proposed.
121
122 For example, if you negate them all in an AllOf clause, you get cases
123 where a lot can change:
124 For '?? ( a ( b c d e f ... z ) )' and USE="a b c ... z", it gives you
125 USE="a -b -c -d ... -z" changing 25 useflags.
126 If you just negate the first of the AllOf, you get as output
127 USE="a -b c d ... z" changing only one useflag.
128
129 [...]
130 > > > > > Implication(useflag, consequence) ->
131 > > > >
132 > > > > if not input[useflag]: raise "impossible"
133 > > >
134 > > > Why impossible? Unless I'm missing something, it's false
135 > > > already.
136 > >
137 > > 'foo? bar' is always true if foo is false; so it's impossible to
138 > > make it false
139 >
140 > Yes, you are correct. I was actually thinking of 'if LHS is false, we
141 > do not enforce RHS'.
142
143 I'm wrong actually. It can be falsified by setting foo to True and bar
144 to False. More on it below.
145
146 > > it's really a corner case as I think we don't allow nested
147 > > implications inside ||, ^^, () or ??, which is the only way to
148 > > reach that.
149 >
150 > Strictly speaking, there's no rule prohibiting that. And I think we
151 > actually have or had used 'foo?' inside '||' at least in dependencies.
152 > The basic idea is that if the flag is disabled, the contents disappear
153 > from the containing '||' block.
154
155 Interesting. Then we should, sadly, support that.
156
157 Let's think a bit about its meaning.
158 ?? ( X Y ) is "if X then not Y".
159 ?? ( a b? ( c ) ) is "if a then not "b? ( c )", that is, "if a then b
160 and not c", so that's rewritten as "a? ( b !c )".
161
162 That doesn't really seem to match your "basic idea". Instead, this
163 could be rewritten as:
164 b? ( ?? ( a c ) ) !b? ( ?? ( a ) )
165 that is: "b? ( a? ( !c ) )"
166
167 "If a and b are enabled then disable c" seems a much better
168 interpretation than "If a is enabled then enable b and disable c".
169
170
171 Now, we can apply your basic idea to bubble up all the implications so
172 that they're only at toplevel.
173
174 Something([begin Implication(X,Y) end]) is rewritten as:
175 Implication(X, Something([begin Y end]))
176 Implication(!X, Something([begin end]))
177
178 > Now, this makes a lot of things harder. For plain solve(), it's not
179 > a major problem -- worst case, we put an extra reordering on each
180 > iteration based on enabled USE flags. For nsolve(), it's harder since
181 > it makes the iteration graph PITA. I'll have to think if we can even
182 > translate it reasonably.
183
184
185 With the above translation they should not make any difference for
186 nsolve: the two implications have different domain as one is X ->
187 something and the other is !X -> something.
188
189
190 > > > >
191 > > > > Note how the above favors leftmost in all cases. If it needs to
192 > > > > change something, it always tries to leave the leftmost
193 > > > > untouched. Note also that it processes everything left to right
194 > > > > (the AllOf case where REQUIRED_USE="AllOf(list of clauses)"
195 > > > > ).
196 > > >
197 > > > You need to be able to reorder the clauses to handle use.force
198 > > > and use.mask.
199 > >
200 > > Not sure if reorder is the best way. It sure works, but maybe we'd
201 > > want a repoman error if e.g. 'foo? ( bar )' is in REQUIRED_USE, bar
202 > > is masked but not foo. That'd be a matter of eliminating the
203 > > constants in the ast and if we get 'false' for a profile we error
204 > > out.
205 >
206 > Yes, we want that. It makes sense for pure implications. For n-ary
207 > thingies, it's harder than that and I'd rather not require developers
208 > to figure out a specific order to make things work.
209 >
210 > Think of the following:
211 >
212 > || ( X Y )
213 >
214 > with X being masked on profile A, Y being masked on profile B. You
215 > just can't get it right then.
216 >
217 > Of course, this is a bit unrealistic but I think a case when a
218 > preferred (newer) provider is masked on some architectures is
219 > realistic. I don't think we want to prefer a worse solution (or go
220 > back to applying package.use workarounds) because of fringe arches.
221 >
222 > Another question is whether we could solve this specific problem via
223 > applying some kind of 'if forced/masked' logic into the AST.
224
225 I think we're having communication interferences here :) That's exactly
226 what I'm talking about.
227
228 More specifically:
229
230 For each profile:
231 formula=replace_masks_and_force_by_constants(required_use)
232 simplify(formula)
233 nsolve(formula)
234
235 In your example above, we'd call 'nsolve("|| ( X )")' and 'nsolve("||
236 ( Y )")' (or even simpler, depending on how simplify() is defined). If
237 both X and Y are masked on a profile, then that'd reduce to
238 'nsolve("False")' which would rant.
239
240 I think that for solve(), reordering is basically equivalent since we
241 always favour leftmost. However, we have to somehow guarantee that
242 solve() will never try to enable a masked flag. This is guaranteed by
243 the above checks on profiles if enforced by repoman. If we apply the
244 same logic for solve that'd basically be built-in (masked/forced flags
245 wont appear in solve()'s input anymore), which I believe is better. Keep
246 in mind that users can mask and force some useflags, so the repoman
247 check won't catch that.
248
249
250 > > > > So, the very first thing to do is to agree that the above solver
251 > > > > (the trueify function) is what we want to implement and set
252 > > > > this in stone. There's no point in implementing a proper requse
253 > > > > checker if the algorithm is meant to change. Having a formal
254 > > > > definition will also be necessary to mandate that in future
255 > > > > EAPIs.
256 > > > >
257 > > > > Then, and only then, we'd need to have the above solver
258 > > > > implemented into portage (hidden under a FEATURES) and import
259 > > > > my nsolve into repoman (after due cleanup).
260 > > > >
261 > > >
262 > > > Yes, that's my goal. However, before we can set the algorithm in
263 > > > stone we need to verify that it will work in all of the supported
264 > > > cases.
265 > >
266 > > Yep, that's the point of nsolve/classify :)
267 > >
268 > > > Preferably it should also be as simple as possible to avoid
269 > > > putting too much complexity in the spec.
270 > >
271 > > Yes; not sure if anything simpler than the above can be achieved
272 > > though.
273 >
274 > Actually, I think we should roughly proceed in the following way:
275 >
276 > 1. Design the solve() algorithm.
277 >
278 > 1a. Test solve() on isolated test cases.
279 >
280 > 1b. Integrate solve() into Portage (as optional feature) and encourage
281 > wider testing.
282 >
283 > 2. Design the nsolve() verification based on solve().
284 >
285 > 2a. Test nsolve() separately.
286 >
287 > 2b. Start fixing simple cases that do not bring any controversy (like
288 > most of the reorderings).
289 >
290 > 2c. Integrate nsolve() into repoman.
291 >
292 > 3. Spec the whole thing and decide how to go next.
293 >
294 >
295 > Assuming that 1* and 2* is to be done simultaneously, and each
296 > subpoint implies that if we hit any walls, we can go back to a
297 > previous point and fix the design.
298 >
299 > I think we're mostly past 1a and 2a now. We should try to think a bit
300 > more about the corner cases right now, and when we're done with that,
301 > the next step would be to:
302 >
303 > A. start fixing some cases ('need topo sort' at least?) to improve
304 > testing area for the implementation,
305 >
306 > B. integrate solve() into Portage,
307 >
308 > C. maybe optionally integrate nsolve() into repoman.
309 >
310 > However, I don't want to set the spec into stone until we have some
311 > live testing via Portage with FEATURES=solve-required-use or alike.
312 > For repoman, we can add the verification optionally (i.e. with the
313 > above feature or via a separate command-line option) but I don't
314 > think we should enable it by default until we verify it.
315 >
316
317 Yes, I agree. Since solve() and nsolve() are very related we should
318 carefully think how to organize the code then in order to avoid
319 duplication or them getting out of sync.
320
321 As for enabling it into repoman, I'm not sure which is best. Safest way
322 is obviously as you suggest, but a warning is not a blocker, so
323 enabling it by default can have a nice effect of receiving hate mails
324 from angry developers if something is wrong.
325
326 Alexis.

Replies