Пример #1
0
    def _assign(self, variable: Union[c_ast.ID, c_ast.ArrayRef],
                expression: ExprType, node: c_ast.Node):
        """T-Asgn rule, which can be re-used both in Assignment node and Decl node"""
        # get new distance from the assignment expression (T-Asgn)
        variable_name = variable.name if isinstance(
            variable, c_ast.ID) else variable.name.name
        var_aligned, var_shadow, *_ = self._type_system.get_types(
            variable_name)
        aligned, shadow = DistanceGenerator(
            self._type_system).visit(expression)
        if self._loop_level == 0:
            # insert x^align = n^align if x^aligned is *
            if var_aligned == '*' or aligned != '0':
                self._insert_at(
                    node,
                    parse(
                        f'{constants.ALIGNED_DISTANCE}_{variable_name} = {aligned}'
                    ),
                    after=True)

            if self._enable_shadow:
                # generate x^shadow = x + x^shadow - e according to (T-Asgn)
                if self._pc:
                    if isinstance(variable, c_ast.ID):
                        shadow_distance = c_ast.ID(
                            name=f'{constants.SHADOW_DISTANCE}_{variable_name}'
                        )
                    elif isinstance(variable, c_ast.ArrayRef):
                        shadow_distance = c_ast.ArrayRef(
                            name=f'{constants.SHADOW_DISTANCE}_{variable_name}',
                            subscript=variable.subscript)
                    else:
                        raise NotImplementedError(
                            f'Assigned value type not supported {type(variable)}'
                        )
                    # insert x^shadow = x + x^shadow - e;
                    insert_node = c_ast.Assignment(
                        op='=',
                        lvalue=shadow_distance,
                        rvalue=c_ast.BinaryOp(op='-',
                                              left=c_ast.BinaryOp(
                                                  op='+',
                                                  left=variable,
                                                  right=shadow_distance),
                                              right=expression))
                    self._insert_at(node, insert_node, after=False)
                # insert x^shadow = n^shadow if n^shadow is not 0
                elif var_shadow == '*' or shadow != '0':
                    self._insert_at(
                        node,
                        parse(
                            f'{constants.SHADOW_DISTANCE}_{variable_name} = {shadow}'
                        ),
                        after=True)

        shadow_distance = '*' if self._pc or shadow != '0' or var_shadow == '*' else '0'
        aligned_distance = '*' if aligned != '0' or var_aligned == '*' else '0'
        self._type_system.update_distance(variable_name, aligned_distance,
                                          shadow_distance)
Пример #2
0
 def process(self, node: Union[c_ast.FileAST, c_ast.FuncDef]) \
         -> Tuple[c_ast.FuncDef, TypeSystem, Iterable[str], Iterable[str], str]:
     if not isinstance(node, (c_ast.FileAST, c_ast.FuncDef)):
         raise TypeError('node must be of type Union(FileAST, FuncDef)')
     processed = self.visit(node)
     # scale up the random noise scales due to limitations of symbolic executor KLEE only supporting integers
     all_scales = [
         f'1 / ({generate(func_call.args.exprs[0])})'
         for func_call in self._random_scales
     ]
     lcm = sp.lcm(
         tuple(
             map(lambda scale: sp.fraction(scale)[1],
                 all_scales + [self._goal])))
     # TODO: use less hackery method to tackle the Pow operation in sympy
     # see also https://stackoverflow.com/questions/14264431/expanding-algebraic-powers-in-python-sympy
     if str(lcm) != '1':
         logger.warning(
             f'Scaling down the noise scales by {lcm} due to limitations of symbolic executor KLEE '
             f'only supporting integers, therefore the cost calculations will not contain divisions.'
         )
         for scale in self._random_scales:
             scale.args.exprs[0] = parse(
                 expr_simplify(
                     f'({generate(scale.args.exprs[0])}) / ({lcm})'))
         logger.warning(
             f'Scaling up the final goal by {lcm} as well due to the cost scale-ups.'
         )
         self._goal = expr_simplify(f'({self._goal}) * ({lcm})')
     return processed, self._type_system, self._preconditions, self._hole_preconditions, self._goal
Пример #3
0
def transform(code: str, enable_shadow: bool = False):
    node = parse(code)
    # first preprocess the node, extract the annotations and do sanity checks
    logger.info('Transformation starts')
    logger.info('Preprocess starts')
    preprocessed, type_system, preconditions, hole_preconditions, goal = Preprocessor(
    ).process(node)
    # TODO: here we use a simple regex to find custom hole variables
    holes = list(set(re.findall(f'{constants.HOLE}_\\d+', code)))
    # update the type system with the custom hole variables
    for hole in holes:
        type_system.update_base_type(hole, 'int', False)
    # update the type system with the symbolic cost variables
    type_system.update_base_type(constants.SYMBOLIC_COST, 'int', True)
    type_system.update_distance(constants.SYMBOLIC_COST, '0', '0')
    logger.info('Preprocess finished')
    logger.debug(f'Initial type system : {type_system}')
    logger.debug(f'Extracted preconditions : {preconditions}')
    logger.debug(f'Final goal to check : {goal}')
    logger.info('Core transformation starts')
    transformed, type_system = Transformer(
        type_system, enable_shadow).transform(preprocessed)
    templates, alignment_array_types = RandomDistanceGenerator(
        type_system, enable_shadow).generate_macros(transformed)
    logger.debug(f'alignment array types: {alignment_array_types}')
    logger.info('Core transformation finishes')
    logger.info('Postprocess starts')
    postprocessed, sample_array_size_func = PostProcessor(
        type_system, custom_variables=holes).process(transformed)
    logger.info('Postprocess finishes')
    code_template = Template(type_system, postprocessed, templates, goal,
                             alignment_array_types, sample_array_size_func,
                             preconditions, holes, hole_preconditions)
    return code_template
Пример #4
0
def test_multiple_functions():
    source = f"{plain_function} {'int b() { return 1; }'}"
    node = parse(source)
    with pytest.raises(ValueError):
        Preprocessor().process(node)

    # add a target function, should not raise any error
    Preprocessor(target_function='a').process(node)
Пример #5
0
 def transform(self,
               node: c_ast.FuncDef) -> Tuple[c_ast.FuncDef, TypeSystem]:
     if not isinstance(node, c_ast.FuncDef):
         raise TypeError(
             'Input node must have type c_ast.FuncDef, try to preprocess the node first.'
         )
     transformed = self.visit_FuncDef(node)
     # add returning the final cost variable
     transformed.body.block_items.append(
         parse(f'return {constants.V_EPSILON};'))
     return transformed, self._type_system
Пример #6
0
    def visit_FuncCall(self, node: c_ast.FuncCall):
        """T-Return rule, which adds assertion after the OUTPUT command."""
        if self._loop_level == 0 and node.name.name == constants.OUTPUT:
            distance_generator = DistanceGenerator(self._type_system)
            # add assertion of the output distance == 0
            aligned_distance = distance_generator.visit(node.args.exprs[0])[0]
            # there is no need to add assertion if the distance is obviously 0
            if aligned_distance != '0':
                self._insert_at(
                    node,
                    parse(f'{constants.ASSERT}({aligned_distance} == 0)'))

        self.generic_visit(node)
Пример #7
0
def assert_templates(name: str, templates: Dict[str, Tuple[Set[str], Set[str]]], enable_shadow=False):
    # remove comments
    pattern = re.compile(r'\/\/.*|\/\*.*\*\/')
    with open(example_folder / Path(name).with_suffix('.c')) as f:
        node = parse(pattern.sub('', f.read()))
    preprocessed, type_system, preconditions, hole_preconditions, goal = Preprocessor().process(node)
    transformed, type_system = Transformer(type_system, enable_shadow=enable_shadow).transform(preprocessed)
    generate_templates = RandomDistanceGenerator(type_system).generate(transformed)
    for name, (conditions, variables) in generate_templates.items():
        assert name in templates, f'Template for {name} is generated but not specified'
        specified_conditions, specified_variables = templates[name]
        assert specified_conditions == conditions
        assert specified_variables == variables
Пример #8
0
    def visit_FuncDef(self, node: c_ast.FuncDef) -> c_ast.FuncDef:
        # the start of the transformation
        logger.info(f'Start transforming function {node.decl.name} ...')

        # make a deep copy and transform on the copied node
        node = deepcopy(node)

        self._parameters = tuple(decl.name
                                 for decl in node.decl.type.args.params)
        logger.debug(f'Params: {self._parameters}')

        # visit children
        self.generic_visit(node)

        insert_statements = [
            # insert float CHECKDP_v_epsilon = 0;
            parse(f'float {constants.V_EPSILON} = 0'),
            # insert int SAMPLE_INDEX = 0;
            parse(f'int {constants.SAMPLE_INDEX} = 0')
        ]

        # add declarations of distance variables for dynamically tracked local variables
        for name, *distances, _, _ in filter(
                lambda variable: variable[0] not in self._parameters,
                self._type_system.variables()):
            for version, distance in zip(
                (constants.ALIGNED_DISTANCE, constants.SHADOW_DISTANCE),
                    distances):
                # skip shadow generation if enable_shadow is not specified
                if version == constants.SHADOW_DISTANCE and not self._enable_shadow:
                    continue
                if distance == '*' or distance == f'{version}_{name}':
                    insert_statements.append(
                        parse(f'float {version}_{name} = 0'))

        # prepend the inserted statements
        node.body.block_items[:0] = insert_statements
        return node
Пример #9
0
    def visit_FuncDef(self, node: c_ast.FuncDef) -> c_ast.FuncDef:
        self._parameters = tuple(decl.name for decl in node.decl.type.args.params)
        query = self._parameters[0]
        # if it is a dynamically tracked parameter, add new parameters
        # add distance variables for dynamically tracked parameters
        for name in self._parameters:
            *distances, _, _ = self._type_system.get_types(name)
            for index, distance in enumerate(distances):
                version = constants.ALIGNED_DISTANCE if index == 0 else constants.SHADOW_DISTANCE
                if distance == '*':
                    _, _, plain_type, is_array = self._type_system.get_types(name)
                    distance_variable = f'{plain_type} {version}_{name}'
                    distance_variable = distance_variable + '[]' if is_array else distance_variable
                    node.decl.type.args.params.append(parse(distance_variable))
                    # add the plain type to the type system
                    self._type_system.update_base_type(f'{version}_{name}', plain_type, is_array)
                    if name == query:
                        # only generate aligned distance for query variable
                        break

        # add sample array variable
        node.decl.type.args.params.append(parse(f'int {constants.SAMPLE_ARRAY}[]'))
        self._type_system.update_base_type(f'{constants.SAMPLE_ARRAY}', 'int', True)

        # add alignment array
        node.decl.type.args.params.append(parse(f'int {constants.ALIGNMENT_ARRAY}[]'))
        self._type_system.update_base_type(f'{constants.ALIGNMENT_ARRAY}', 'int', True)

        # add custom variables
        for hole in self._custom_varaibles:
            node.decl.type.args.params.append(parse(f'int {hole}'))

        # change the return type to int
        node.decl.type.type.type.names = ['int']

        self.generic_visit(node)

        return node
Пример #10
0
 def random_distance(self, alignments_values):
     alignments_values = alignments_values[-1][constants.ALIGNMENT_ARRAY]
     alignments = {}
     for match in re.finditer(
             f'#define\\s+{constants.RANDOM_DISTANCE}_([a-zA-Z_][a-zA-Z0-9_]+)\\s+(.*)',
             self._random_distances):
         alignment = match.group(2)
         for index, value in enumerate(alignments_values):
             alignment = alignment.replace(
                 f'{constants.ALIGNMENT_ARRAY}[{index}]', str(value))
         # try to simplify the expression (mostly eliminating the terms with coefficient 0)
         try:
             alignment = _simplify_node(parse(alignment))
         finally:
             alignments[match.group(1)] = alignment
     return alignments
Пример #11
0
    def generate_macros(
            self, node: c_ast.FuncDef) -> Tuple[str, List[AlignmentIndexType]]:
        """Generate C-style macros for random variable templates"""
        self.visit(node)
        logger.debug(f'The generated templates: {self._templates}')

        # iterate through all collected templates and insert macros
        # e.g. #define CHECKDP_RANDOM_DISTANCE_eta (condition_set ? variable_set)
        distance_generator = DistanceGenerator(self._type_system)
        inserted, alignment_array_types = [], []
        for random_variable, (conditions,
                              variables) in self._templates.items():
            distance_variables = (distance_generator.visit(parse(variable))[0]
                                  for variable in variables)
            if self._enable_shadow:
                # generate template for selectors
                if len(conditions) == 0:
                    template = constants.SELECT_ALIGNED
                else:
                    template = _generate_random_distance(tuple(conditions),
                                                         tuple(),
                                                         alignment_array_types,
                                                         is_selector=True)
                logger.debug(
                    f'Generated selector template for {random_variable}: {template}'
                )
                inserted.append(
                    f'#define {constants.SELECTOR}_{random_variable} ({template})'
                )
            # convert the sets to tuples since our naive recursive implementation requires orders
            template = _generate_random_distance(tuple(conditions),
                                                 tuple(distance_variables),
                                                 alignment_array_types)
            logger.debug(
                f'Generated alignment template for {random_variable}: {template}'
            )
            inserted.append(
                f'#define {constants.RANDOM_DISTANCE}_{random_variable} ({template})'
            )

        logger.debug(
            f'Final alignment array size: {len(alignment_array_types)}')
        return '\n'.join(inserted), alignment_array_types
Пример #12
0
    def _instrument(self, type_system_1: TypeSystem, type_system_2: TypeSystem,
                    pc: bool) -> Sequence[c_ast.Assignment]:
        inserted_statement = []

        for name in set(type_system_1.names()).intersection(
                type_system_2.names()):
            for version, distance_1, distance_2 in zip(
                (constants.ALIGNED_DISTANCE, constants.SHADOW_DISTANCE),
                    type_system_1.get_types(name),
                    type_system_2.get_types(name)):
                if distance_1 is None or distance_2 is None:
                    continue
                # do not instrument shadow statements if pc = True or enable_shadow is not specified
                if not self._enable_shadow or (
                        version == constants.SHADOW_DISTANCE and pc):
                    continue
                if distance_1 != '*' and distance_2 == '*':
                    inserted_statement.append(
                        parse(f'{version}_{name} = {distance_1}'))

        return inserted_statement
Пример #13
0
    def visit_Decl(self, node: c_ast.Decl):
        logger.debug(f'Line {str(node.coord.line)}: {generate(node)}')

        # ignore the FuncDecl node since it's already preprocessed
        if isinstance(node.type, c_ast.FuncDecl):
            return
        # TODO - Enhancement: Array Declaration support
        elif not isinstance(node.type, c_ast.TypeDecl):
            raise NotImplementedError(
                f'Declaration type {node.type} currently not supported for statement: {generate(node)}'
            )

        # if declarations are in function body, store distance into type system
        assert isinstance(node.type, c_ast.TypeDecl)
        # if no initial value is given, default to (0, 0)
        if not node.init:
            self._type_system.update_distance(node.name, '0', '0')
        # else update the distance to the distance of initial value (T-Asgn)
        elif isinstance(
                node.init,
            (c_ast.Constant, c_ast.BinaryOp, c_ast.BinaryOp, c_ast.UnaryOp)):
            self._assign(c_ast.ID(name=node.name), node.init, node)
        # if it is random variable declaration (T-Laplace)
        elif isinstance(node.init, c_ast.FuncCall):
            if self._enable_shadow and self._pc:
                raise ValueError(
                    'Cannot have random variable assignment in shadow-diverging branches'
                )
            self._random_variables.add(node.name)
            logger.debug(f'Random variables: {self._random_variables}')

            # set the random variable distance
            self._type_system.update_distance(node.name, '*', '0')

            if self._enable_shadow:
                # since we have to dynamically switch (the aligned distances) to shadow version, we have to guard the
                # switch with the selector
                shadow_type_system = deepcopy(self._type_system)
                for name, _, shadow_distance, _, _ in shadow_type_system.variables(
                ):
                    # skip the distance of custom holes
                    if constants.HOLE in name:
                        continue
                    shadow_type_system.update_distance(name, shadow_distance,
                                                       shadow_distance)
                self._type_system.merge(shadow_type_system)

            if self._loop_level == 0:
                to_inserts = []

                if self._enable_shadow:
                    # insert distance updates for normal variables
                    distance_update_statements = []
                    for name, align, shadow, _, _ in self._type_system.variables(
                    ):
                        if align == '*' and name not in self._parameters and name != node.name:
                            shadow_distance = f'{constants.SHADOW_DISTANCE}_{name}' if shadow == '*' else shadow
                            distance_update_statements.append(
                                parse(
                                    f'{constants.ALIGNED_DISTANCE}_{name} = {shadow_distance};'
                                ))
                    distance_update = c_ast.If(cond=parse(
                        f'{constants.SELECTOR}_{node.name} == {constants.SELECT_SHADOW}'
                    ),
                                               iftrue=c_ast.Compound(
                                                   block_items=
                                                   distance_update_statements),
                                               iffalse=None)
                    to_inserts.append(distance_update)

                # insert distance template for the variable
                distance = parse(
                    f'{constants.ALIGNED_DISTANCE}_{node.name} = {constants.RANDOM_DISTANCE}_{node.name}'
                )
                to_inserts.append(distance)

                # insert cost variable update statement
                scale = generate(node.init.args.exprs[0])
                cost = expr_simplify(
                    f'(Abs({constants.ALIGNED_DISTANCE}_{node.name}) * (1 / ({scale})))'
                )
                # calculate v_epsilon by combining normal cost and sampling cost
                if self._enable_shadow:
                    previous_cost = \
                        f'(({constants.SELECTOR}_{node.name} == {constants.SELECT_ALIGNED}) ? {constants.V_EPSILON} : 0)'
                else:
                    previous_cost = constants.V_EPSILON

                v_epsilon = parse(
                    f'{constants.V_EPSILON} = {previous_cost} + {cost}')
                to_inserts.append(v_epsilon)

                # transform sampling command to havoc command
                node.init = parse(
                    f'{constants.SAMPLE_ARRAY}[{constants.SAMPLE_INDEX}]')
                to_inserts.append(
                    parse(
                        f'{constants.SAMPLE_INDEX} = {constants.SAMPLE_INDEX} + 1;'
                    ))

                self._insert_at(node, to_inserts)
        else:
            raise NotImplementedError(
                f'Initial value currently not supported: {node.init}')

        logger.debug(f'types: {self._type_system}')
Пример #14
0
def test_sensitivities():
    for sensitivity in ('ALL_DIFFER', 'ONE_DIFFER', 'INCREASING',
                        'DECREASING'):
        node = parse(plain_function.replace('ALL_DIFFER', sensitivity))
        _, _, preconditions, _, _ = Preprocessor().process(node)
        assert sensitivity in preconditions