]>
Commit | Line | Data |
---|---|---|
03d4611c PW |
1 | # This code supports verifying group implementations which have branches |
2 | # or conditional statements (like cmovs), by allowing each execution path | |
3 | # to independently set assumptions on input or intermediary variables. | |
4 | # | |
5 | # The general approach is: | |
8c7ea22d | 6 | # * A constraint is a tuple of two sets of symbolic expressions: |
03d4611c PW |
7 | # the first of which are required to evaluate to zero, the second of which |
8 | # are required to evaluate to nonzero. | |
9 | # - A constraint is said to be conflicting if any of its nonzero expressions | |
10 | # is in the ideal with basis the zero expressions (in other words: when the | |
11 | # zero expressions imply that one of the nonzero expressions are zero). | |
12 | # * There is a list of laws that describe the intended behaviour, including | |
13 | # laws for addition and doubling. Each law is called with the symbolic point | |
14 | # coordinates as arguments, and returns: | |
15 | # - A constraint describing the assumptions under which it is applicable, | |
16 | # called "assumeLaw" | |
17 | # - A constraint describing the requirements of the law, called "require" | |
18 | # * Implementations are transliterated into functions that operate as well on | |
19 | # algebraic input points, and are called once per combination of branches | |
73aca836 | 20 | # executed. Each execution returns: |
03d4611c PW |
21 | # - A constraint describing the assumptions this implementation requires |
22 | # (such as Z1=1), called "assumeFormula" | |
23 | # - A constraint describing the assumptions this specific branch requires, | |
24 | # but which is by construction guaranteed to cover the entire space by | |
25 | # merging the results from all branches, called "assumeBranch" | |
26 | # - The result of the computation | |
27 | # * All combinations of laws with implementation branches are tried, and: | |
28 | # - If the combination of assumeLaw, assumeFormula, and assumeBranch results | |
29 | # in a conflict, it means this law does not apply to this branch, and it is | |
30 | # skipped. | |
31 | # - For others, we try to prove the require constraints hold, assuming the | |
32 | # information in assumeLaw + assumeFormula + assumeBranch, and if this does | |
33 | # not succeed, we fail. | |
34 | # + To prove an expression is zero, we check whether it belongs to the | |
35 | # ideal with the assumed zero expressions as basis. This test is exact. | |
36 | # + To prove an expression is nonzero, we check whether each of its | |
37 | # factors is contained in the set of nonzero assumptions' factors. | |
38 | # This test is not exact, so various combinations of original and | |
39 | # reduced expressions' factors are tried. | |
40 | # - If we succeed, we print out the assumptions from assumeFormula that | |
41 | # weren't implied by assumeLaw already. Those from assumeBranch are skipped, | |
42 | # as we assume that all constraints in it are complementary with each other. | |
43 | # | |
44 | # Based on the sage verification scripts used in the Explicit-Formulas Database | |
45 | # by Tanja Lange and others, see http://hyperelliptic.org/EFD | |
46 | ||
47 | class fastfrac: | |
48 | """Fractions over rings.""" | |
49 | ||
50 | def __init__(self,R,top,bot=1): | |
51 | """Construct a fractional, given a ring, a numerator, and denominator.""" | |
52 | self.R = R | |
53 | if parent(top) == ZZ or parent(top) == R: | |
54 | self.top = R(top) | |
55 | self.bot = R(bot) | |
56 | elif top.__class__ == fastfrac: | |
57 | self.top = top.top | |
58 | self.bot = top.bot * bot | |
59 | else: | |
60 | self.top = R(numerator(top)) | |
61 | self.bot = R(denominator(top)) * bot | |
62 | ||
63 | def iszero(self,I): | |
64 | """Return whether this fraction is zero given an ideal.""" | |
65 | return self.top in I and self.bot not in I | |
66 | ||
67 | def reduce(self,assumeZero): | |
68 | zero = self.R.ideal(map(numerator, assumeZero)) | |
69 | return fastfrac(self.R, zero.reduce(self.top)) / fastfrac(self.R, zero.reduce(self.bot)) | |
70 | ||
71 | def __add__(self,other): | |
72 | """Add two fractions.""" | |
73 | if parent(other) == ZZ: | |
74 | return fastfrac(self.R,self.top + self.bot * other,self.bot) | |
75 | if other.__class__ == fastfrac: | |
76 | return fastfrac(self.R,self.top * other.bot + self.bot * other.top,self.bot * other.bot) | |
77 | return NotImplemented | |
78 | ||
79 | def __sub__(self,other): | |
80 | """Subtract two fractions.""" | |
81 | if parent(other) == ZZ: | |
82 | return fastfrac(self.R,self.top - self.bot * other,self.bot) | |
83 | if other.__class__ == fastfrac: | |
84 | return fastfrac(self.R,self.top * other.bot - self.bot * other.top,self.bot * other.bot) | |
85 | return NotImplemented | |
86 | ||
87 | def __neg__(self): | |
88 | """Return the negation of a fraction.""" | |
89 | return fastfrac(self.R,-self.top,self.bot) | |
90 | ||
91 | def __mul__(self,other): | |
92 | """Multiply two fractions.""" | |
93 | if parent(other) == ZZ: | |
94 | return fastfrac(self.R,self.top * other,self.bot) | |
95 | if other.__class__ == fastfrac: | |
96 | return fastfrac(self.R,self.top * other.top,self.bot * other.bot) | |
97 | return NotImplemented | |
98 | ||
99 | def __rmul__(self,other): | |
100 | """Multiply something else with a fraction.""" | |
101 | return self.__mul__(other) | |
102 | ||
103 | def __div__(self,other): | |
104 | """Divide two fractions.""" | |
105 | if parent(other) == ZZ: | |
106 | return fastfrac(self.R,self.top,self.bot * other) | |
107 | if other.__class__ == fastfrac: | |
108 | return fastfrac(self.R,self.top * other.bot,self.bot * other.top) | |
109 | return NotImplemented | |
110 | ||
111 | def __pow__(self,other): | |
112 | """Compute a power of a fraction.""" | |
113 | if parent(other) == ZZ: | |
114 | if other < 0: | |
115 | # Negative powers require flipping top and bottom | |
116 | return fastfrac(self.R,self.bot ^ (-other),self.top ^ (-other)) | |
117 | else: | |
118 | return fastfrac(self.R,self.top ^ other,self.bot ^ other) | |
119 | return NotImplemented | |
120 | ||
121 | def __str__(self): | |
122 | return "fastfrac((" + str(self.top) + ") / (" + str(self.bot) + "))" | |
123 | def __repr__(self): | |
124 | return "%s" % self | |
125 | ||
126 | def numerator(self): | |
127 | return self.top | |
128 | ||
129 | class constraints: | |
130 | """A set of constraints, consisting of zero and nonzero expressions. | |
131 | ||
132 | Constraints can either be used to express knowledge or a requirement. | |
133 | ||
134 | Both the fields zero and nonzero are maps from expressions to description | |
135 | strings. The expressions that are the keys in zero are required to be zero, | |
136 | and the expressions that are the keys in nonzero are required to be nonzero. | |
137 | ||
138 | Note that (a != 0) and (b != 0) is the same as (a*b != 0), so all keys in | |
139 | nonzero could be multiplied into a single key. This is often much less | |
140 | efficient to work with though, so we keep them separate inside the | |
141 | constraints. This allows higher-level code to do fast checks on the individual | |
142 | nonzero elements, or combine them if needed for stronger checks. | |
143 | ||
144 | We can't multiply the different zero elements, as it would suffice for one of | |
145 | the factors to be zero, instead of all of them. Instead, the zero elements are | |
146 | typically combined into an ideal first. | |
147 | """ | |
148 | ||
149 | def __init__(self, **kwargs): | |
150 | if 'zero' in kwargs: | |
151 | self.zero = dict(kwargs['zero']) | |
152 | else: | |
153 | self.zero = dict() | |
154 | if 'nonzero' in kwargs: | |
155 | self.nonzero = dict(kwargs['nonzero']) | |
156 | else: | |
157 | self.nonzero = dict() | |
158 | ||
159 | def negate(self): | |
160 | return constraints(zero=self.nonzero, nonzero=self.zero) | |
161 | ||
162 | def __add__(self, other): | |
163 | zero = self.zero.copy() | |
164 | zero.update(other.zero) | |
165 | nonzero = self.nonzero.copy() | |
166 | nonzero.update(other.nonzero) | |
167 | return constraints(zero=zero, nonzero=nonzero) | |
168 | ||
169 | def __str__(self): | |
170 | return "constraints(zero=%s,nonzero=%s)" % (self.zero, self.nonzero) | |
171 | ||
172 | def __repr__(self): | |
173 | return "%s" % self | |
174 | ||
175 | ||
176 | def conflicts(R, con): | |
177 | """Check whether any of the passed non-zero assumptions is implied by the zero assumptions""" | |
178 | zero = R.ideal(map(numerator, con.zero)) | |
179 | if 1 in zero: | |
180 | return True | |
181 | # First a cheap check whether any of the individual nonzero terms conflict on | |
182 | # their own. | |
183 | for nonzero in con.nonzero: | |
184 | if nonzero.iszero(zero): | |
185 | return True | |
186 | # It can be the case that entries in the nonzero set do not individually | |
187 | # conflict with the zero set, but their combination does. For example, knowing | |
188 | # that either x or y is zero is equivalent to having x*y in the zero set. | |
189 | # Having x or y individually in the nonzero set is not a conflict, but both | |
190 | # simultaneously is, so that is the right thing to check for. | |
191 | if reduce(lambda a,b: a * b, con.nonzero, fastfrac(R, 1)).iszero(zero): | |
192 | return True | |
193 | return False | |
194 | ||
195 | ||
196 | def get_nonzero_set(R, assume): | |
197 | """Calculate a simple set of nonzero expressions""" | |
198 | zero = R.ideal(map(numerator, assume.zero)) | |
199 | nonzero = set() | |
200 | for nz in map(numerator, assume.nonzero): | |
201 | for (f,n) in nz.factor(): | |
202 | nonzero.add(f) | |
203 | rnz = zero.reduce(nz) | |
204 | for (f,n) in rnz.factor(): | |
205 | nonzero.add(f) | |
206 | return nonzero | |
207 | ||
208 | ||
209 | def prove_nonzero(R, exprs, assume): | |
210 | """Check whether an expression is provably nonzero, given assumptions""" | |
211 | zero = R.ideal(map(numerator, assume.zero)) | |
212 | nonzero = get_nonzero_set(R, assume) | |
213 | expl = set() | |
214 | ok = True | |
215 | for expr in exprs: | |
216 | if numerator(expr) in zero: | |
217 | return (False, [exprs[expr]]) | |
218 | allexprs = reduce(lambda a,b: numerator(a)*numerator(b), exprs, 1) | |
219 | for (f, n) in allexprs.factor(): | |
220 | if f not in nonzero: | |
221 | ok = False | |
222 | if ok: | |
223 | return (True, None) | |
224 | ok = True | |
225 | for (f, n) in zero.reduce(numerator(allexprs)).factor(): | |
226 | if f not in nonzero: | |
227 | ok = False | |
228 | if ok: | |
229 | return (True, None) | |
230 | ok = True | |
231 | for expr in exprs: | |
232 | for (f,n) in numerator(expr).factor(): | |
233 | if f not in nonzero: | |
234 | ok = False | |
235 | if ok: | |
236 | return (True, None) | |
237 | ok = True | |
238 | for expr in exprs: | |
239 | for (f,n) in zero.reduce(numerator(expr)).factor(): | |
240 | if f not in nonzero: | |
241 | expl.add(exprs[expr]) | |
242 | if expl: | |
243 | return (False, list(expl)) | |
244 | else: | |
245 | return (True, None) | |
246 | ||
247 | ||
248 | def prove_zero(R, exprs, assume): | |
249 | """Check whether all of the passed expressions are provably zero, given assumptions""" | |
250 | r, e = prove_nonzero(R, dict(map(lambda x: (fastfrac(R, x.bot, 1), exprs[x]), exprs)), assume) | |
251 | if not r: | |
252 | return (False, map(lambda x: "Possibly zero denominator: %s" % x, e)) | |
253 | zero = R.ideal(map(numerator, assume.zero)) | |
254 | nonzero = prod(x for x in assume.nonzero) | |
255 | expl = [] | |
256 | for expr in exprs: | |
257 | if not expr.iszero(zero): | |
258 | expl.append(exprs[expr]) | |
259 | if not expl: | |
260 | return (True, None) | |
261 | return (False, expl) | |
262 | ||
263 | ||
264 | def describe_extra(R, assume, assumeExtra): | |
265 | """Describe what assumptions are added, given existing assumptions""" | |
266 | zerox = assume.zero.copy() | |
267 | zerox.update(assumeExtra.zero) | |
268 | zero = R.ideal(map(numerator, assume.zero)) | |
269 | zeroextra = R.ideal(map(numerator, zerox)) | |
270 | nonzero = get_nonzero_set(R, assume) | |
271 | ret = set() | |
272 | # Iterate over the extra zero expressions | |
273 | for base in assumeExtra.zero: | |
274 | if base not in zero: | |
275 | add = [] | |
276 | for (f, n) in numerator(base).factor(): | |
277 | if f not in nonzero: | |
278 | add += ["%s" % f] | |
279 | if add: | |
280 | ret.add((" * ".join(add)) + " = 0 [%s]" % assumeExtra.zero[base]) | |
281 | # Iterate over the extra nonzero expressions | |
282 | for nz in assumeExtra.nonzero: | |
283 | nzr = zeroextra.reduce(numerator(nz)) | |
284 | if nzr not in zeroextra: | |
285 | for (f,n) in nzr.factor(): | |
286 | if zeroextra.reduce(f) not in nonzero: | |
287 | ret.add("%s != 0" % zeroextra.reduce(f)) | |
288 | return ", ".join(x for x in ret) | |
289 | ||
290 | ||
291 | def check_symbolic(R, assumeLaw, assumeAssert, assumeBranch, require): | |
292 | """Check a set of zero and nonzero requirements, given a set of zero and nonzero assumptions""" | |
293 | assume = assumeLaw + assumeAssert + assumeBranch | |
294 | ||
295 | if conflicts(R, assume): | |
296 | # This formula does not apply | |
297 | return None | |
298 | ||
299 | describe = describe_extra(R, assumeLaw + assumeBranch, assumeAssert) | |
300 | ||
301 | ok, msg = prove_zero(R, require.zero, assume) | |
302 | if not ok: | |
303 | return "FAIL, %s fails (assuming %s)" % (str(msg), describe) | |
304 | ||
305 | res, expl = prove_nonzero(R, require.nonzero, assume) | |
306 | if not res: | |
307 | return "FAIL, %s fails (assuming %s)" % (str(expl), describe) | |
308 | ||
309 | if describe != "": | |
310 | return "OK (assuming %s)" % describe | |
311 | else: | |
312 | return "OK" | |
313 | ||
314 | ||
315 | def concrete_verify(c): | |
316 | for k in c.zero: | |
317 | if k != 0: | |
318 | return (False, c.zero[k]) | |
319 | for k in c.nonzero: | |
320 | if k == 0: | |
321 | return (False, c.nonzero[k]) | |
322 | return (True, None) |