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)
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
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)
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)
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 )
: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.
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)