406 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| """A shuffle-select vector fuzz tester.
 | |
| 
 | |
| This is a python program to fuzz test the LLVM shufflevector and select
 | |
| instructions. It generates a function with a random sequnece of shufflevectors
 | |
| while optionally attaching it with a select instruction (regular or zero merge),
 | |
| maintaining the element mapping accumulated across the function. It then
 | |
| generates a main function which calls it with a different value in each element
 | |
| and checks that the result matches the expected mapping.
 | |
| 
 | |
| Take the output IR printed to stdout, compile it to an executable using whatever
 | |
| set of transforms you want to test, and run the program. If it crashes, it found
 | |
| a bug (an error message with the expected and actual result is printed).
 | |
| """
 | |
| from __future__ import print_function
 | |
| 
 | |
| import random
 | |
| import uuid
 | |
| import argparse
 | |
| 
 | |
| # Possibility of one undef index in generated mask for shufflevector instruction
 | |
| SHUF_UNDEF_POS = 0.15
 | |
| 
 | |
| # Possibility of one undef index in generated mask for select instruction
 | |
| SEL_UNDEF_POS = 0.15
 | |
| 
 | |
| # Possibility of adding a select instruction to the result of a shufflevector
 | |
| ADD_SEL_POS = 0.4
 | |
| 
 | |
| # If we are adding a select instruction, this is the possibility of a
 | |
| # merge-select instruction (1 - MERGE_SEL_POS = possibility of zero-merge-select
 | |
| # instruction.
 | |
| MERGE_SEL_POS = 0.5
 | |
| 
 | |
| 
 | |
| test_template = r'''
 | |
| define internal fastcc {ty} @test({inputs}) noinline nounwind {{
 | |
| entry:
 | |
| {instructions}
 | |
|   ret {ty} {last_name}
 | |
| }}
 | |
| '''
 | |
| 
 | |
| error_template = r'''@error.{lane} = private unnamed_addr global [64 x i8] c"FAIL: lane {lane}, expected {exp}, found %d\0A{padding}"'''
 | |
| 
 | |
| main_template = r'''
 | |
| define i32 @main() {{
 | |
| entry:
 | |
|   ; Create a scratch space to print error messages.
 | |
|   %str = alloca [64 x i8]
 | |
|   %str.ptr = getelementptr inbounds [64 x i8], [64 x i8]* %str, i32 0, i32 0
 | |
| 
 | |
|   ; Build the input vector and call the test function.
 | |
|   %v = call fastcc {ty} @test({inputs})
 | |
|   br label %test.0
 | |
| 
 | |
|   {check_die}
 | |
| }}
 | |
| 
 | |
| declare i32 @strlen(i8*)
 | |
| declare i32 @write(i32, i8*, i32)
 | |
| declare i32 @sprintf(i8*, i8*, ...)
 | |
| declare void @llvm.trap() noreturn nounwind
 | |
| '''
 | |
| 
 | |
| check_template = r'''
 | |
| test.{lane}:
 | |
|   %v.{lane} = extractelement {ty} %v, i32 {lane}
 | |
|   %cmp.{lane} = {i_f}cmp {ordered}ne {scalar_ty} %v.{lane}, {exp}
 | |
|   br i1 %cmp.{lane}, label %die.{lane}, label %test.{n_lane}
 | |
| '''
 | |
| 
 | |
| undef_check_template = r'''
 | |
| test.{lane}:
 | |
| ; Skip this lane, its value is undef.
 | |
|   br label %test.{n_lane}
 | |
| '''
 | |
| 
 | |
| die_template = r'''
 | |
| die.{lane}:
 | |
| ; Capture the actual value and print an error message.
 | |
|   call i32 (i8*, i8*, ...) @sprintf(i8* %str.ptr, i8* getelementptr inbounds ([64 x i8], [64 x i8]* @error.{lane}, i32 0, i32 0), {scalar_ty} %v.{lane})
 | |
|   %length.{lane} = call i32 @strlen(i8* %str.ptr)
 | |
|   call i32 @write(i32 2, i8* %str.ptr, i32 %length.{lane})
 | |
|   call void @llvm.trap()
 | |
|   unreachable
 | |
| '''
 | |
| 
 | |
| class Type:
 | |
|   def __init__(self, is_float, elt_width, elt_num):
 | |
|     self.is_float = is_float        # Boolean
 | |
|     self.elt_width = elt_width      # Integer
 | |
|     self.elt_num = elt_num          # Integer
 | |
| 
 | |
|   def dump(self):
 | |
|     if self.is_float:
 | |
|       str_elt = 'float' if self.elt_width == 32 else 'double'
 | |
|     else:
 | |
|       str_elt = 'i' + str(self.elt_width)
 | |
| 
 | |
|     if self.elt_num == 1:
 | |
|       return str_elt
 | |
|     else:
 | |
|       return '<' + str(self.elt_num) + ' x ' + str_elt + '>'
 | |
| 
 | |
|   def get_scalar_type(self):
 | |
|     return Type(self.is_float, self.elt_width, 1)
 | |
| 
 | |
| 
 | |
| 
 | |
| # Class to represent any value (variable) that can be used.
 | |
| class Value:
 | |
|   def __init__(self, name, ty, value = None):
 | |
|     self.ty = ty                  # Type
 | |
|     self.name = name              # String
 | |
|     self.value = value            # list of integers or floating points
 | |
| 
 | |
| 
 | |
| # Class to represent an IR instruction (shuffle/select).
 | |
| class Instruction(Value):
 | |
|   def __init__(self, name, ty, op0, op1, mask):
 | |
|     Value.__init__(self, name, ty)
 | |
|     self.op0 = op0                # Value
 | |
|     self.op1 = op1                # Value
 | |
|     self.mask = mask              # list of integers
 | |
| 
 | |
|   def dump(self): pass
 | |
| 
 | |
|   def calc_value(self): pass
 | |
| 
 | |
| 
 | |
| # Class to represent an IR shuffle instruction
 | |
| class ShufInstr(Instruction):
 | |
| 
 | |
|   shuf_template = '  {name} = shufflevector {ty} {op0}, {ty} {op1}, <{num} x i32> {mask}\n'
 | |
| 
 | |
|   def __init__(self, name, ty, op0, op1, mask):
 | |
|     Instruction.__init__(self, '%shuf' + name, ty, op0, op1, mask)
 | |
| 
 | |
|   def dump(self):
 | |
|     str_mask = [('i32 ' + str(idx)) if idx != -1 else 'i32 undef' for idx in self.mask]
 | |
|     str_mask = '<' + (', ').join(str_mask) + '>'
 | |
|     return self.shuf_template.format(name = self.name, ty = self.ty.dump(), op0 = self.op0.name,
 | |
|                                op1 = self.op1.name, num = self.ty.elt_num, mask = str_mask)
 | |
| 
 | |
|   def calc_value(self):
 | |
|     if self.value != None:
 | |
|       print('Trying to calculate the value of a shuffle instruction twice')
 | |
|       exit(1)
 | |
| 
 | |
|     result = []
 | |
|     for i in range(len(self.mask)):
 | |
|       index = self.mask[i]
 | |
| 
 | |
|       if index < self.ty.elt_num and index >= 0:
 | |
|         result.append(self.op0.value[index])
 | |
|       elif index >= self.ty.elt_num:
 | |
|         index = index % self.ty.elt_num
 | |
|         result.append(self.op1.value[index])
 | |
|       else: # -1 => undef
 | |
|         result.append(-1)
 | |
| 
 | |
|     self.value = result
 | |
| 
 | |
| 
 | |
| # Class to represent an IR select instruction
 | |
| class SelectInstr(Instruction):
 | |
| 
 | |
|   sel_template = '  {name} = select <{num} x i1> {mask}, {ty} {op0}, {ty} {op1}\n'
 | |
| 
 | |
|   def __init__(self, name, ty, op0, op1, mask):
 | |
|     Instruction.__init__(self, '%sel' + name, ty, op0, op1, mask)
 | |
| 
 | |
|   def dump(self):
 | |
|     str_mask = [('i1 ' + str(idx)) if idx != -1 else 'i1 undef' for idx in self.mask]
 | |
|     str_mask = '<' + (', ').join(str_mask) + '>'
 | |
|     return self.sel_template.format(name = self.name, ty = self.ty.dump(), op0 = self.op0.name,
 | |
|                                op1 = self.op1.name, num = self.ty.elt_num, mask = str_mask)
 | |
| 
 | |
|   def calc_value(self):
 | |
|     if self.value != None:
 | |
|       print('Trying to calculate the value of a select instruction twice')
 | |
|       exit(1)
 | |
| 
 | |
|     result = []
 | |
|     for i in range(len(self.mask)):
 | |
|       index = self.mask[i]
 | |
| 
 | |
|       if index == 1:
 | |
|         result.append(self.op0.value[i])
 | |
|       elif index == 0:
 | |
|         result.append(self.op1.value[i])
 | |
|       else: # -1 => undef
 | |
|         result.append(-1)
 | |
| 
 | |
|     self.value = result
 | |
| 
 | |
| 
 | |
| # Returns a list of Values initialized with actual numbers according to the
 | |
| # provided type
 | |
| def gen_inputs(ty, num):
 | |
|   inputs = []
 | |
|   for i in range(num):
 | |
|     inp = []
 | |
|     for j in range(ty.elt_num):
 | |
|       if ty.is_float:
 | |
|         inp.append(float(i*ty.elt_num + j))
 | |
|       else:
 | |
|         inp.append((i*ty.elt_num + j) % (1 << ty.elt_width))
 | |
|     inputs.append(Value('%inp' + str(i), ty, inp))
 | |
| 
 | |
|   return inputs
 | |
| 
 | |
| 
 | |
| # Returns a random vector type to be tested
 | |
| # In case one of the dimensions (scalar type/number of elements) is provided,
 | |
| # fill the blank dimension and return appropriate Type object.
 | |
| def get_random_type(ty, num_elts):
 | |
|   if ty != None:
 | |
|     if ty == 'i8':
 | |
|       is_float = False
 | |
|       width = 8
 | |
|     elif ty == 'i16':
 | |
|       is_float = False
 | |
|       width = 16
 | |
|     elif ty == 'i32':
 | |
|       is_float = False
 | |
|       width = 32
 | |
|     elif ty == 'i64':
 | |
|       is_float = False
 | |
|       width = 64
 | |
|     elif ty == 'f32':
 | |
|       is_float = True
 | |
|       width = 32
 | |
|     elif ty == 'f64':
 | |
|       is_float = True
 | |
|       width = 64
 | |
| 
 | |
|   int_elt_widths = [8, 16, 32, 64]
 | |
|   float_elt_widths = [32, 64]
 | |
| 
 | |
|   if num_elts == None:
 | |
|     num_elts = random.choice(range(2, 65))
 | |
| 
 | |
|   if ty == None:
 | |
|     # 1 for integer type, 0 for floating-point
 | |
|     if random.randint(0,1):
 | |
|       is_float = False
 | |
|       width = random.choice(int_elt_widths)
 | |
|     else:
 | |
|       is_float = True
 | |
|       width = random.choice(float_elt_widths)
 | |
| 
 | |
|   return Type(is_float, width, num_elts)
 | |
| 
 | |
| 
 | |
| # Generate mask for shufflevector IR instruction, with SHUF_UNDEF_POS possibility
 | |
| # of one undef index.
 | |
| def gen_shuf_mask(ty):
 | |
|   mask = []
 | |
|   for i in range(ty.elt_num):
 | |
|     if SHUF_UNDEF_POS/ty.elt_num > random.random():
 | |
|       mask.append(-1)
 | |
|     else:
 | |
|       mask.append(random.randint(0, ty.elt_num*2 - 1))
 | |
| 
 | |
|   return mask
 | |
| 
 | |
| 
 | |
| # Generate mask for select IR instruction, with SEL_UNDEF_POS possibility
 | |
| # of one undef index.
 | |
| def gen_sel_mask(ty):
 | |
|   mask = []
 | |
|   for i in range(ty.elt_num):
 | |
|     if SEL_UNDEF_POS/ty.elt_num > random.random():
 | |
|       mask.append(-1)
 | |
|     else:
 | |
|       mask.append(random.randint(0, 1))
 | |
| 
 | |
|   return mask
 | |
| 
 | |
| # Generate shuffle instructions with optional select instruction after.
 | |
| def gen_insts(inputs, ty):
 | |
|   int_zero_init = Value('zeroinitializer', ty, [0]*ty.elt_num)
 | |
|   float_zero_init = Value('zeroinitializer', ty, [0.0]*ty.elt_num)
 | |
| 
 | |
|   insts = []
 | |
|   name_idx = 0
 | |
|   while len(inputs) > 1:
 | |
|     # Choose 2 available Values - remove them from inputs list.
 | |
|     [idx0, idx1] = sorted(random.sample(range(len(inputs)), 2))
 | |
|     op0 = inputs[idx0]
 | |
|     op1 = inputs[idx1]
 | |
| 
 | |
|     # Create the shuffle instruction.
 | |
|     shuf_mask = gen_shuf_mask(ty)
 | |
|     shuf_inst = ShufInstr(str(name_idx), ty, op0, op1, shuf_mask)
 | |
|     shuf_inst.calc_value()
 | |
| 
 | |
|     # Add the new shuffle instruction to the list of instructions.
 | |
|     insts.append(shuf_inst)
 | |
| 
 | |
|     # Optionally, add select instruction with the result of the previous shuffle.
 | |
|     if random.random() < ADD_SEL_POS:
 | |
|       #  Either blending with a random Value or with an all-zero vector.
 | |
|       if random.random() < MERGE_SEL_POS:
 | |
|         op2 = random.choice(inputs)
 | |
|       else:
 | |
|         op2 = float_zero_init if ty.is_float else int_zero_init
 | |
| 
 | |
|       select_mask = gen_sel_mask(ty)
 | |
|       select_inst = SelectInstr(str(name_idx), ty, shuf_inst, op2, select_mask)
 | |
|       select_inst.calc_value()
 | |
| 
 | |
|       # Add the select instructions to the list of instructions and to the available Values.
 | |
|       insts.append(select_inst)
 | |
|       inputs.append(select_inst)
 | |
|     else:
 | |
|       # If the shuffle instruction is not followed by select, add it to the available Values.
 | |
|       inputs.append(shuf_inst)
 | |
| 
 | |
|     del inputs[idx1]
 | |
|     del inputs[idx0]
 | |
|     name_idx += 1
 | |
| 
 | |
|   return insts
 | |
| 
 | |
| 
 | |
| def main():
 | |
|   parser = argparse.ArgumentParser(description=__doc__)
 | |
|   parser.add_argument('--seed', default=str(uuid.uuid4()),
 | |
|                       help='A string used to seed the RNG')
 | |
|   parser.add_argument('--max-num-inputs', type=int, default=20,
 | |
|           help='Specify the maximum number of vector inputs for the test. (default: 20)')
 | |
|   parser.add_argument('--min-num-inputs', type=int, default=10,
 | |
|           help='Specify the minimum number of vector inputs for the test. (default: 10)')
 | |
|   parser.add_argument('--type', default=None,
 | |
|                       help='''
 | |
|                           Choose specific type to be tested.
 | |
|                           i8, i16, i32, i64, f32 or f64.
 | |
|                           (default: random)''')
 | |
|   parser.add_argument('--num-elts', default=None, type=int,
 | |
|                       help='Choose specific number of vector elements to be tested. (default: random)')
 | |
|   args = parser.parse_args()
 | |
| 
 | |
|   print('; The seed used for this test is ' + args.seed)
 | |
| 
 | |
|   assert args.min_num_inputs < args.max_num_inputs , "Minimum value greater than maximum."
 | |
|   assert args.type in [None, 'i8', 'i16', 'i32', 'i64', 'f32', 'f64'], "Illegal type."
 | |
|   assert args.num_elts == None or args.num_elts > 0, "num_elts must be a positive integer."
 | |
| 
 | |
|   random.seed(args.seed)
 | |
|   ty = get_random_type(args.type, args.num_elts)
 | |
|   inputs = gen_inputs(ty, random.randint(args.min_num_inputs, args.max_num_inputs))
 | |
|   inputs_str = (', ').join([inp.ty.dump() + ' ' + inp.name for inp in inputs])
 | |
|   inputs_values = [inp.value for inp in inputs]
 | |
| 
 | |
|   insts = gen_insts(inputs, ty)
 | |
| 
 | |
|   assert len(inputs) == 1, "Only one value should be left after generating phase"
 | |
|   res = inputs[0]
 | |
| 
 | |
|   # print the actual test function by dumping the generated instructions.
 | |
|   insts_str = ''.join([inst.dump() for inst in insts])
 | |
|   print(test_template.format(ty = ty.dump(), inputs = inputs_str,
 | |
|                              instructions = insts_str, last_name = res.name))
 | |
| 
 | |
|   # Print the error message templates as global strings
 | |
|   for i in range(len(res.value)):
 | |
|     pad = ''.join(['\\00']*(31 - len(str(i)) - len(str(res.value[i]))))
 | |
|     print(error_template.format(lane = str(i), exp = str(res.value[i]),
 | |
|                                 padding = pad))
 | |
| 
 | |
|   # Prepare the runtime checks and failure handlers.
 | |
|   scalar_ty = ty.get_scalar_type()
 | |
|   check_die = ''
 | |
|   i_f = 'f' if ty.is_float else 'i'
 | |
|   ordered = 'o' if ty.is_float else ''
 | |
|   for i in range(len(res.value)):
 | |
|     if res.value[i] != -1:
 | |
|       # Emit runtime check for each non-undef expected value.
 | |
|       check_die += check_template.format(lane = str(i), n_lane = str(i+1),
 | |
|                              ty = ty.dump(), i_f = i_f, scalar_ty = scalar_ty.dump(),
 | |
|                              exp = str(res.value[i]), ordered = ordered)
 | |
|       # Emit failure handler for each runtime check with proper error message
 | |
|       check_die += die_template.format(lane = str(i), scalar_ty = scalar_ty.dump())
 | |
|     else:
 | |
|       # Ignore lanes with undef result
 | |
|       check_die += undef_check_template.format(lane = str(i), n_lane = str(i+1))
 | |
| 
 | |
|   check_die += '\ntest.' + str(len(res.value)) + ':\n'
 | |
|   check_die += '  ret i32 0'
 | |
| 
 | |
|   # Prepare the input values passed to the test function.
 | |
|   inputs_values = [', '.join([scalar_ty.dump() + ' ' + str(i) for i in inp]) for inp in inputs_values]
 | |
|   inputs = ', '.join([ty.dump() + ' <' + inp + '>' for inp in inputs_values])
 | |
| 
 | |
|   print(main_template.format(ty = ty.dump(), inputs = inputs, check_die = check_die))
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|   main()
 | |
| 
 | |
| 
 |