Exemple #1
0
    def modeled_interpreter(interps):
        actual_interp, model_interp = interps

        dom = domains.Product(actual_interp.domain, model_interp.domain)

        @Transformer.as_transformer
        def original_signature(sig):
            if sig.contains(dom):
                return sig, sig.substituted(dom, actual_interp.domain)

        @Transformer.as_transformer
        def transform_implementation(sig_impl):
            sig, (def_impl, inv_impl) = sig_impl
            implicitly_converted_inputs = set(
                i for i, d in enumerate(sig.input_domains) if d == dom)
            implicitly_converted_output = sig.output_domain == dom
            model_top = model_interp.domain.top

            def new_def_impl(*args):
                res = def_impl(
                    *(arg[0] if i in implicitly_converted_inputs else arg
                      for i, arg in enumerate(args)))

                return (res, model_top) if implicitly_converted_output else res

            def new_inv_impl(expected, *constrs):
                new_expected = (expected[0]
                                if implicitly_converted_output else expected)
                new_constrs = tuple(constrs[i][0] if i in
                                    implicitly_converted_inputs else constrs[i]
                                    for i in range(len(constrs)))
                res = inv_impl(new_expected, *new_constrs)

                return tuple(
                    (res[i],
                     model_top) if i in implicitly_converted_inputs else res[i]
                    for i in range(len(res)))

            return new_def_impl, new_inv_impl

        @Transformer.as_transformer
        def model_provider(sig):
            if sig.name == ops.GET_MODEL and sig.input_domains[0] == dom:
                return product_ops.getter(1), product_ops.inv_getter(dom, 1)

        @Transformer.make_memoizing
        @Transformer.from_transformer_builder
        def provider():
            return (
                original_signature >>
                (Transformer.identity() & actual_interp.def_provider_builder)
                >> transform_implementation) | model_provider

        def builder(lit):
            return actual_interp.builder(lit), model_interp.domain.top

        return TypeInterpretation(dom, lambda _: provider, builder)
Exemple #2
0
from lalcheck.ai import domains
from lalcheck.ai.domain_ops import (boolean_ops, interval_ops,
                                    finite_lattice_ops, product_ops)
from itertools import product

first_elem = domains.Intervals(-2, 2)
second_elem = boolean_ops.Boolean

test_dom = domains.Product(first_elem, second_elem)

elem_eqs = [interval_ops.eq(first_elem), finite_lattice_ops.eq(second_elem)]

elem_inv_eqs = [
    interval_ops.inv_eq(first_elem),
    finite_lattice_ops.inv_eq(second_elem)
]

elem_inv_neqs = [
    interval_ops.inv_neq(first_elem),
    finite_lattice_ops.inv_neq(second_elem)
]


class InverseOperationTest(object):
    """
    Abstract test class. Can be inherited to test the inverse of a binary
    operation on a sparse array domain.
    """
    def __init__(self, doms, debug):
        self.domains = doms
        self.debug = debug
Exemple #3
0
    def array_interpreter(attribute_interps):
        """
        :param (iterable[TypeInterpretation], TypeInterpretation)
            attribute_interps: The interpretations of the types of the indices,
            and the interpretation of the type of the components.

        :return: The interpretation for the array type

        :rtype: TypeInterpretation
        """
        index_interps, component_interp = attribute_interps

        indices_dom = domains.Product(*(interp.domain
                                        for interp in index_interps))
        comp_dom = component_interp.domain

        array_dom = domains.SparseArray(indices_dom, comp_dom, max_elems=15)

        call_sig = _signer((array_dom, ) + tuple(indices_dom.domains),
                           comp_dom)(ops.CALL)

        updated_sig = _signer(
            (array_dom, comp_dom) + tuple(indices_dom.domains),
            array_dom)(ops.UPDATED)

        # Get the raw implementations of array operations:

        array_get = sparse_array_ops.get(array_dom)
        array_updated = sparse_array_ops.updated(array_dom)
        array_index_range = sparse_array_ops.index_range(array_dom)
        array_in_values_of = sparse_array_ops.in_values_of(array_dom)
        array_inv_get = sparse_array_ops.inv_get(array_dom)
        array_inv_updated = sparse_array_ops.inv_updated(array_dom)
        array_inv_index_range = sparse_array_ops.inv_index_range(array_dom)
        array_inv_in_values_of = sparse_array_ops.inv_in_values_of(array_dom)
        array_string = sparse_array_ops.array_string(array_dom)

        # Wrap them in actual implementations. Indeed, the format of the
        # arguments differ between the function call generated during the IR
        # and the one defined in array_ops. The reason is that the index domain
        # of our sparse array domain is a Product of all index domains, meaning
        # that the expected parameter type is a tuple (an element of that
        # product domain). However, the calls generated during the IR are
        # flatten the indices. For example, we have:
        #
        # Get(my_two_dimensional_array, 3, 4).
        #
        # Instead of
        #
        # Get(my_two_dimensional_array, (3, 4)).
        #
        # This choice was made due to the fact that the expression (3, 4)
        # does not existing in the original source and therefore would require
        # more work to type.
        # So, the purpose of these wrapper is to transform a flattened list of
        # indices into a list of tuple.

        def actual_get(array, *indices):
            return array_get(array, indices)

        def actual_updated(array, val, *indices):
            return array_updated(array, val, indices)

        def actual_string(*args):
            # Every index i must become (i,) to be a valid element of the index
            # domain. Since args is a flattened list of pairs (index, elem),
            # an index occurs every even argument.
            return array_string(*((arg, ) if i % 2 == 0 else arg
                                  for i, arg in enumerate(args)))

        def actual_in_index_range(dim):
            idx_included = util_ops.included(indices_dom.domains[dim - 1])

            def do(index, array):
                return idx_included(index, array_index_range(array)[dim - 1])

            return do

        def actual_inv_get(res, array_constr, *indices_constr):
            arr, indices = array_inv_get(res, array_constr, indices_constr)
            return (arr, ) + indices

        def actual_inv_udpated(res, array_constr, val_constr, *indices_constr):
            return array_inv_updated(res, array_constr, val_constr,
                                     indices_constr)

        def actual_inv_string(res, *arg_constrs):
            # Every index i must become (i,) to be a valid element of the index
            # domain. Since args is a flattened list of pairs (index, elem),
            # an index occurs every even argument.
            return arg_constrs

        def actual_inv_in_index_range(dim):
            index_dom = indices_dom.domains[dim - 1]
            inv_included = util_ops.inv_included(index_dom)
            prod_get = product_ops.getter(dim - 1)
            prod_inv_get = product_ops.inv_getter(indices_dom, dim - 1)

            def do(res, index_constr, array_constr):
                rng_constr = array_index_range(array_constr)

                index_constr, rng_dim_constr = inv_included(
                    res, index_constr,
                    prod_get(rng_constr)) or (index_dom.bottom,
                                              index_dom.bottom)

                array_constr = array_inv_index_range(
                    prod_inv_get(rng_dim_constr, rng_constr)
                    or indices_dom.bottom, array_constr)

                if (index_dom.is_empty(index_constr)
                        or array_dom.is_empty(array_constr)):
                    return None

                return index_constr, array_constr

            return do

        @def_provider_builder
        def provider(sig):
            if sig == call_sig:
                return actual_get, actual_inv_get
            elif sig == updated_sig:
                return actual_updated, actual_inv_udpated
            elif (sig.name == ops.STRING and sig.output_domain == array_dom):
                return actual_string, actual_inv_string
            elif (sig.name == ops.IN_VALUES_OF and len(sig.input_domains) == 2
                  and sig.input_domains[1] == array_dom):
                return array_in_values_of, array_inv_in_values_of
            elif (isinstance(sig.name, ops.InRangeName)
                  and len(sig.input_domains) == 2
                  and sig.input_domains[1] == array_dom):
                dim = sig.name.index
                return (actual_in_index_range(dim),
                        actual_inv_in_index_range(dim))

        return TypeInterpretation(array_dom, provider, sparse_array_ops.lit)
Exemple #4
0
    def product_interpreter(elem_interpretations):
        """
        :param list[TypeInterpretation] elem_interpretations:
        :return:
        """
        elem_doms = [interp.domain for interp in elem_interpretations]
        prod_dom = domains.Product(*elem_doms)
        bool_dom = boolean_ops.Boolean

        bin_rel_sig = _signer((prod_dom, prod_dom), bool_dom)

        elem_bin_rel_sigs = [
            _signer((interp.domain, interp.domain), bool_dom)
            for interp in elem_interpretations
        ]

        getter_sig = [_signer((prod_dom, ), e_dom) for e_dom in elem_doms]

        updated_sig = [
            _signer((prod_dom, e_dom), prod_dom) for e_dom in elem_doms
        ]

        def provider_builder(inner_prov):
            """
            Given a definition provider for the components of this product
            (the types of its fields), creates a provider for the whole product
            type. It defines the equal, not equal operators as well as
            accessors for its fields.

            :param DefProvider inner_prov: The provider for this product's
                components.

            :return: A provider for this product type.
            """

            # This provider is composed of several transformers such that:
            # 1. If the definition of the "equal" operator is asked:
            #    a. A first transformer (case_bin_op(ops.EQ)) creates the
            #       signature of the "equal" operators of each field of the
            #       product.
            #    b. A second transformer (prov_lifted) uses the given provider
            #       "inner_prov" to retrieve the definitions of the "equal"
            #       operators of the fields using their signature.
            #    c. A third transformer (bin_eq_provider) uses these
            #       definitions to generate a definition of for the "equal"
            #       operator of this particular product domain.
            #
            # 2. If the definition of the "not equal" operator is asked, the
            #    process is similar.
            #
            # 3. If a definition of a field accessors/updaters is asked, the
            #    case_get_update transformer provides it.

            prov_lifted = inner_prov.lifted()

            def case_bin_op(op):
                @Transformer.as_transformer
                def components_eq_sigs(sig):
                    if sig.name == op and sig == bin_rel_sig(op):
                        return [s(ops.EQ) for s in elem_bin_rel_sigs]

                return components_eq_sigs

            def bin_op_provider_builder(op_impl, inv_op_impl):
                @Transformer.as_transformer
                def bin_op_provider(comp_eqs):
                    eq_defs = [eq_def[0] for eq_def in comp_eqs]
                    eq_inv_defs = [eq_def[1] for eq_def in comp_eqs]

                    return (op_impl(eq_defs),
                            inv_op_impl(prod_dom, eq_inv_defs, eq_defs))

                return bin_op_provider

            @def_provider
            def case_get_update(sig):
                if (isinstance(sig.name, ops.GetName)
                        and sig.name.index < len(getter_sig)
                        and sig == getter_sig[sig.name.index](sig.name)):
                    return (product_ops.getter(sig.name.index),
                            product_ops.inv_getter(prod_dom, sig.name.index))
                elif (isinstance(sig.name, ops.UpdatedName)
                      and sig.name.index < len(updated_sig)
                      and sig == updated_sig[sig.name.index](sig.name)):
                    return (product_ops.updater(sig.name.index),
                            product_ops.inv_updater(prod_dom, sig.name.index))

            bin_eq_provider = bin_op_provider_builder(product_ops.eq,
                                                      product_ops.inv_eq)
            bin_neq_provider = bin_op_provider_builder(product_ops.neq,
                                                       product_ops.inv_neq)

            return ((case_bin_op(ops.EQ) >> prov_lifted >> bin_eq_provider) |
                    (case_bin_op(ops.NEQ) >> prov_lifted >> bin_neq_provider)
                    | case_get_update)

        return TypeInterpretation(prod_dom, provider_builder, product_ops.lit)
Exemple #5
0
def compute_semantics(prog, prog_model, merge_pred_builder, arg_values=None):
    evaluator = ExprEvaluator(prog_model)
    solver = ExprSolver(prog_model)

    # setup widening configuration
    visit_counter = KeyCounter()
    widening_delay = 5
    narrowing_delay = 3

    def do_widen(counter):
        # will widen when counter == widen_delay, then narrow. If it has not
        # converged after narrow_delay is reached, widening is triggered again
        # but without a follow-up narrowing.
        return (counter == widening_delay
                or counter >= narrowing_delay + widening_delay)

    cfg = prog.visit(CFGBuilder())
    roots = cfg.roots()
    non_roots = [n for n in cfg.nodes if n not in roots]

    # find the variables that appear in the program
    var_set = set(n for n in prog_model.keys() if isinstance(n, Variable))

    # build an index
    indexed_vars = {var.data.index: var for var in var_set}
    last_index = max(indexed_vars.keys()) if len(indexed_vars) > 0 else -1

    # define the variables domain
    vars_domain = domains.Product(*(
        prog_model[indexed_vars[i]].domain
        if i in indexed_vars else _unit_domain
        for i in range(last_index + 1)
    ))

    # define the trace domain
    trace_domain = _SimpleTraceLattice(cfg.nodes)

    # define the State domain that we track at each program point.
    lat = domains.Powerset(
        domains.Product(
            trace_domain,
            vars_domain
        ),
        merge_pred_builder.build(
            trace_domain,
            vars_domain
        ),
        None  # We don't need a top element here.
    )

    # the transfer function
    transfer_func = _VarTracker(var_set, vars_domain, evaluator, solver)

    def transfer(new_states, node, inputs):
        transferred = (
            (
                trace,
                node.data.node.visit(transfer_func, values)
                if node.data.node is not None else values
            )
            for trace, values in inputs
        )

        output = lat.build([
            (
                trace_domain.join(trace, trace_domain.build([node])),
                values
            )
            for trace, values in transferred
            if not vars_domain.is_empty(values)
        ])

        if node.data.is_widening_point:
            if do_widen(visit_counter.get_incr(node)):
                output = lat.update(new_states[node], output, True)

        return output

    def it(states):
        new_states = states.copy()

        for node in non_roots:
            new_states[node] = transfer(new_states, node, reduce(
                lat.join,
                (new_states[anc] for anc in cfg.ancestors(node))
            ))

        return new_states

    # initial state of the variables at the entry of the program
    init_vars = tuple(
        arg_values[indexed_vars[i]]
        if (i in indexed_vars and
            arg_values is not None and
            indexed_vars[i] in arg_values)
        else vars_domain.domains[i].top
        for i in range(last_index + 1)
    )

    # initial state at the the entry of the program
    init_lat = lat.build([(trace_domain.bottom, init_vars)])

    # last state of the program (all program points)
    last = concat_dicts(
        {n: transfer({}, n, init_lat) for n in roots},
        {n: lat.bottom for n in non_roots}
    )

    # current state of the program (all program points)
    result = it(last)

    # find a fix-point.
    while any(not lat.eq(x, result[i]) for i, x in last.iteritems()):
        last, result = result, it(result)

    formatted_results = {
        node: {
            trace: {
                v: values[v.data.index] for v in var_set
            }
            for trace, values in state
        }
        for node, state in result.iteritems()
    }

    return AnalysisResults(
        cfg,
        formatted_results,
        trace_domain,
        vars_domain,
        evaluator,
        prog.data.fun_id
    )
Exemple #6
0
        :param irt.Expr expr: The expression to evaluate using the knowledge
            at that specific program point.

        :rtype: dict[frozenset[Digraph.Node], object]
        """
        return {
            trace: self.evaluator.eval(
                expr,
                self._to_state(env)
            )
            for trace, env in self.semantics[node].iteritems()
        }


_unit_domain = domains.Product()


def compute_semantics(prog, prog_model, merge_pred_builder, arg_values=None):
    evaluator = ExprEvaluator(prog_model)
    solver = ExprSolver(prog_model)

    # setup widening configuration
    visit_counter = KeyCounter()
    widening_delay = 5
    narrowing_delay = 3

    def do_widen(counter):
        # will widen when counter == widen_delay, then narrow. If it has not
        # converged after narrow_delay is reached, widening is triggered again
        # but without a follow-up narrowing.
Exemple #7
0
def compute_semantics(prog, prog_model, merge_pred_builder, arg_values=None):
    evaluator = ExprEvaluator(prog_model)
    solver = ExprSolver(prog_model)

    # setup widening configuration
    visit_counter = KeyCounter()
    widening_delay = 5
    narrowing_delay = 3

    def do_widen(counter):
        # will widen when counter == widen_delay, then narrow. If it has not
        # converged after narrow_delay is reached, widening is triggered again
        # but without a follow-up narrowing.
        return (counter == widening_delay
                or counter >= narrowing_delay + widening_delay)

    cfg = prog.visit(CFGBuilder())
    roots = cfg.roots()
    non_roots = [n for n in cfg.nodes if n not in roots]

    # find the variables that appear in the program
    var_set = set(n for n in prog_model.keys() if isinstance(n, Variable))

    # build an index
    indexed_vars = {var.data.index: var for var in var_set}
    last_index = max(indexed_vars.keys()) if len(indexed_vars) > 0 else -1

    # define the variables domain
    vars_domain = domains.Product(*(prog_model[indexed_vars[i]].domain if i in
                                    indexed_vars else _unit_domain
                                    for i in range(last_index + 1)))

    # define the trace domain
    trace_domain = _SimpleTraceLattice(cfg.nodes)

    # define the State domain that we track at each program point.
    lat = domains.Powerset(
        domains.Product(trace_domain, vars_domain),
        merge_pred_builder.build(trace_domain, vars_domain),
        None  # We don't need a top element here.
    )

    # the transfer function
    transfer_func = _VarTracker(var_set, vars_domain, evaluator, solver)

    def transfer(states, node, inputs):
        """
        Applies the transfer function to the given nodes using the abstract
        domain element that represents an over-approximation of the states at
        the program points that precede it.

        Returns a new element of the abstract domain, representing the state
        at this node after the application of the transfer function.

        :param dict[Digraph.Node, object] states: The state at each program
            point.
        :param Digraph.Node node: The node on which to apply the transfer
            function.
        :param object inputs: The element of the abstract domain that
            represents the state of all the predecessors combined.
        :rtype: object
        """

        transferred = ((trace, node.data.node.visit(transfer_func, values)
                        if node.data.node is not None else values)
                       for trace, values in inputs)

        output = lat.build([
            (trace_domain.join(trace, trace_domain.build([node])), values)
            for trace, values in transferred
            if not vars_domain.is_empty(values)
        ])

        if node.data.is_widening_point:
            if do_widen(visit_counter.get_incr(node)):
                output = lat.update(states[node], output, True)

        return output

    def iterate_once(states, ordering):
        """
        Perform one iteration over the system of data-flow equations associated
        with the given subset of nodes.

        This procedure updates "states" in-place.

        :param dict[Digraph.Node, object] states: The state at each program
            point.
        :param Digraph.HierarchicalOrdering ordering: The ordering, describing:
            - The order in which to apply the transfer functions to each node.
            - The subset of program points to consider in this iteration.
        """
        for elem, is_node in ordering:
            if is_node:
                states[elem] = transfer(
                    states, elem,
                    reduce(lat.join,
                           (states[anc] for anc in cfg.ancestors(elem))))
            else:
                fix(states, elem)

    def is_eq(last, current):
        """
        Given two dictionaries describing the state at each program point,
        returns True iff the state at each program point in `last` is equal
        to the state at *those* program points in `current`.

        :param dict[Digraph.Node, object] last: The last state (possibly
            containing less nodes than current).
        :param dict[Digraph.Node, object] current: The current state.
        :rtype: bool
        """
        for n, x in last.iteritems():
            if not lat.eq(x, current[n]):
                return False
        return True

    def sub_states(states, ordering):
        """
        Creates a copy of the given "states" dictionary which contains entries
        only for the nodes that are given by the first level of the ordering
        (i.e. from the current component).

        :type states: dict[Digraph.Node, object]
        :type ordering: Digraph.HierarchicalOrdering
        :rtype: dict[Digraph.Node, object]
        """
        return {elem: states[elem] for elem, is_node in ordering if is_node}

    def fix(states, ordering):
        """
        Solve the data-flow equations on the given subset of nodes of the CFG.
        Finds a fix-point by successive iterations.

        This procedure updates "states" in-place.

        :param dict[Digraph.Node, object] states: The state at each program
            point.
        :param Digraph.HierarchicalOrdering ordering: The ordering, describing:
            - The order in which to apply the transfer functions to each node.
            - The subset of program points for which to find a fix-point.
        """
        last = sub_states(states, ordering)
        iterate_once(states, ordering)

        # loop until the current state is equivalent to the last one.
        while not is_eq(last, states):
            last = sub_states(states, ordering)
            iterate_once(states, ordering)

    # initial state of the variables at the entry of the program
    init_vars = tuple(arg_values[indexed_vars[i]] if (
        i in indexed_vars and arg_values is not None
        and indexed_vars[i] in arg_values) else vars_domain.domains[i].top
                      for i in range(last_index + 1))

    # initial state to use at the entry of the program
    init_lat = lat.build([(trace_domain.bottom, init_vars)])

    # initial state at each program point the program
    states = concat_dicts({n: transfer({}, n, init_lat)
                           for n in roots}, {n: lat.bottom
                                             for n in non_roots})

    # Find a fix-point.
    fix(states, cfg.subgraph(non_roots).flat_topological_ordering())

    formatted_results = {
        node: {
            trace: {v: values[v.data.index]
                    for v in var_set}
            for trace, values in state
        }
        for node, state in states.iteritems()
    }

    return AnalysisResults(cfg, formatted_results, trace_domain, vars_domain,
                           evaluator, prog.data.fun_id)