1#!/usr/bin/python
2# Copyright 2017 the V8 project authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# for py2/py3 compatibility
7from __future__ import print_function
8
9import argparse
10import math
11import multiprocessing
12import os
13import random
14import subprocess
15import sys
16import tempfile
17
18# Configuration.
19kChars = "0123456789abcdef"
20kBase = 16
21kLineLength = 70  # A bit less than 80.
22kNumInputsGenerate = 20
23kNumInputsStress = 1000
24
25# Internally used sentinels.
26kNo = 0
27kYes = 1
28kRandom = 2
29
30TEST_HEADER = """\
31// Copyright 2017 the V8 project authors. All rights reserved.
32// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
34
35// Generated by %s.
36""" % sys.argv[0]
37
38TEST_BODY = """
39var error_count = 0;
40for (var i = 0; i < data.length; i++) {
41  var d = data[i];
42%s
43}
44if (error_count !== 0) {
45  print("Finished with " + error_count + " errors.")
46  quit(1);
47}"""
48
49def GenRandom(length, negative=kRandom):
50  if length == 0: return "0n"
51  s = []
52  if negative == kYes or (negative == kRandom and (random.randint(0, 1) == 0)):
53    s.append("-")  # 50% chance of negative.
54  s.append("0x")
55  s.append(kChars[random.randint(1, kBase - 1)])  # No leading zero.
56  for i in range(1, length):
57    s.append(kChars[random.randint(0, kBase - 1)])
58  s.append("n")
59  return "".join(s)
60
61def Parse(x):
62  assert x[-1] == 'n', x
63  return int(x[:-1], kBase)
64
65def Format(x):
66  original = x
67  negative = False
68  if x == 0: return "0n"
69  if x < 0:
70    negative = True
71    x = -x
72  s = ""
73  while x > 0:
74    s = kChars[x % kBase] + s
75    x = x / kBase
76  s = "0x" + s + "n"
77  if negative:
78    s = "-" + s
79  assert Parse(s) == original
80  return s
81
82class TestGenerator(object):
83  # Subclasses must implement these.
84  # Returns a JSON snippet defining inputs and expected output for one test.
85  def EmitOne(self): raise NotImplementedError
86  # Returns a snippet of JavaScript that will operate on a variable "d"
87  # whose content is defined by the result of a call to "EmitOne".
88  def EmitTestCore(self): raise NotImplementedError
89
90  def EmitHeader(self):
91    return TEST_HEADER
92
93  def EmitData(self, count):
94    s = []
95    for i in range(count):
96      s.append(self.EmitOne())
97    return "var data = [" + ", ".join(s) + "];"
98
99  def EmitTestBody(self):
100    return TEST_BODY % self.EmitTestCore()
101
102  def PrintTest(self, count):
103    print(self.EmitHeader())
104    print(self.EmitData(count))
105    print(self.EmitTestBody())
106
107  def RunTest(self, count, binary):
108    try:
109      fd, path = tempfile.mkstemp(suffix=".js", prefix="bigint-test-")
110      with open(path, "w") as f:
111        f.write(self.EmitData(count))
112        f.write(self.EmitTestBody())
113      return subprocess.call("%s %s" % (binary, path),
114                             shell=True)
115    finally:
116      os.close(fd)
117      os.remove(path)
118
119class UnaryOp(TestGenerator):
120  # Subclasses must implement these two.
121  def GetOpString(self): raise NotImplementedError
122  def GenerateResult(self, x): raise NotImplementedError
123
124  # Subclasses may override this:
125  def GenerateInput(self):
126    return GenRandom(random.randint(0, kLineLength))
127
128  # Subclasses should not override anything below.
129  def EmitOne(self):
130    x_str = self.GenerateInput()
131    x_num = Parse(x_str)
132    result_num = self.GenerateResult(x_num)
133    result_str = Format(result_num)
134    return "{\n  a: %s,\n  r: %s\n}" % (x_str, result_str)
135
136  def EmitTestCore(self):
137    return """\
138  var r = %(op)sd.a;
139  if (d.r !== r) {
140    print("Input:    " + d.a.toString(%(base)d));
141    print("Result:   " + r.toString(%(base)d));
142    print("Expected: " + d.r);
143    error_count++;
144  }""" % {"op": self.GetOpString(), "base": kBase}
145
146class BinaryOp(TestGenerator):
147  # Subclasses must implement these two.
148  def GetOpString(self): raise NotImplementedError
149  def GenerateResult(self, left, right): raise NotImplementedError
150
151  # Subclasses may override these:
152  def GenerateInputLengths(self):
153    return random.randint(0, kLineLength), random.randint(0, kLineLength)
154
155  def GenerateInputs(self):
156    left_length, right_length = self.GenerateInputLengths()
157    return GenRandom(left_length), GenRandom(right_length)
158
159  # Subclasses should not override anything below.
160  def EmitOne(self):
161    left_str, right_str = self.GenerateInputs()
162    left_num = Parse(left_str)
163    right_num = Parse(right_str)
164    result_num = self.GenerateResult(left_num, right_num)
165    result_str = Format(result_num)
166    return ("{\n  a: %s,\n  b: %s,\n  r: %s\n}" %
167            (left_str, right_str, result_str))
168
169  def EmitTestCore(self):
170    return """\
171  var r = d.a %(op)s d.b;
172  if (d.r !== r) {
173    print("Input A:  " + d.a.toString(%(base)d));
174    print("Input B:  " + d.b.toString(%(base)d));
175    print("Result:   " + r.toString(%(base)d));
176    print("Expected: " + d.r);
177    print("Op: %(op)s");
178    error_count++;
179  }""" % {"op": self.GetOpString(), "base": kBase}
180
181class Neg(UnaryOp):
182  def GetOpString(self): return "-"
183  def GenerateResult(self, x): return -x
184
185class BitNot(UnaryOp):
186  def GetOpString(self): return "~"
187  def GenerateResult(self, x): return ~x
188
189class Inc(UnaryOp):
190  def GetOpString(self): return "++"
191  def GenerateResult(self, x): return x + 1
192
193class Dec(UnaryOp):
194  def GetOpString(self): return "--"
195  def GenerateResult(self, x): return x - 1
196
197class Add(BinaryOp):
198  def GetOpString(self): return "+"
199  def GenerateResult(self, a, b): return a + b
200
201class Sub(BinaryOp):
202  def GetOpString(self): return "-"
203  def GenerateResult(self, a, b): return a - b
204
205class Mul(BinaryOp):
206  def GetOpString(self): return "*"
207  def GenerateResult(self, a, b): return a * b
208  def GenerateInputLengths(self):
209    left_length = random.randint(1, kLineLength)
210    return left_length, kLineLength - left_length
211
212class Div(BinaryOp):
213  def GetOpString(self): return "/"
214  def GenerateResult(self, a, b):
215    result = abs(a) / abs(b)
216    if (a < 0) != (b < 0): result = -result
217    return result
218  def GenerateInputLengths(self):
219    # Let the left side be longer than the right side with high probability,
220    # because that case is more interesting.
221    min_left = kLineLength * 6 / 10
222    max_right = kLineLength * 7 / 10
223    return random.randint(min_left, kLineLength), random.randint(1, max_right)
224
225class Mod(Div):  # Sharing GenerateInputLengths.
226  def GetOpString(self): return "%"
227  def GenerateResult(self, a, b):
228    result = a % b
229    if a < 0 and result > 0:
230      result -= abs(b)
231    if a > 0 and result < 0:
232      result += abs(b)
233    return result
234
235class Shl(BinaryOp):
236  def GetOpString(self): return "<<"
237  def GenerateInputsInternal(self, small_shift_positive):
238    left_length = random.randint(0, kLineLength - 1)
239    left = GenRandom(left_length)
240    small_shift = random.randint(0, 1) == 0
241    if small_shift:
242      right_length = 1 + int(math.log((kLineLength - left_length), kBase))
243      neg = kNo if small_shift_positive else kYes
244    else:
245      right_length = random.randint(0, 3)
246      neg = kYes if small_shift_positive else kNo
247    right = GenRandom(right_length, negative=neg)
248    return left, right
249
250  def GenerateInputs(self): return self.GenerateInputsInternal(True)
251  def GenerateResult(self, a, b):
252    if b < 0: return a >> -b
253    return a << b
254
255class Sar(Shl):  # Sharing GenerateInputsInternal.
256  def GetOpString(self): return ">>"
257  def GenerateInputs(self):
258    return self.GenerateInputsInternal(False)
259  def GenerateResult(self, a, b):
260    if b < 0: return a << -b
261    return a >> b
262
263class BitAnd(BinaryOp):
264  def GetOpString(self): return "&"
265  def GenerateResult(self, a, b): return a & b
266
267class BitOr(BinaryOp):
268  def GetOpString(self): return "|"
269  def GenerateResult(self, a, b): return a | b
270
271class BitXor(BinaryOp):
272  def GetOpString(self): return "^"
273  def GenerateResult(self, a, b): return a ^ b
274
275OPS = {
276  "add": Add,
277  "sub": Sub,
278  "mul": Mul,
279  "div": Div,
280  "mod": Mod,
281  "inc": Inc,
282  "dec": Dec,
283  "neg": Neg,
284  "not": BitNot,
285  "shl": Shl,
286  "sar": Sar,
287  "and": BitAnd,
288  "or": BitOr,
289  "xor": BitXor
290}
291
292OPS_NAMES = ", ".join(sorted(OPS.keys()))
293
294def RunOne(op, num_inputs, binary):
295  return OPS[op]().RunTest(num_inputs, binary)
296def WrapRunOne(args):
297  return RunOne(*args)
298def RunAll(args):
299  for op in args.op:
300    for r in range(args.runs):
301      yield (op, args.num_inputs, args.binary)
302
303def Main():
304  parser = argparse.ArgumentParser(
305      description="Helper for generating or running BigInt tests.")
306  parser.add_argument(
307      "action", help="Action to perform: 'generate' or 'stress'")
308  parser.add_argument(
309      "op", nargs="+",
310      help="Operation(s) to test, one or more of: %s. In 'stress' mode, "
311           "special op 'all' tests all ops." % OPS_NAMES)
312  parser.add_argument(
313      "-n", "--num-inputs", type=int, default=-1,
314      help="Number of input/output sets in each generated test. Defaults to "
315           "%d for 'generate' and '%d' for 'stress' mode." %
316           (kNumInputsGenerate, kNumInputsStress))
317
318  stressopts = parser.add_argument_group("'stress' mode arguments")
319  stressopts.add_argument(
320      "-r", "--runs", type=int, default=1000,
321      help="Number of tests (with NUM_INPUTS each) to generate and run. "
322           "Default: %(default)s.")
323  stressopts.add_argument(
324      "-b", "--binary", default="out/x64.debug/d8",
325      help="The 'd8' binary to use. Default: %(default)s.")
326  args = parser.parse_args()
327
328  for op in args.op:
329    if op not in OPS.keys() and op != "all":
330      print("Invalid op '%s'. See --help." % op)
331      return 1
332
333  if len(args.op) == 1 and args.op[0] == "all":
334    args.op = OPS.keys()
335
336  if args.action == "generate":
337    if args.num_inputs < 0: args.num_inputs = kNumInputsGenerate
338    for op in args.op:
339      OPS[op]().PrintTest(args.num_inputs)
340  elif args.action == "stress":
341    if args.num_inputs < 0: args.num_inputs = kNumInputsStress
342    result = 0
343    pool = multiprocessing.Pool(multiprocessing.cpu_count())
344    for r in pool.imap_unordered(WrapRunOne, RunAll(args)):
345      result = result or r
346    return result
347  else:
348    print("Invalid action '%s'. See --help." % args.action)
349    return 1
350
351if __name__ == "__main__":
352  sys.exit(Main())
353