def __init__(self, name, type_params, params, const_params, body, local_decls): super(P4Control, self).__init__(name, params, body) self.local_decls = local_decls self.type_params = type_params self.const_params = const_params self.merged_consts = OrderedDict() self.locals["apply"] = self.apply self.type_context = {}
def set_context(self, p4_state, merged_args, ref_criteria): # we have to subclass because of slight different behavior # inout and out parameters are not undefined they are set # with a new free constant param_buffer = OrderedDict() for param_name, arg in merged_args.items(): # Sometimes expressions are passed, resolve those first arg_expr = p4_state.resolve_expr(arg.p4_val) # for now use the param name, not the arg_name constructed here # FIXME: there are some passes that rename causing issues arg_name = f"{self.name}_{param_name}" # it can happen that we receive a list # infer the type, generate, and set if isinstance(arg_expr, list): # if the type is undefined, do nothing if isinstance(arg.p4_type, P4ComplexType): arg_instance = gen_instance(arg_name, arg.p4_type) arg_instance.set_list(arg_expr) arg_expr = arg_instance if arg.is_ref in ref_criteria: # outs are left-values so the arg must be a string # infer the type value at runtime, param does not work yet # outs reset the input # In the case that the instance is a complex type make sure # to propagate the variable through all its members log.debug("Resetting %s to %s", arg_expr, param_name) if isinstance(arg_expr, P4ComplexInstance): # assume that for inout header validity is not touched if arg.is_ref == "inout": arg_expr.bind_to_name(arg_name) else: arg_expr = arg_expr.p4z3_type.instantiate(arg_name) # we do not know whether the expression is valid afterwards arg_expr.propagate_validity_bit() else: arg_expr = z3.Const(f"{param_name}", arg_expr.sort()) log.debug("Copy-in: %s to %s", arg_expr, param_name) # it is possible to pass an int as value, we need to cast it if isinstance(arg_expr, int) and isinstance( arg.p4_type, (z3.BitVecSortRef, z3.BitVecRef)): arg_expr = z3_cast(arg_expr, arg.p4_type) # buffer the value, do NOT set it yet param_buffer[param_name] = arg_expr # now we can set the arguments without influencing subsequent variables for param_name, param_val in param_buffer.items(): p4_state.set_or_add_var(param_name, param_val)
def save_variables(p4_state, merged_args): var_buffer = OrderedDict() # save all the variables that may be overridden for param_name, arg in merged_args.items(): try: param_val = p4_state.resolve_reference(param_name) var_buffer[param_name] = (arg.mode, arg.p4_val, param_val) except RuntimeError: # if the variable name does not exist, set the value to None var_buffer[param_name] = (arg.mode, arg.p4_val, None) return var_buffer
def __init__(self, name, **properties): super(P4Table, self).__init__(name, params={}) self.keys = [] self.const_entries = [] self.actions = OrderedDict() self.default_action = None self.tbl_action = z3.Int(f"{self.name}_action") self.implementation = None self.locals["hit"] = z3.BoolVal(False) self.locals["miss"] = z3.BoolVal(True) self.locals["action_run"] = self self.locals["apply"] = self.apply # some custom logic to resolve properties self.add_keys(properties) self.add_default(properties) self.add_actions(properties) self.add_const_entries(properties) # set the rest self.properties = properties
def __init__(self, states): self.states = {} for state in states: self.states[state.name] = state self.states["accept"] = AcceptState() self.states["reject"] = RejectState() self.nodes = OrderedDict() self.max_loop = 1 self.terminal_nodes = {} visited_states = set() self.start_node = self.get_parser_dag(visited_states, self.states["start"])
def __init__(self, name, **properties): super(P4Table, self).__init__(name, None, {}) self.keys = [] self.const_entries = [] self.actions = OrderedDict() self.default_action = None self.tbl_action = z3.Int(f"{self.name}_action") self.p4_attrs["hit"] = z3.BoolVal(False) self.p4_attrs["miss"] = z3.BoolVal(True) self.p4_attrs["action_run"] = self self.p4_attrs["apply"] = self.apply self.add_keys(properties) self.add_default(properties) self.add_actions(properties) self.add_const_entries(properties)
class P4Control(P4Callable): def __init__(self, name, z3_reg, params, const_params, body, local_decls): super(P4Control, self).__init__(name, z3_reg, params, body) self.locals = local_decls self.const_params = const_params self.merged_consts = OrderedDict() self.p4_attrs["apply"] = self.apply def init_type_params(self, *args, **kwargs): for idx, c_param in enumerate(self.const_params): c_param.p4_type = args[idx] return self def initialize(self, *args, **kwargs): self.merged_consts = merge_parameters(self.const_params, *args, **kwargs) return self def __call__(self, *args, **kwargs): return self.initialize(*args, **kwargs) def apply(self, p4_state, *args, **kwargs): return self.eval(p4_state, *args, **kwargs) def eval_callable(self, p4_state, merged_args, var_buffer): # initialize the local context of the function for execution p4_context = P4Context(var_buffer) merged_vars = save_variables(p4_state, self.merged_consts) p4_context.prepend_to_buffer(merged_vars) self.set_context(p4_state, merged_args, ("out")) for const_param_name, const_arg in self.merged_consts.items(): const_val = const_arg.p4_val const_val = p4_state.resolve_expr(const_val) p4_state.set_or_add_var(const_param_name, const_val) # execute the action expression with the new environment p4_state.insert_exprs(p4_context) p4_state.insert_exprs(self.statements) p4_state.insert_exprs(self.locals) p4z3_expr = p4_state.pop_next_expr() return p4z3_expr.eval(p4_state)
def copy_in(self, p4_state, merged_args): param_buffer = OrderedDict() for param_name, arg in merged_args.items(): # Sometimes expressions are passed, resolve those first arg_expr = p4_state.resolve_expr(arg.p4_val) # for now use the param name, not the arg_name constructed here # FIXME: there are some passes that rename causing issues arg_name = "undefined" # f"{self.name}_{param_name}" # it can happen that we receive a list # infer the type, generate, and set p4_type = resolve_type(p4_state, arg.p4_type) if isinstance(arg_expr, list): # if the type is undefined, do nothing if isinstance(p4_type, P4ComplexType): arg_instance = gen_instance(p4_state, arg_name, p4_type) arg_instance.set_list(arg_expr) arg_expr = arg_instance # it is possible to pass an int as value, we need to cast it elif isinstance(arg_expr, int): arg_expr = z3_cast(arg_expr, p4_type) # need to work with an independent copy # the purpose is to handle indirect assignments in an action if arg.mode in ("in", "inout") and isinstance( arg_expr, StructInstance): arg_expr = copy.copy(arg_expr) if arg.mode == "out": # outs are left-values so the arg must be a string # infer the type value at runtime, param does not work yet # outs reset the input # In the case that the instance is a complex type make sure # to propagate the variable through all its members log.debug("Resetting %s to %s", arg_expr, param_name) arg_expr = gen_instance(p4_state, arg_name, p4_type) log.debug("Copy-in: %s to %s", arg_expr, param_name) # buffer the value, do NOT set it yet param_buffer[param_name] = arg_expr return param_buffer
class P4Table(P4Callable): def __init__(self, name, **properties): super(P4Table, self).__init__(name, params={}) self.keys = [] self.const_entries = [] self.actions = OrderedDict() self.default_action = None self.tbl_action = z3.Int(f"{self.name}_action") self.implementation = None self.locals["hit"] = z3.BoolVal(False) self.locals["miss"] = z3.BoolVal(True) self.locals["action_run"] = self self.locals["apply"] = self.apply # some custom logic to resolve properties self.add_keys(properties) self.add_default(properties) self.add_actions(properties) self.add_const_entries(properties) # set the rest self.properties = properties def add_actions(self, properties): if "actions" not in properties: return for action_expr in properties["actions"]: action_name, action_args = resolve_action(action_expr) index = len(self.actions) + 1 self.actions[action_name] = (index, action_name, action_args) def add_default(self, properties): if "default_action" not in properties: # In case there is no default action, the first action is default self.default_action = (0, "NoAction", ()) else: def_action = properties["default_action"] action_name, action_args = resolve_action(def_action) self.default_action = (0, action_name, action_args) def add_keys(self, properties): if "key" not in properties: return for table_key in properties["key"]: self.keys.append(table_key) def add_const_entries(self, properties): if "entries" not in properties: return for entry in properties["entries"]: const_keys = entry[0] action_expr = entry[1] if len(self.keys) != len(const_keys): raise RuntimeError("Constant keys must match table keys!") action_name, action_args = resolve_action(action_expr) self.const_entries.append((const_keys, (action_name, action_args))) def apply(self, p4_state): self.eval(p4_state) return self def eval_keys(self, p4_state): key_pairs = [] if not self.keys: # there is nothing to match with... return z3.BoolVal(False) for index, (key_expr, key_type) in enumerate(self.keys): key_eval = p4_state.resolve_expr(key_expr) key_sort = key_eval.sort() key_match = z3.Const(f"{self.name}_table_key_{index}", key_sort) if key_type == "exact": # Just a simple comparison, nothing special key_pairs.append(key_eval == key_match) elif key_type == "lpm": # I think this can be arbitrarily large... # If the shift exceeds the bit width, everything will be zero # but that does not matter # TODO: Test this? mask_var = z3.BitVec(f"{self.name}_table_mask_{index}", key_sort) lpm_mask = z3.BitVecVal(2**key_sort.size() - 1, key_sort) << mask_var match = (key_eval & lpm_mask) == (key_match & lpm_mask) key_pairs.append(match) elif key_type == "ternary": # Just apply a symbolic mask, any zero bit is a wildcard # TODO: Test this? mask = z3.Const(f"{self.name}_table_mask_{index}", key_sort) # this is dumb... if isinstance(key_sort, z3.BoolSortRef): match = z3.And(key_eval, mask) == z3.And(key_match, mask) else: match = (key_eval & mask) == (key_match & mask) key_pairs.append(match) elif key_type == "range": # Pick an arbitrary minimum and maximum within the bit range # the minimum must be strictly lesser than the max # I do not think a match is needed? # TODO: Test this? min_key = z3.Const(f"{self.name}_table_min_{index}", key_sort) max_key = z3.Const(f"{self.name}_table_max_{index}", key_sort) match = z3.And(z3.ULE(min_key, key_eval), z3.UGE(max_key, key_eval)) key_pairs.append(z3.And(match, z3.ULT(min_key, max_key))) elif key_type == "optional": # As far as I understand this is just a wildcard for control # plane purposes. Semantically, there is no point? # TODO: Test this? key_pairs.append(z3.BoolVal(True)) elif key_type == "selector": # Selectors are a deep rabbit hole # This rabbit hole does not yet make sense to me # FIXME: Implement # will intentionally fail if no implementation is present # impl = self.properties["implementation"] # impl_extern = self.p4_state.resolve_reference(impl) key_pairs.append(z3.BoolVal(True)) else: # weird key, might be some specific specification raise RuntimeError(f"Key type {key_type} not supported!") return z3.And(key_pairs) def eval_action(self, p4_state, action_name, action_args): p4_action = p4_state.resolve_reference(action_name) if not isinstance(p4_action, P4Action): raise TypeError(f"Expected a P4Action got {type(p4_action)}!") merged_action_args = [] action_args_len = len(action_args) - 1 for idx, param in enumerate(p4_action.params): if idx > action_args_len: # this is a ctrl argument, generate an input ctrl_arg = gen_instance(p4_state, f"{self.name}{param.name}", param.p4_type) merged_action_args.append(ctrl_arg) else: merged_action_args.append(action_args[idx]) return p4_action(p4_state, *merged_action_args) def eval_default(self, p4_state): _, action_name, action_args = self.default_action log.debug("Evaluating default action...") return self.eval_action(p4_state, action_name, action_args) def eval_const_entries(self, p4_state, action_exprs, action_matches): context = p4_state.current_context() forward_cond_copy = context.tmp_forward_cond for c_keys, (action_name, action_args) in reversed(self.const_entries): matches = [] # match the constant keys with the normal table keys # this generates the match expression for a specific constant entry # this is a little inefficient, fix. # TODO: Figure out if key type matters here? for index, (key_expr, key_type) in enumerate(self.keys): c_key_expr = c_keys[index] # default implies don't care, do not add # TODO: Verify that this assumption is right... if isinstance(c_key_expr, DefaultExpression): continue key_eval = p4_state.resolve_expr(key_expr) if isinstance(c_key_expr, P4Range): x = c_key_expr.min y = c_key_expr.max c_key_eval = z3.And(z3.ULE(x, key_eval), z3.UGE(y, key_eval)) matches.append(c_key_eval) elif isinstance(c_key_expr, P4Mask): val = p4_state.resolve_expr(c_key_expr.mask) mask = c_key_expr.mask c_key_eval = (val & mask) == (key_eval & mask) matches.append(c_key_eval) else: c_key_eval = p4_state.resolve_expr(c_key_expr) matches.append(key_eval == c_key_eval) action_match = z3.And(*matches) log.debug("Evaluating constant action %s...", action_name) # state forks here var_store, contexts = p4_state.checkpoint() cond = z3.And(self.locals["hit"], action_match) context.tmp_forward_cond = z3.And(forward_cond_copy, cond) self.eval_action(p4_state, action_name, action_args) if not p4_state.has_exited: action_exprs.append((cond, p4_state.get_attrs())) p4_state.has_exited = False action_matches.append(action_match) p4_state.restore(var_store, contexts) def eval_table_entries(self, p4_state, action_exprs, action_matches): context = p4_state.current_context() forward_cond_copy = context.tmp_forward_cond for act_id, act_name, act_args in reversed(self.actions.values()): action_match = (self.tbl_action == z3.IntVal(act_id)) log.debug("Evaluating action %s...", act_name) # state forks here var_store, contexts = p4_state.checkpoint() cond = z3.And(self.locals["hit"], action_match) context.tmp_forward_cond = z3.And(forward_cond_copy, cond) self.eval_action(p4_state, act_name, act_args) if not p4_state.has_exited: action_exprs.append((cond, p4_state.get_attrs())) p4_state.has_exited = False action_matches.append(action_match) p4_state.restore(var_store, contexts) def eval_table(self, p4_state): action_exprs = [] action_matches = [] context = p4_state.current_context() forward_cond_copy = context.tmp_forward_cond # only bother to evaluate if the table can actually hit if not self.locals["hit"] == z3.BoolVal(False): # note: the action lists are pass by reference here # first evaluate all the constant entries self.eval_const_entries(p4_state, action_exprs, action_matches) # then append dynamic table entries to the constant entries self.eval_table_entries(p4_state, action_exprs, action_matches) # finally start evaluating the default entry var_store, contexts = p4_state.checkpoint() # this hits when the table is either missed, or no action matches cond = z3.Or(self.locals["miss"], z3.Not(z3.Or(*action_matches))) context.tmp_forward_cond = z3.And(forward_cond_copy, cond) self.eval_default(p4_state) if p4_state.has_exited: p4_state.restore(var_store, contexts) p4_state.has_exited = False context.tmp_forward_cond = forward_cond_copy # generate a nested set of if expressions per available action for cond, then_vars in action_exprs: merge_attrs(p4_state, cond, then_vars) def eval_callable(self, p4_state, merged_args, var_buffer): # tables are a little bit special since they also have attributes # so what we do here is first initialize the key hit = self.eval_keys(p4_state) self.locals["hit"] = z3.simplify(hit) self.locals["miss"] = z3.Not(hit) # then execute the table as the next expression in the chain self.eval_table(p4_state)
class P4Control(P4Callable): def __init__(self, name, type_params, params, const_params, body, local_decls): super(P4Control, self).__init__(name, params, body) self.local_decls = local_decls self.type_params = type_params self.const_params = const_params self.merged_consts = OrderedDict() self.locals["apply"] = self.apply self.type_context = {} def bind_to_ctrl_type(self, context, ctrl_type): # TODO Figure out what to actually do here # FIXME: A hack to deal with lack of input params if len(ctrl_type.params) < len(self.type_params): return self init_ctrl = copy.copy(self) # the type params sometimes include the return type also # it is typically the first value, but is bound somewhere else for idx, t_param in enumerate(init_ctrl.type_params): sub_type = resolve_type(context, ctrl_type.params[idx].p4_type) init_ctrl.type_context[t_param] = sub_type for param_idx, param in enumerate(init_ctrl.params): if isinstance(param.p4_type, str) and param.p4_type == t_param: init_ctrl.params[param_idx] = ctrl_type.params[idx] return init_ctrl def init_type_params(self, context, *args, **kwargs): # TODO Figure out what to actually do here init_ctrl = copy.copy(self) # the type params sometimes include the return type also # it is typically the first value, but is bound somewhere else for idx, t_param in enumerate(init_ctrl.type_params): arg = resolve_type(context, args[idx]) init_ctrl.type_context[t_param] = arg return init_ctrl def initialize(self, context, *args, **kwargs): ctrl_copy = copy.copy(self) ctrl_copy.merged_consts = merge_parameters(ctrl_copy.const_params, *args, **kwargs) # also bind types, because for reasons you can bind types everywhere... for idx, const_param in enumerate(ctrl_copy.const_params): # this means the type is generic p4_type = resolve_type(context, const_param.p4_type) if p4_type is None: # grab the type of the input arguments ctrl_copy.type_context[const_param.p4_type] = args[idx].sort() return ctrl_copy def apply(self, p4_state, *args, **kwargs): local_context = {} for type_name, p4_type in self.type_context.items(): local_context[type_name] = resolve_type(p4_state, p4_type) p4_state.type_contexts.append(self.type_context) self.eval(p4_state, *args, **kwargs) p4_state.type_contexts.pop() def eval_callable(self, p4_state, merged_args, var_buffer): # initialize the local context of the function for execution context = p4_state.current_context() merged_vars = save_variables(p4_state, self.merged_consts) context.prepend_to_buffer(merged_vars) for const_param_name, const_arg in self.merged_consts.items(): const_val = const_arg.p4_val const_val = p4_state.resolve_expr(const_val) p4_state.set_or_add_var(const_param_name, const_val) for expr in self.local_decls: expr.eval(p4_state) if p4_state.has_exited or context.has_returned: break self.statements.eval(p4_state) def __copy__(self): cls = self.__class__ result = cls.__new__(cls) result.__dict__.update(self.__dict__) result.type_params = copy.copy(self.type_params) result.params = [] for param in self.params: result.params.append(copy.copy(param)) result.const_params = [] for param in self.const_params: result.const_params.append(copy.copy(param)) result.locals = {} result.locals["apply"] = result.apply return result
def __init__(self, z3_reg, name, params, type_params): super(P4Package, self).__init__(name, params) self.pipes = OrderedDict() self.z3_reg = z3_reg self.type_params = type_params self.type_context = {}
def __init__(self, name, z3_reg, params, const_params, body, local_decls): super(P4Control, self).__init__(name, z3_reg, params, body) self.locals = local_decls self.const_params = const_params self.merged_consts = OrderedDict() self.p4_attrs["apply"] = self.apply
def __init__(self, z3_reg, name, params): self.pipes = OrderedDict() self.params = params self.name = name self.z3_reg = z3_reg