def should_consider_replacement(target: Exp, target_context: Context, subexp: Exp, subexp_context: Context, subexp_pool: Pool, subexp_fp: Fingerprint, replacement: Exp, replacement_fp: Fingerprint) -> bool: """Heuristic that controls "blind" replacements. Besides replacing subexpressions with improved versions, Cozy also attempts "blind" replacements where the subexpression and the replacement do not behave exactly the same. In some cases this can actually make a huge difference, for instance by replacing a collection with a singleton. However, not all blind replacements are worth trying. This function controls which ones Cozy actually attempts. Preconditions: - subexp and replacement are both legal in (subexp_context, subexp_pool) - subexp and replacement have the same type """ if not is_collection(subexp.type): return No("only collections matter") if not replacement_fp.subset_of(subexp_fp): return No("not a subset") return True
def check_wf(e, ctx, pool): with task("checking well-formedness", size=e.size()): try: exp_wf(e, pool=pool, context=ctx, assumptions=self.assumptions, solver=self.wf_solver) except ExpIsNotWf as exc: return No("at {}: {}".format( pprint(exc.offending_subexpression), exc.reason)) for (sub, sub_ctx, sub_pool) in shred(e, ctx, pool): res = good_idea(self.wf_solver, sub, sub_ctx, sub_pool, assumptions=self.assumptions) if not res: return res if pool == RUNTIME_POOL and self.cost_model.compare( e, self.targets[0], ctx, pool) == Order.GT: # from cozy.cost_model import debug_comparison # debug_comparison(self.cost_model, e, self.target, ctx) return No("too expensive") # if isinstance(e.type, TBag): # c = self.cost_model.cardinality(e) # if all(cc < c for cc in cards): # # print("too big: {}".format(pprint(e))) # return No("too big") return True
def exp_wf_nonrecursive(solver, e: Exp, context: Context, pool=RUNTIME_POOL, assumptions: Exp = ETRUE): """Check the well-formedness of `e` but do not recurse into its children. Returns True or an instance of No explaining why `e` is not well-formed. See `exp_wf` for an explanation of well-formedness and the parameters that this function requires. """ if hasattr(e, "_wf"): return True state_vars = OrderedSet(v for v, p in context.vars() if p == STATE_POOL) args = OrderedSet(v for v, p in context.vars() if p == RUNTIME_POOL) h = extension_handler(type(e)) if h is not None: assumptions = EAll([assumptions, context.path_condition()]) msg = h.check_wf(e, state_vars=state_vars, args=args, pool=pool, assumptions=assumptions, is_valid=solver.valid) if msg is not None: return No(msg) e._wf = True return True at_runtime = pool == RUNTIME_POOL if isinstance(e, EStateVar) and not at_runtime: return No("EStateVar in state pool position") if isinstance(e, EVar): if at_runtime and e in state_vars: return No("state var at runtime") elif not at_runtime and e in args: return No("arg in state exp") e._wf = True return True
def possibly_useful(self, e : Exp, context, pool : Pool, assumptions : Exp, ops : [Op], solver) -> bool: if isinstance(e, EHeapPeek) or isinstance(e, EHeapPeek2): if pool != RUNTIME_POOL: return No("heap peek in state position") return True
def check_wf(e, ctx, pool): with task("pruning", size=e.size()): is_wf = exp_wf(e, pool=pool, context=ctx, solver=wf_solver) if not is_wf: return is_wf res = possibly_useful(wf_solver, e, ctx, pool, ops=ops) if not res: return res if cost_pruning.value and pool == RUNTIME_POOL and cost_model.compare( e, targets[0], ctx, pool) == Order.GT: return No("too expensive") return True
def check_wf(e, ctx, pool): with task("checking well-formedness", size=e.size()): is_wf = exp_wf(e, pool=pool, context=ctx, solver=self.wf_solver) if not is_wf: return is_wf res = good_idea_recursive(self.wf_solver, e, ctx, pool, ops=self.ops) if not res: return res if pool == RUNTIME_POOL and self.cost_model.compare( e, self.targets[0], ctx, pool) == Order.GT: return No("too expensive") return True
def possibly_useful(self, e: Exp, context, pool: Pool, assumptions: Exp, ops: [Op], solver) -> bool: if isinstance(e, ETreeMultisetPeek): if pool != RUNTIME_POOL: return No("ordered peek in state position") return True
def possibly_useful_nonrecursive( solver, e: Exp, context: Context, pool=RUNTIME_POOL, assumptions: Exp = ETRUE, ops: [Op] = ()) -> bool: """Heuristic filter to ignore expressions that are almost certainly useless.""" state_vars = OrderedSet(v for v, p in context.vars() if p == STATE_POOL) args = OrderedSet(v for v, p in context.vars() if p == RUNTIME_POOL) assumptions = EAll([assumptions, context.path_condition()]) at_runtime = pool == RUNTIME_POOL h = extension_handler(type(e)) if h is not None: res = h.possibly_useful(e, context, pool, assumptions, ops, solver) if not res: return res if isinstance(e, EStateVar) and not free_vars(e.e): return No("constant value in state position") if (isinstance(e, EDropFront) or isinstance(e, EDropBack)) and not at_runtime: return No("EDrop* in state position") if not allow_big_sets.value and isinstance(e, EFlatMap) and not at_runtime: return No("EFlatMap in state position") if not allow_int_arithmetic_state.value and not at_runtime and isinstance( e, EBinOp) and e.type == INT: return No("integer arithmetic in state position") if is_collection(e.type) and not is_scalar(e.type.elem_type): return No("collection of nonscalar: e {}\n elem_type: {}\n".format( e, e.type.elem_type)) if isinstance(e.type, TMap) and not is_scalar(e.type.k): return No("bad key type {}".format(pprint(e.type.k))) if isinstance(e.type, TMap) and isinstance(e.type.v, TMap): return No("map to map") # This check is probably a bad idea: whether `the` is legal may depend on # the contex that the expression is embedded within, so we can't skip it # during synthesis just because it looks invalid now. # if isinstance(e, EUnaryOp) and e.op == UOp.The: # len = EUnaryOp(UOp.Length, e.e).with_type(INT) # if not valid(EImplies(assumptions, EBinOp(len, "<=", ENum(1).with_type(INT)).with_type(BOOL))): # return No("illegal application of 'the': could have >1 elems") if not at_runtime and isinstance( e, EBinOp) and e.op == "-" and is_collection(e.type): return No("collection subtraction in state position") # if not at_runtime and isinstance(e, ESingleton): # return No("singleton in state position") if not allow_nonzero_state_constants.value and not at_runtime and isinstance( e, ENum) and e.val != 0: return No("nonzero integer constant in state position") if not allow_binop_state.value and at_runtime and isinstance( e, EStateVar) and isinstance(e.e, EBinOp) and is_scalar( e.e.e1.type) and is_scalar(e.e.e2.type): return No( "constant-time binary operator {!r} in state position".format( e.e.op)) if not allow_conditional_state.value and not at_runtime and isinstance( e, ECond): return No("conditional in state position") if isinstance(e, EMakeMap2) and isinstance(e.e, EEmptyList): return No("trivially empty map") if isinstance(e, EMakeMap2) and isinstance(e.e, ESingleton): return No("really tiny map") if not at_runtime and (isinstance(e, EArgMin) or isinstance(e, EArgMax)): # Cozy has no way to efficiently implement mins/maxes when more than # one element may leave the collection. from cozy.state_maintenance import mutate for op in ops: elems = e.e elems_prime = mutate(elems, op.body) formula = EAll([assumptions] + list(op.assumptions) + [ EGt( ELen( EBinOp(elems, "-", elems_prime).with_type(elems.type)), ONE) ]) if solver.satisfiable(formula): return No( "more than one element might be removed during {}".format( op.name)) if not allow_peels.value and not at_runtime and isinstance(e, EFilter): # catch "peels": removal of zero or one elements if solver.valid( EImplies( assumptions, ELe( ELen( EFilter( e.e, ELambda(e.predicate.arg, ENot( e.predicate.body))).with_type(e.type)), ONE))): return No("filter is a peel") if not allow_big_maps.value and not at_runtime and isinstance( e, EMakeMap2) and is_collection(e.type.v): all_collections = [sv for sv in state_vars if is_collection(sv.type)] total_size = ENum(0).with_type(INT) for c in all_collections: total_size = EBinOp(total_size, "+", EUnaryOp(UOp.Length, c).with_type(INT)).with_type(INT) my_size = EUnaryOp( UOp.Length, EFlatMap( EUnaryOp(UOp.Distinct, e.e).with_type(e.e.type), e.value_function).with_type(e.type.v)).with_type(INT) s = EImplies(assumptions, EBinOp(total_size, ">=", my_size).with_type(BOOL)) if not solver.valid(s): return No("non-polynomial-sized map") return True
def good_idea(solver, e: Exp, context: Context, pool=RUNTIME_POOL, assumptions: Exp = T) -> bool: """Heuristic filter to ignore expressions that are almost certainly useless.""" state_vars = OrderedSet(v for v, p in context.vars() if p == STATE_POOL) args = OrderedSet(v for v, p in context.vars() if p == RUNTIME_POOL) assumptions = EAll([assumptions, context.path_condition()]) at_runtime = pool == RUNTIME_POOL if isinstance(e, EStateVar) and not free_vars(e.e): return No("constant value in state position") if (isinstance(e, EDropFront) or isinstance(e, EDropBack)) and not at_runtime: return No("EDrop* in state position") if not allow_big_sets.value and isinstance(e, EFlatMap) and not at_runtime: return No("EFlatMap in state position") if not allow_int_arithmetic_state.value and not at_runtime and isinstance( e, EBinOp) and e.type == INT: return No("integer arithmetic in state position") if is_collection(e.type) and not is_scalar(e.type.t): return No("collection of nonscalar") if isinstance(e.type, TMap) and not is_scalar(e.type.k): return No("bad key type {}".format(pprint(e.type.k))) if isinstance(e.type, TMap) and isinstance(e.type.v, TMap): return No("map to map") # This check is probably a bad idea: whether `the` is legal may depend on # the contex that the expression is embedded within, so we can't skip it # during synthesis just because it looks invalid now. # if isinstance(e, EUnaryOp) and e.op == UOp.The: # len = EUnaryOp(UOp.Length, e.e).with_type(INT) # if not valid(EImplies(assumptions, EBinOp(len, "<=", ENum(1).with_type(INT)).with_type(BOOL))): # return No("illegal application of 'the': could have >1 elems") if not at_runtime and isinstance( e, EBinOp) and e.op == "-" and is_collection(e.type): return No("collection subtraction in state position") # if not at_runtime and isinstance(e, ESingleton): # return No("singleton in state position") # if not at_runtime and isinstance(e, ENum) and e.val != 0 and e.type == INT: # return No("nonzero integer constant in state position") if at_runtime and isinstance(e, EStateVar) and isinstance( e.e, EBinOp) and is_scalar(e.e.e1.type) and is_scalar(e.e.e2.type): return No("constant-time binary operator in state position") if not allow_conditional_state.value and not at_runtime and isinstance( e, ECond): return No("conditional in state position") if isinstance(e, EMakeMap2) and isinstance(e.e, EEmptyList): return No("trivially empty map") if not allow_peels.value and not at_runtime and isinstance(e, EFilter): # catch "peels": removal of zero or one elements if solver.valid( EImplies( assumptions, ELe( ELen( EFilter(e.e, ELambda(e.p.arg, ENot( e.p.body))).with_type(e.type)), ONE))): return No("filter is a peel") if not allow_big_maps.value and not at_runtime and isinstance( e, EMakeMap2) and is_collection(e.type.v): all_collections = [sv for sv in state_vars if is_collection(sv.type)] total_size = ENum(0).with_type(INT) for c in all_collections: total_size = EBinOp(total_size, "+", EUnaryOp(UOp.Length, c).with_type(INT)).with_type(INT) my_size = EUnaryOp( UOp.Length, EFlatMap(EUnaryOp(UOp.Distinct, e.e).with_type(e.e.type), e.value).with_type(e.type.v)).with_type(INT) s = EImplies(assumptions, EBinOp(total_size, ">=", my_size).with_type(BOOL)) if not solver.valid(s): # from cozy.evaluation import eval # from cozy.solver import satisfy # model = satisfy(EAll([assumptions, EBinOp(total_size, "<", my_size).with_type(BOOL)]), collection_depth=3, validate_model=True) # assert model is not None # return No("non-polynomial-sized map ({}); total_size={}, this_size={}".format(model, eval(total_size, model), eval(my_size, model))) return No("non-polynomial-sized map") return True