From c0f605cc1b339c65f05f3eb9d792254f87fc203e Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 5 Sep 2017 15:00:41 +0200 Subject: [PATCH 01/31] Added call slot annotations to nagini_contracts --- src/nagini_contracts/contracts.py | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index a4168626d..1a0cd52ab 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -254,7 +254,7 @@ def ContractOnly(func: T) -> T: """ return func - + def GhostReturns(start_index: int) -> Callable[[T], T]: """ Decorator for functions which specifies which return values are ghost @@ -271,6 +271,44 @@ def wrap(func: T) -> T: return wrap +def CallSlot(call_slot: Callable[..., None]) -> Callable[..., Any]: + """ + Decorator to mark a method as a call slot declaration. + """ + + def call_slot_handler(*args, **kwargs) -> Any: + + def uq_handler(*args, **kwargs) -> None: + pass + + return uq_handler + + return call_slot_handler + + +def UniversallyQuantified(uq: Callable[..., None]) -> None: + """ + Decorator to mark a method as introducing universally quantified + variables inside a call slot. + """ + pass + + +def CallSlotProof(call_slot: Callable[..., Any]) -> Callable[[Callable[..., None]], None]: + """ + Decorator to mark a method as a proof for a call slot. + """ + pass + + +def ClosureCall(Any) -> Any: + """ + Justifies a closure call by proving static dispatch (in contrast to using + a call slot). + """ + pass + + def list_pred(l: List[T]) -> bool: """ Special, predefined predicate that represents the permissions belonging From 8b1512136bdb9f4dd22257d3982dad5916c85622 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Wed, 6 Sep 2017 16:18:08 +0200 Subject: [PATCH 02/31] Started modifying the Analyzer for call slots First attempt through an external 'CallSlotAnalyzer'. Might not work so well, might need to modify the Analyzer directly. --- src/nagini_translation/analyzer.py | 10 ++- src/nagini_translation/call_slot_analyzers.py | 67 +++++++++++++++++++ src/nagini_translation/lib/program_nodes.py | 18 +++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/nagini_translation/call_slot_analyzers.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 8db74a745..4149a15be 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -52,6 +52,7 @@ ) from nagini_translation.lib.views import PythonModuleView from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from nagini_translation import call_slot_analyzers logger = logging.getLogger('nagini_translation.analyzer') @@ -453,6 +454,10 @@ def _is_illegal_magic_method_name(self, name: str) -> bool: return False def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + if call_slot_analyzers.is_call_slot(node): + call_slot_analyzers.CallSlotAnalyzer(self).analyze(node) + # FIXME: store the call slot somewhere (current module?) + return if self.current_function: raise UnsupportedException(node, 'nested function declaration') name = node.name @@ -1159,7 +1164,10 @@ def visit_Try(self, node: ast.Try) -> None: def _incompatible_decorators(self, decorators) -> bool: return ((('Predicate' in decorators) and ('Pure' in decorators)) or (('IOOperation' in decorators) and (len(decorators) != 1)) or - (('property' in decorators) and (len(decorators) != 1))) + (('property' in decorators) and (len(decorators) != 1)) or + (('CallSlot' in decorators) and (len(decorators) != 1)) or + (('UniversallyQuantified' in decorators) and (len(decorators) != 1)) or + (('CallSlotProof' in decorators) and (len(decorators) != 1))) def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: """ diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py new file mode 100644 index 000000000..9bb447aec --- /dev/null +++ b/src/nagini_translation/call_slot_analyzers.py @@ -0,0 +1,67 @@ +import ast +from nagini_translation import analyzer +from nagini_translation.lib.program_nodes import CallSlot + + +class CallSlotAnalyzer(ast.NodeVisitor): + + def __init__(self, analyzer: 'analyzer.Analyzer') -> None: + self.analyzer = analyzer + self.call_slot = None # type: CallSlot + + def analyze(self, node: ast.FunctionDef) -> None: + """ + Preprocess the call slot `node'. + """ + assert is_call_slot(node) + + self.call_slot = CallSlot(node) + self.__collect_normal_variables() + + def __collect_normal_variables(self) -> None: + assert self.call_slot + + for arg in self.call_slot.node.args.args: + self.__add_normal_variable(arg) + + def __add_normal_variable(self, arg: ast.arg) -> None: + # FIXME: do we need to considere type variables here? + arg_type = self.analyzer.typeof(arg) + + arg_var = self.analyzer.node_factory.create_python_var(arg.arg, arg, arg_type) + arg_var.alt_types = self.get_alt_types(arg) + + self.call_slot.normal_variables[arg.arg] = arg_var + + + +def is_call_slot(node: ast.FunctionDef) -> bool: + """ + Whether node is a call slot declaration. + """ + return _has_single_decorator(node, 'CallSlot') + + +def is_universally_quantified(node: ast.FunctionDef) -> bool: + """ + Whether a function introduces universally quantified variables + """ + return _has_single_decorator(node, 'UniversallyQuantified') + + +def is_call_slot_proof(node: ast.FunctionDef) -> bool: + """ + Whether a function introduces universally quantified variables + """ + return _has_single_decorator(node, 'CallSlotProof') + + +def _has_single_decorator(node: ast.FunctionDef, decorator: str) -> bool: + """ + Whether `node' has only one decorator that equals to `decorator' + """ + # NOTE: could be refactored out into a nagini 'util' package + return ( + len(node.decorator_list) == 1 and + node.decorator_list[0].id == decorator + ) diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 7b6a741b7..0bc20749a 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1564,3 +1564,21 @@ def create_python_class(self, name: str, superscope: PythonScope, interface=False): return PythonClass(name, superscope, node_factory, node, superclass, interface) + + +class CallSlot(PythonScope, ContainerInterface): + + def __init__(self, node: ast.FunctionDef) -> None: + self.node = node + self.name = node.name + # TODO: more precise types + self.precondition = [] # type: List + self.postcondition = [] # type: List + self.normal_variables = {} # type: Dict[str, PythonVar] + # universally quantified variables + self.uq_variables = None # type: List[PythonVar] + # TODO: add call & return values + + def get_contents(self, only_top: bool) -> Dict: + # TODO: implement + return {} From a94c5a485a4b9258dea24fb90e5bf04d8cb8e5de Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Wed, 6 Sep 2017 19:23:16 +0200 Subject: [PATCH 03/31] Progressed with call slot analyzer --- src/nagini_translation/call_slot_analyzers.py | 38 +++++++++++++-- src/nagini_translation/lib/program_nodes.py | 48 +++++++++++++------ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 9bb447aec..1d2d4ccb3 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -1,11 +1,15 @@ import ast -from nagini_translation import analyzer +from nagini_translation import analyzer as analzyer_pkg from nagini_translation.lib.program_nodes import CallSlot +from nagini_translation.lib.util import ( + UnsupportedException, + InvalidProgramException, +) class CallSlotAnalyzer(ast.NodeVisitor): - def __init__(self, analyzer: 'analyzer.Analyzer') -> None: + def __init__(self, analyzer: 'analyzer_pkg.Analyzer') -> None: self.analyzer = analyzer self.call_slot = None # type: CallSlot @@ -15,8 +19,33 @@ def analyze(self, node: ast.FunctionDef) -> None: """ assert is_call_slot(node) - self.call_slot = CallSlot(node) - self.__collect_normal_variables() + analyzer = self.analyzer + scope = analyzer.module + + if analyzer.current_function: + raise UnsupportedException(node, 'nested call slots') + if analyzer.current_class: + raise UnsupportedException(node, 'call slot as class member') + if analyzer._is_illegal_magic_method_name(node.name): + raise InvalidProgramException(node, 'illegal.magic.method') + + analyzer.define_new(scope, node.name, node) + + self.call_slot = analyzer.node_factory.crreate_call_slot( + node.name, + node, + scope, + analyzer.node_factory + ) + + scope.methods[node.name] = self.call_slot + + # TODO: set current_func (restore old current_func at the end) + # TODO: visit args + # self.__collect_normal_variables() + # TODO: set call_slot.type + # TODO: visit call slot body + # TODO: other call slot preprocessing/checks def __collect_normal_variables(self) -> None: assert self.call_slot @@ -29,6 +58,7 @@ def __add_normal_variable(self, arg: ast.arg) -> None: arg_type = self.analyzer.typeof(arg) arg_var = self.analyzer.node_factory.create_python_var(arg.arg, arg, arg_type) + # FIXME: has no get_alt_types arg_var.alt_types = self.get_alt_types(arg) self.call_slot.normal_variables[arg.arg] = arg_var diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 0bc20749a..d477810f1 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1547,6 +1547,20 @@ def create_python_method( container_factory, interface, interface_dict, method_type) + def create_call_slot( + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + container_factory: 'ProgramNodeFactory' + ) -> 'CallSlot': + return CallSlot( + name, + node, + superscope, + container_factory + ) + def create_python_io_operation(self, name: str, node: ast.AST, superscope: PythonScope, container_factory: 'ProgramNodeFactory', @@ -1566,19 +1580,25 @@ def create_python_class(self, name: str, superscope: PythonScope, superclass, interface) -class CallSlot(PythonScope, ContainerInterface): +class CallSlot(PythonMethod): - def __init__(self, node: ast.FunctionDef) -> None: - self.node = node - self.name = node.name - # TODO: more precise types - self.precondition = [] # type: List - self.postcondition = [] # type: List - self.normal_variables = {} # type: Dict[str, PythonVar] - # universally quantified variables - self.uq_variables = None # type: List[PythonVar] - # TODO: add call & return values + def __init__( + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + node_factory: 'ProgramNodeFactory', + ) -> None: - def get_contents(self, only_top: bool) -> Dict: - # TODO: implement - return {} + PythonMethod.__init__( + self, + name, + node, + None, # cls: PythonClass + superscope, + False, # pure: bool + False, # contract_only: bool + node_factory # node_factory: 'ProgramNodeFactory' + ) + # universally quantified variables + self.uq_variables = OrderedDict() # type: dict[str, PythonVar] From d4097052076c1b9621ab6b5b01b917090bc2be45 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 26 Sep 2017 17:11:35 +0200 Subject: [PATCH 04/31] Updated contracts for new ClosureCall syntax and __all__ bugfix --- src/nagini_contracts/contracts.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 1a0cd52ab..568ad324f 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -301,7 +301,7 @@ def CallSlotProof(call_slot: Callable[..., Any]) -> Callable[[Callable[..., None pass -def ClosureCall(Any) -> Any: +def ClosureCall(call: Any, justification: Any) -> Any: """ Justifies a closure call by proving static dispatch (in contrast to using a call slot). @@ -366,6 +366,10 @@ def dict_pred(d: Dict[T, V]) -> bool: 'set_pred', 'Sequence', 'ToSeq', + 'CallSlot', + 'UniversallyQuantified', + 'CallSlotProof', + 'ClosureCall', 'MaySet', 'MayCreate', ] From 6b0c5290193016f155c89e85cc4e90cab6461d4c Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 26 Sep 2017 17:14:11 +0200 Subject: [PATCH 05/31] Enabled Callable types in TypeVisitor --- src/nagini_translation/lib/typeinfo.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 6223d40e1..85f86e4ae 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -99,9 +99,8 @@ def visit_name_expr(self, node: mypy.nodes.NameExpr): break if (node.name not in LITERALS and not is_alias): name_type = self.type_of(node) - if not isinstance(name_type, mypy.types.CallableType): - self.set_type(self.prefix + [node.name], name_type, - node.line, col(node)) + self.set_type(self.prefix + [node.name], name_type, + node.line, col(node)) def visit_star_expr(self, node: mypy.nodes.StarExpr): node.expr.accept(self) From ff68f771deb65ef0bfe67fcd4119b290ee5eaebc Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 26 Sep 2017 18:02:10 +0200 Subject: [PATCH 06/31] Progressed with analyzation stage of call slots --- src/nagini_translation/analyzer.py | 12 +- src/nagini_translation/call_slot_analyzers.py | 243 +++++++++++++++--- src/nagini_translation/lib/program_nodes.py | 6 +- 3 files changed, 228 insertions(+), 33 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 4149a15be..72e4acb10 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -72,6 +72,7 @@ def __init__(self, types: TypeInfo, path: str, selected: Set[str]): self.global_module, sil_names=self.global_module.sil_names) self.current_class = None + self.outer_functions = [] # type: List[PythonMethod] self.current_function = None self.current_scopes = [] self.contract_only = False @@ -111,7 +112,9 @@ def define_new(self, container: Union[PythonModule, PythonClass], name in container.methods or name in container.predicates or (isinstance(container, PythonClass) and - name in container.static_methods)): + name in container.static_methods) or + (isinstance(container, PythonModule) and + name in container.call_slots)): raise InvalidProgramException(node, 'multiple.definitions') def collect_imports(self, abs_path: str) -> None: @@ -456,7 +459,9 @@ def _is_illegal_magic_method_name(self, name: str) -> bool: def visit_FunctionDef(self, node: ast.FunctionDef) -> None: if call_slot_analyzers.is_call_slot(node): call_slot_analyzers.CallSlotAnalyzer(self).analyze(node) - # FIXME: store the call slot somewhere (current module?) + return + if call_slot_analyzers.is_call_slot_proof(node): + call_slot_analyzers.CallSlotProofAnalyzer(self).analyze(node) return if self.current_function: raise UnsupportedException(node, 'nested function declaration') @@ -1014,6 +1019,7 @@ def get_alt_types(self, node: ast.AST) -> Dict[int, PythonType]: context = [] if self.current_class is not None: context.append(self.current_class.name) + context.extend(map(lambda method: method.name, self.outer_functions)) if self.current_function is not None: context.append(self.current_function.name) name = node.id if isinstance(node, ast.Name) else node.arg @@ -1038,6 +1044,7 @@ def typeof(self, node: ast.AST) -> PythonType: context = [] if self.current_class is not None: context.append(self.current_class.name) + context.extend(map(lambda method: method.name, self.outer_functions)) if self.current_function is not None: context.append(self.current_function.name) context.extend(self.current_scopes) @@ -1066,6 +1073,7 @@ def typeof(self, node: ast.AST) -> PythonType: context = [] if self.current_class is not None: context.append(self.current_class.name) + context.extend(map(lambda method: method.name, self.outer_functions)) context.append(self.current_function.name) context.extend(self.current_scopes) type, _ = self.module.get_type(context, node.arg) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 1d2d4ccb3..59f56cfa1 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -1,13 +1,18 @@ import ast -from nagini_translation import analyzer as analzyer_pkg -from nagini_translation.lib.program_nodes import CallSlot +from typing import Union +from nagini_translation import analyzer as analyzer_pkg +from nagini_translation.lib.program_nodes import ( + PythonModule, + PythonMethod, + CallSlot +) from nagini_translation.lib.util import ( UnsupportedException, InvalidProgramException, ) -class CallSlotAnalyzer(ast.NodeVisitor): +class CallSlotAnalyzer: def __init__(self, analyzer: 'analyzer_pkg.Analyzer') -> None: self.analyzer = analyzer @@ -21,48 +26,210 @@ def analyze(self, node: ast.FunctionDef) -> None: analyzer = self.analyzer scope = analyzer.module + assert isinstance(scope, PythonModule) - if analyzer.current_function: + if analyzer.current_function is not None: raise UnsupportedException(node, 'nested call slots') - if analyzer.current_class: + if analyzer.current_class is not None: raise UnsupportedException(node, 'call slot as class member') - if analyzer._is_illegal_magic_method_name(node.name): - raise InvalidProgramException(node, 'illegal.magic.method') analyzer.define_new(scope, node.name, node) - self.call_slot = analyzer.node_factory.crreate_call_slot( + self.call_slot = analyzer.node_factory.create_call_slot( node.name, node, scope, analyzer.node_factory ) - scope.methods[node.name] = self.call_slot + scope.call_slots[node.name] = self.call_slot + + analyzer.current_function = self.call_slot + _check_method_declaration(self.call_slot, analyzer) + + body_node = node + + has_uq_vars = _is_uq_vars(node.body) + if has_uq_vars: + body_node = node.body[0] + mock_call_slot = analyzer.node_factory.create_call_slot( + body_node.name, + body_node, + self.call_slot, + analyzer.node_factory + ) + + analyzer.outer_functions.append(self.call_slot) + analyzer.current_function = mock_call_slot + + _check_method_declaration(mock_call_slot, analyzer) + self.call_slot.uq_variables = mock_call_slot.args + # TODO: Check uq vars (shadowing of normal vars?) + body_node._parent = node + + for child in body_node.body: + analyzer.visit(child, body_node) - # TODO: set current_func (restore old current_func at the end) - # TODO: visit args - # self.__collect_normal_variables() - # TODO: set call_slot.type - # TODO: visit call slot body + # TODO: pure call slots # TODO: other call slot preprocessing/checks + # - body consists only of + # - Preconditions (done) + # - Postconditions (done) + # - One single call declaration + # - check 'well-formedness' of call + # - gather return values (save in CallSlot?) + # - Only variables from call slot are used (no globals) + # - don't want a call slot with a call to a global closure + + self._check_body(body_node.body) + + # cleanup + if has_uq_vars: + self.call_slot.precondition = mock_call_slot.precondition + self.call_slot.postcondition = mock_call_slot.postcondition + analyzer.outer_functions.pop() + analyzer.current_function = None + + def _check_body(self, body): + self.has_call = False + for child in body: + + if isinstance(child, ast.Expr) and isinstance(child.value, ast.Call): + if is_precondition(child.value) or is_postcondition(child.value): + continue + self._check_call_declaration(child.value) + + elif isinstance(child, ast.Assign): + self._check_call_declaration(child) + + else: + raise InvalidProgramException( + child, + 'call_slots.body.invalid_stmt', + 'Callslot declarations must only consist of contracts and a single call' + ) + + if not self.has_call: + raise InvalidProgramException( + self.call_slot.node, + 'call_slots.no.call', + "Callslot '%s' doesn't declare a call" % self.call_slot.node.name + ) + + def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: + + if isinstance(node, ast.Assign): + + if isinstance(node.value, ast.Call): + call = node.value + else: + raise InvalidProgramException( + node, + 'call_slots.body.invalid_stmt', + 'Callslot declarations must only consist of contracts and a single call' + ) + + if len(node.targets) > 1: + raise UnsupportedException( + node, + "Callslot's call can't have more than one return target" + ) + + assert len(node.targets) == 1 + + if isinstance(node.targets[0], ast.Name): + self.call_slot.return_variables = [node.targets[0]] + elif isinstance(node.targets[0], ast.Tuple): + # FIXME: doesn't work with nested destructuring + # e.g., `a, (b, c), d = f(1, 2, 3)` + self.call_slot.return_variables = node.targets[0].elts + else: + raise UnsupportedException( + node, + "Callslot's call has an unsupported return target" + ) + else: + call = node + + self._check_call(call) + + def _check_call(self, node: ast.Call) -> None: + if self.has_call: + raise InvalidProgramException( + node, + 'call_slots.multiple.calls', + "Callslot '%s' declares more than one call" % self.call_slot.node.name + ) + self.has_call = True + self.call_slot.call = node + +# FIXME: check call slot application + + +class CallSlotProofAnalyzer: + + def __init__(self, analyzer: 'analyzer_pkg.Analyzer') -> None: + self.analyzer = analyzer + + def analyze(self, node: ast.FunctionDef) -> None: + """ + Preprocess the call slot `node'. + """ + pass # FIXME: implement + - def __collect_normal_variables(self) -> None: - assert self.call_slot +def _check_method_declaration(method: PythonMethod, analyzer: 'analyzer_pkg.Analyzer') -> None: + """ + Checks whether `node' is a method declaration valid for a call slot or + universally quantified variables. If not raises an appropriate + exception. Expects analyzer.{current_function, outer_functions} to be + set correctly. + + * No magic name ('__...__') + * Return type = None + * No *args + * No **kwargs + """ - for arg in self.call_slot.node.args.args: - self.__add_normal_variable(arg) + if analyzer._is_illegal_magic_method_name(method.node.name): + raise InvalidProgramException(method.node, 'illegal.magic.method') - def __add_normal_variable(self, arg: ast.arg) -> None: - # FIXME: do we need to considere type variables here? - arg_type = self.analyzer.typeof(arg) + method.type = analyzer.convert_type( + analyzer.module.get_func_type(method.scope_prefix)) - arg_var = self.analyzer.node_factory.create_python_var(arg.arg, arg, arg_type) - # FIXME: has no get_alt_types - arg_var.alt_types = self.get_alt_types(arg) + if method.type is not None: + raise InvalidProgramException( + method.node, + 'call_slots.return.not_none', + "Method '%s' doesn't return 'None'" % method.node.name + ) - self.call_slot.normal_variables[arg.arg] = arg_var + analyzer.visit(method.node.args, method.node) + if method.var_arg is not None: + raise InvalidProgramException( + method.node, + 'call_slots.parameters.var_args', + ("Method '%s' contains illegal variadic parameters" + % method.node.name) + ) + + if method.kw_arg is not None: + raise InvalidProgramException( + method.node, + 'call_slots.parameters.kw_args', + ("Method '%s' contains illegal keyword parameters" + % method.node.name) + ) + # TODO: what about defaults? + + +def _is_uq_vars(body) -> bool: + return ( + len(body) == 1 and + isinstance(body[0], ast.FunctionDef) and + is_universally_quantified(body[0]) + ) def is_call_slot(node: ast.FunctionDef) -> bool: @@ -86,12 +253,28 @@ def is_call_slot_proof(node: ast.FunctionDef) -> bool: return _has_single_decorator(node, 'CallSlotProof') -def _has_single_decorator(node: ast.FunctionDef, decorator: str) -> bool: +def _has_single_decorator(node: ast.FunctionDef, decorator_name: str) -> bool: """ Whether `node' has only one decorator that equals to `decorator' """ # NOTE: could be refactored out into a nagini 'util' package - return ( - len(node.decorator_list) == 1 and - node.decorator_list[0].id == decorator - ) + if len(node.decorator_list) != 1: + return False + decorator = node.decorator_list[0] + + if isinstance(decorator, ast.Name): + return decorator.id == decorator_name + + if isinstance(decorator, ast.Call): + return decorator.func.id == decorator_name + + # FIXME: should probably raise instead + return False + + +def is_precondition(call: ast.Call) -> bool: + return call.func.id == 'Requires' + + +def is_postcondition(call: ast.Call) -> bool: + return call.func.id == 'Ensures' diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index d477810f1..0b47251b3 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -119,6 +119,7 @@ def __init__(self, types: TypeInfo, self.classes = OrderedDict() self.functions = OrderedDict() self.methods = OrderedDict() + self.call_slots = OrderedDict() # type: Dict[str, CallSlot] self.predicates = OrderedDict() self.io_operations = OrderedDict() self.global_vars = OrderedDict() @@ -1601,4 +1602,7 @@ def __init__( node_factory # node_factory: 'ProgramNodeFactory' ) # universally quantified variables - self.uq_variables = OrderedDict() # type: dict[str, PythonVar] + self.uq_variables = OrderedDict() # type: Dict[str, PythonVar] + + self.call = None # type: ast.Call + self.return_variables = None # type: List[ast.Name] From edf787ff5fe3f2d4dcc3c5214427fd18c9b77867 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 26 Sep 2017 18:03:16 +0200 Subject: [PATCH 07/31] Added 'closures' test suite to test framework --- src/nagini_translation/conftest.py | 8 +++++++- src/nagini_translation/lib/config.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/conftest.py b/src/nagini_translation/conftest.py index e2dea6343..f5f0ea23a 100644 --- a/src/nagini_translation/conftest.py +++ b/src/nagini_translation/conftest.py @@ -20,6 +20,7 @@ _SIF_TESTS_DIR = 'tests/sif/' _IO_TESTS_DIR = 'tests/io/' _OBLIGATIONS_TESTS_DIR = 'tests/obligations/' +_CLOSURES_TESTS_DIR = 'tests/closures/' class PyTestConfig: @@ -49,6 +50,8 @@ def add_test(self, test: str): self._add_test_dir(_IO_TESTS_DIR) elif test == 'obligations': self._add_test_dir(_OBLIGATIONS_TESTS_DIR) + elif test == 'closures': + self._add_test_dir(_CLOSURES_TESTS_DIR) else: print('Unrecognized test set.') @@ -108,6 +111,7 @@ def pytest_addoption(parser: 'pytest.config.Parser'): parser.addoption('--sif', dest='sif', action='store_true') parser.addoption('--io', dest='io', action='store_true') parser.addoption('--obligations', dest='obligations', action='store_true') + parser.addoption('--closures', dest='closures', action='store_true') parser.addoption('--all-verifiers', dest='all_verifiers', action='store_true') parser.addoption('--silicon', dest='silicon', action='store_true') @@ -119,7 +123,7 @@ def pytest_configure(config: 'pytest.config.Config'): # Setup tests. tests = [] if config.option.all_tests: - tests = ['functional', 'sif', 'io', 'obligations'] + tests = ['functional', 'sif', 'io', 'obligations', 'closures'] else: if config.option.functional: tests.append('functional') @@ -129,6 +133,8 @@ def pytest_configure(config: 'pytest.config.Config'): tests.append('io') if config.option.obligations: tests.append('obligations') + if config.option.closures: + tests.append('closures') if tests: # Overwrite config file options. _pytest_config.clear_tests() diff --git a/src/nagini_translation/lib/config.py b/src/nagini_translation/lib/config.py index 15fc7b5ca..d86f57832 100644 --- a/src/nagini_translation/lib/config.py +++ b/src/nagini_translation/lib/config.py @@ -119,7 +119,7 @@ def __init__(self, config) -> None: tests_value = self._info.get('tests') if not tests_value: - self.tests = ['functional', 'sif', 'io', 'obligations'] + self.tests = ['functional', 'sif', 'io', 'obligations', 'closures'] else: self.tests = tests_value.strip().split() From 816de825f1044b2af012c341dcab0910ae5db636 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 26 Sep 2017 18:03:41 +0200 Subject: [PATCH 08/31] Added method_name_collision test --- .../translation/test_method_name_collision.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/closures/translation/test_method_name_collision.py diff --git a/tests/closures/translation/test_method_name_collision.py b/tests/closures/translation/test_method_name_collision.py new file mode 100644 index 000000000..5de56dcb2 --- /dev/null +++ b/tests/closures/translation/test_method_name_collision.py @@ -0,0 +1,11 @@ +from typing import Callable +from nagini_contracts.contracts import * + + +def some_name() -> None: + pass + +#:: ExpectedOutput(type.error:Name 'some_name' already defined) +@CallSlot +def some_name(f: Callable[[int], None]) -> None: + f(2) From fde4d7269059bf8eb417ccdd995cebde0f69e958 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 30 Sep 2017 18:03:38 +0200 Subject: [PATCH 09/31] Changes from PR --- src/nagini_contracts/contracts.py | 5 +- src/nagini_translation/analyzer.py | 17 ++++-- src/nagini_translation/call_slot_analyzers.py | 52 +++++++++++++++---- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 568ad324f..117d3582a 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -303,8 +303,9 @@ def CallSlotProof(call_slot: Callable[..., Any]) -> Callable[[Callable[..., None def ClosureCall(call: Any, justification: Any) -> Any: """ - Justifies a closure call by proving static dispatch (in contrast to using - a call slot). + Justifies a closure call through either + * a CallSlot (justification == the callslot instance) + * proofing static dispatch (justification == the static method) """ pass diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 72e4acb10..8e73c1ef8 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -52,7 +52,12 @@ ) from nagini_translation.lib.views import PythonModuleView from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union -from nagini_translation import call_slot_analyzers +from nagini_translation.call_slot_analyzers import ( + CallSlotAnalyzer, + is_call_slot, + CallSlotProofAnalyzer, + is_call_slot_proof +) logger = logging.getLogger('nagini_translation.analyzer') @@ -86,6 +91,8 @@ def __init__(self, types: TypeInfo, path: str, selected: Set[str]): self._aliases = {} # Dict[str, PythonBaseVar] self.current_loop_invariant = None self.selected = selected + self.call_slot_analyzer = CallSlotAnalyzer(self) + self.call_slot_proof_analyzer = CallSlotProofAnalyzer(self) @property def node_factory(self): @@ -457,11 +464,11 @@ def _is_illegal_magic_method_name(self, name: str) -> bool: return False def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - if call_slot_analyzers.is_call_slot(node): - call_slot_analyzers.CallSlotAnalyzer(self).analyze(node) + if is_call_slot(node): + self.call_slot_analyzer.analyze(node) return - if call_slot_analyzers.is_call_slot_proof(node): - call_slot_analyzers.CallSlotProofAnalyzer(self).analyze(node) + if is_call_slot_proof(node): + self.call_slot_proof_analyzer.analyze(node) return if self.current_function: raise UnsupportedException(node, 'nested function declaration') diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 59f56cfa1..90175dbc8 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -1,10 +1,8 @@ import ast -from typing import Union -from nagini_translation import analyzer as analyzer_pkg +from typing import Union, List from nagini_translation.lib.program_nodes import ( PythonModule, - PythonMethod, - CallSlot + PythonMethod ) from nagini_translation.lib.util import ( UnsupportedException, @@ -14,7 +12,7 @@ class CallSlotAnalyzer: - def __init__(self, analyzer: 'analyzer_pkg.Analyzer') -> None: + def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer self.call_slot = None # type: CallSlot @@ -90,7 +88,7 @@ def analyze(self, node: ast.FunctionDef) -> None: analyzer.outer_functions.pop() analyzer.current_function = None - def _check_body(self, body): + def _check_body(self, body: List[ast.stmt]): self.has_call = False for child in body: @@ -140,8 +138,17 @@ def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: if isinstance(node.targets[0], ast.Name): self.call_slot.return_variables = [node.targets[0]] elif isinstance(node.targets[0], ast.Tuple): - # FIXME: doesn't work with nested destructuring + + # NOTE: could add support for nested destructuring # e.g., `a, (b, c), d = f(1, 2, 3)` + # currently we only support simple tuple assignments + for target in node.targets[0].elts: + if not isinstance(target, ast.Name): + raise UnsupportedException( + target, + "Callslots only support simple tuple assignments" + ) + self.call_slot.return_variables = node.targets[0].elts else: raise UnsupportedException( @@ -168,7 +175,7 @@ def _check_call(self, node: ast.Call) -> None: class CallSlotProofAnalyzer: - def __init__(self, analyzer: 'analyzer_pkg.Analyzer') -> None: + def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer def analyze(self, node: ast.FunctionDef) -> None: @@ -178,7 +185,26 @@ def analyze(self, node: ast.FunctionDef) -> None: pass # FIXME: implement -def _check_method_declaration(method: PythonMethod, analyzer: 'analyzer_pkg.Analyzer') -> None: +class _LimitedVariablesChecker(ast.NodeVisitor): + + def __init__(self, variables: List[str]) -> None: + self.variables = variables + self.offending_nodes = [] # type: List + + def check_name(self, name: str) -> None: + if name.id not in self.variables: + self.offending_nodes.append(name) + + def visit_Name(self, name: ast.Name) -> None: + self.check_name(name.id) + self.generic_visit(name) + + # TODO: check other node types + # ast.Arg for lambdas + # ast.Call for calls (?) + + +def _check_method_declaration(method: PythonMethod, analyzer: 'Analyzer') -> None: """ Checks whether `node' is a method declaration valid for a call slot or universally quantified variables. If not raises an appropriate @@ -204,6 +230,13 @@ def _check_method_declaration(method: PythonMethod, analyzer: 'analyzer_pkg.Anal "Method '%s' doesn't return 'None'" % method.node.name ) + if 0 < len(method.node.args.defaults): + raise InvalidProgramException( + method.node.args.defaults[0], + 'call_slots.parameters.default', + "Method '%s' has a default parameter" % method.node.name + ) + analyzer.visit(method.node.args, method.node) if method.var_arg is not None: @@ -221,7 +254,6 @@ def _check_method_declaration(method: PythonMethod, analyzer: 'analyzer_pkg.Anal ("Method '%s' contains illegal keyword parameters" % method.node.name) ) - # TODO: what about defaults? def _is_uq_vars(body) -> bool: From 2f5523685fc3c53ba94ed0063137a55923176a6b Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 30 Sep 2017 20:56:51 +0200 Subject: [PATCH 10/31] Patch to make debugging with pdb better --- src/nagini_translation/lib/jvmaccess.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/lib/jvmaccess.py b/src/nagini_translation/lib/jvmaccess.py index d6e288142..481089edb 100644 --- a/src/nagini_translation/lib/jvmaccess.py +++ b/src/nagini_translation/lib/jvmaccess.py @@ -7,8 +7,9 @@ class JVM: """ def __init__(self, classpath: str): - jpype.startJVM(jpype.getDefaultJVMPath(), - '-Djava.class.path=' + classpath, '-Xss128m') + if not jpype.isJVMStarted(): + jpype.startJVM(jpype.getDefaultJVMPath(), + '-Djava.class.path=' + classpath, '-Xss128m') self.java = jpype.JPackage('java') self.scala = jpype.JPackage('scala') self.viper = jpype.JPackage('viper') From c90cdceb36468d9dce747c976cd58c0b24365788 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Mon, 2 Oct 2017 22:37:42 +0200 Subject: [PATCH 11/31] Progressed with CallSlotAnalyzer --- src/nagini_contracts/contracts.py | 3 +- src/nagini_translation/call_slot_analyzers.py | 132 +++++++++++++++--- src/nagini_translation/lib/program_nodes.py | 2 +- 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 117d3582a..5ba8db18d 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -20,7 +20,8 @@ CONTRACT_FUNCS = ['Assume', 'Assert', 'Old', 'Result', 'Implies', 'Forall', 'Exists', 'Low', 'Acc', 'Rd', 'Fold', 'Unfold', 'Unfolding', 'Previous', 'RaisedException', 'Sequence', 'ToSeq', 'MaySet', - 'MayCreate',] + 'MayCreate', 'CallSlot', 'CallSlotProof', + 'UniversallyQuantified', 'ClosureCall', ] T = TypeVar('T') V = TypeVar('V') diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 90175dbc8..df837f446 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -1,8 +1,13 @@ import ast -from typing import Union, List +from typing import Union, List, Dict, Set +from nagini_contracts.contracts import ( + CONTRACT_WRAPPER_FUNCS, + CONTRACT_FUNCS +) from nagini_translation.lib.program_nodes import ( PythonModule, - PythonMethod + PythonMethod, + PythonVar ) from nagini_translation.lib.util import ( UnsupportedException, @@ -15,11 +20,19 @@ class CallSlotAnalyzer: def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer self.call_slot = None # type: CallSlot + self.allowed_variables_checker = _AllowedVariablesChecker() def analyze(self, node: ast.FunctionDef) -> None: """ Preprocess the call slot `node'. """ + + # FIXME: refactor this method + # preferably we can refactor it so that we can have a base class + # for both the CallSlotAnalyzer and CallSlotProofAnalyzer + # Basically, the CallSlotProofAnalyzer has to do almost the same as + # the CallSlotAnalyzer, except that more statements are allowed + # in its body assert is_call_slot(node) analyzer = self.analyzer @@ -27,9 +40,17 @@ def analyze(self, node: ast.FunctionDef) -> None: assert isinstance(scope, PythonModule) if analyzer.current_function is not None: - raise UnsupportedException(node, 'nested call slots') + raise InvalidProgramException( + node, + 'call_slots.nested.declaration', + "Callslot '%s' occurs inside a method" % node.name + ) if analyzer.current_class is not None: - raise UnsupportedException(node, 'call slot as class member') + raise InvalidProgramException( + node, + 'call_slots.nested.declaration', + "Callslot '%s' occurs inside a class" % node.name + ) analyzer.define_new(scope, node.name, node) @@ -62,9 +83,16 @@ def analyze(self, node: ast.FunctionDef) -> None: _check_method_declaration(mock_call_slot, analyzer) self.call_slot.uq_variables = mock_call_slot.args - # TODO: Check uq vars (shadowing of normal vars?) + body_node._parent = node + # FIXME: disallow shadowing through return variables? + _check_variables( + self.call_slot.args, + self.call_slot.uq_variables, + ILLEGAL_VARIABLE_NAMES + ) + for child in body_node.body: analyzer.visit(child, body_node) @@ -76,11 +104,24 @@ def analyze(self, node: ast.FunctionDef) -> None: # - One single call declaration # - check 'well-formedness' of call # - gather return values (save in CallSlot?) - # - Only variables from call slot are used (no globals) - # - don't want a call slot with a call to a global closure self._check_body(body_node.body) + valid_varible_uses = ( + self.call_slot.args.keys() | + self.call_slot.uq_variables.keys() | + ILLEGAL_VARIABLE_NAMES | + set(map(lambda name: name.id, self.call_slot.return_variables)) + ) + self.allowed_variables_checker.reset(valid_varible_uses) + illegal_variable_uses = self.allowed_variables_checker.check(node) + if 0 < len(illegal_variable_uses): + raise InvalidProgramException( + illegal_variable_uses[0], + 'call_slot.names.non_local', + "Illegal reference to non-local name '%s'" % illegal_variable_uses[0].id + ) + # cleanup if has_uq_vars: self.call_slot.precondition = mock_call_slot.precondition @@ -118,14 +159,13 @@ def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: if isinstance(node, ast.Assign): - if isinstance(node.value, ast.Call): - call = node.value - else: + if not isinstance(node.value, ast.Call): raise InvalidProgramException( node, 'call_slots.body.invalid_stmt', 'Callslot declarations must only consist of contracts and a single call' ) + call = node.value if len(node.targets) > 1: raise UnsupportedException( @@ -170,11 +210,14 @@ def _check_call(self, node: ast.Call) -> None: self.has_call = True self.call_slot.call = node + # FIXME: check call slot application class CallSlotProofAnalyzer: + # NOTE: we can probably reuse a lot from CallSlotAnalyzer + def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer @@ -185,23 +228,66 @@ def analyze(self, node: ast.FunctionDef) -> None: pass # FIXME: implement -class _LimitedVariablesChecker(ast.NodeVisitor): +ILLEGAL_VARIABLE_NAMES = set(CONTRACT_FUNCS + CONTRACT_WRAPPER_FUNCS) - def __init__(self, variables: List[str]) -> None: - self.variables = variables - self.offending_nodes = [] # type: List - def check_name(self, name: str) -> None: - if name.id not in self.variables: - self.offending_nodes.append(name) +def _check_variables( + normal_variables: Dict[str, PythonVar], + uq_variables: Dict[str, PythonVar], + illegal_variable_names: Set[str] +) -> None: + + shadowed_variables = normal_variables.keys() & uq_variables.keys() + + if 0 < len(shadowed_variables): + shadowed_variable_name = next(iter(shadowed_variables)) + raise InvalidProgramException( + uq_variables[shadowed_variable_name].node, + "call_slots.parameters.illegal_shadowing", + "UQ variable '%s' illegally shadows an outer variable" % shadowed_variable_name + ) + + all_variable_names = normal_variables.keys() | uq_variables.keys() + illegal_variable_names = all_variable_names & illegal_variable_names + + if 0 < len(illegal_variable_names): + illegal_variable_name = next(iter(illegal_variable_names)) + + illegal_variable = ( + normal_variables[illegal_variable_name] if + illegal_variable_name in normal_variables else + uq_variables[illegal_variable_name] + ) + raise InvalidProgramException( + illegal_variable.node, + "call_slots.parameters.illegal_name", + "Variable '%s' has an illegal name" % illegal_variable_name + ) + + +class _AllowedVariablesChecker(ast.NodeVisitor): + + # NOTE: CallSlotProofAnalyzer will require adjustments: + # In callslot proofs there can be nested call slot proofs which introduce + # new valid variables. + + def __init__(self, allowed_variables: Set[str] = set()) -> None: + self.reset(allowed_variables) + + def reset(self, allowed_variables: Set[str]) -> None: + self.offending_nodes = [] # type: List[ast.Name] + self.allowed_variables = allowed_variables + + def check(self, node: ast.AST) -> List[ast.Name]: + self.visit(node) + return self.offending_nodes def visit_Name(self, name: ast.Name) -> None: - self.check_name(name.id) - self.generic_visit(name) + if name.id not in self.allowed_variables: + self.offending_nodes.append(name) - # TODO: check other node types - # ast.Arg for lambdas - # ast.Call for calls (?) + def visit_arg(self, arg: ast.arg) -> None: + return # ignore annotations def _check_method_declaration(method: PythonMethod, analyzer: 'Analyzer') -> None: @@ -217,6 +303,8 @@ def _check_method_declaration(method: PythonMethod, analyzer: 'Analyzer') -> Non * No **kwargs """ + # NOTE: needs tests + if analyzer._is_illegal_magic_method_name(method.node.name): raise InvalidProgramException(method.node, 'illegal.magic.method') diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 0b47251b3..71d8429ae 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1605,4 +1605,4 @@ def __init__( self.uq_variables = OrderedDict() # type: Dict[str, PythonVar] self.call = None # type: ast.Call - self.return_variables = None # type: List[ast.Name] + self.return_variables = [] # type: List[ast.Name] From e9ae8b48b431092fe3b18a1f1b96e4db06b19312 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Mon, 2 Oct 2017 22:41:56 +0200 Subject: [PATCH 12/31] Added more tests --- .../test_call_slot_arg_contract_shadowing.py | 8 ++++++++ .../test_call_slot_global_method.py | 12 ++++++++++++ .../translation/test_call_slot_global_var.py | 9 +++++++++ .../test_call_slot_illegal_assing.py | 19 +++++++++++++++++++ .../translation/test_call_slot_magic_name.py | 8 ++++++++ ...> test_call_slot_method_name_collision.py} | 2 +- .../test_call_slot_nested_class.py | 10 ++++++++++ .../test_call_slot_nested_method.py | 10 ++++++++++ .../translation/test_call_slot_simple.py | 17 +++++++++++++++++ .../test_call_slot_simple_no_uq.py | 13 +++++++++++++ .../test_call_slot_simple_with_tuple.py | 17 +++++++++++++++++ .../test_call_slot_uq_shadowing.py | 15 +++++++++++++++ 12 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 tests/closures/translation/test_call_slot_arg_contract_shadowing.py create mode 100644 tests/closures/translation/test_call_slot_global_method.py create mode 100644 tests/closures/translation/test_call_slot_global_var.py create mode 100644 tests/closures/translation/test_call_slot_illegal_assing.py create mode 100644 tests/closures/translation/test_call_slot_magic_name.py rename tests/closures/translation/{test_method_name_collision.py => test_call_slot_method_name_collision.py} (80%) create mode 100644 tests/closures/translation/test_call_slot_nested_class.py create mode 100644 tests/closures/translation/test_call_slot_nested_method.py create mode 100644 tests/closures/translation/test_call_slot_simple.py create mode 100644 tests/closures/translation/test_call_slot_simple_no_uq.py create mode 100644 tests/closures/translation/test_call_slot_simple_with_tuple.py create mode 100644 tests/closures/translation/test_call_slot_uq_shadowing.py diff --git a/tests/closures/translation/test_call_slot_arg_contract_shadowing.py b/tests/closures/translation/test_call_slot_arg_contract_shadowing.py new file mode 100644 index 000000000..76f2f04a0 --- /dev/null +++ b/tests/closures/translation/test_call_slot_arg_contract_shadowing.py @@ -0,0 +1,8 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + + +@CallSlot +#:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_name) +def call_slot(Acc: Callable[[int], None]) -> None: + Acc(2) diff --git a/tests/closures/translation/test_call_slot_global_method.py b/tests/closures/translation/test_call_slot_global_method.py new file mode 100644 index 000000000..c38b786dc --- /dev/null +++ b/tests/closures/translation/test_call_slot_global_method.py @@ -0,0 +1,12 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + + +def some_gloval_method() -> None: + pass + + +@CallSlot +def some_call_slot(f: Callable[[Callable[[], None]], None]) -> None: + #:: ExpectedOutput(invalid.program:call_slot.names.non_local) + f(some_gloval_method) diff --git a/tests/closures/translation/test_call_slot_global_var.py b/tests/closures/translation/test_call_slot_global_var.py new file mode 100644 index 000000000..8326d0ea0 --- /dev/null +++ b/tests/closures/translation/test_call_slot_global_var.py @@ -0,0 +1,9 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + +some_gloval_var = 5 # type: int + +@CallSlot +def some_call_slot(f: Callable[[int], None]) -> None: + #:: ExpectedOutput(invalid.program:call_slot.names.non_local) + f(some_gloval_var) diff --git a/tests/closures/translation/test_call_slot_illegal_assing.py b/tests/closures/translation/test_call_slot_illegal_assing.py new file mode 100644 index 000000000..c935fbb0d --- /dev/null +++ b/tests/closures/translation/test_call_slot_illegal_assing.py @@ -0,0 +1,19 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + #:: ExpectedOutput(invalid.program:call_slots.body.invalid_stmt) + x += 2 + Ensures(z == x + y) diff --git a/tests/closures/translation/test_call_slot_magic_name.py b/tests/closures/translation/test_call_slot_magic_name.py new file mode 100644 index 000000000..bcde7834f --- /dev/null +++ b/tests/closures/translation/test_call_slot_magic_name.py @@ -0,0 +1,8 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + + +#:: ExpectedOutput(invalid.program:illegal.magic.method) +@CallSlot +def __call_slot__(f: Callable[[int], None]) -> None: + f(2) diff --git a/tests/closures/translation/test_method_name_collision.py b/tests/closures/translation/test_call_slot_method_name_collision.py similarity index 80% rename from tests/closures/translation/test_method_name_collision.py rename to tests/closures/translation/test_call_slot_method_name_collision.py index 5de56dcb2..dd4edd4d6 100644 --- a/tests/closures/translation/test_method_name_collision.py +++ b/tests/closures/translation/test_call_slot_method_name_collision.py @@ -1,5 +1,5 @@ from typing import Callable -from nagini_contracts.contracts import * +from nagini_contracts.contracts import CallSlot def some_name() -> None: diff --git a/tests/closures/translation/test_call_slot_nested_class.py b/tests/closures/translation/test_call_slot_nested_class.py new file mode 100644 index 000000000..6e925ef92 --- /dev/null +++ b/tests/closures/translation/test_call_slot_nested_class.py @@ -0,0 +1,10 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + + +class class_with_call_slot_inside: + + #:: ExpectedOutput(invalid.program:call_slots.nested.declaration) + @CallSlot + def call_slot(self, f: Callable[[int], None]) -> None: + f(2) diff --git a/tests/closures/translation/test_call_slot_nested_method.py b/tests/closures/translation/test_call_slot_nested_method.py new file mode 100644 index 000000000..a3212fe89 --- /dev/null +++ b/tests/closures/translation/test_call_slot_nested_method.py @@ -0,0 +1,10 @@ +from typing import Callable +from nagini_contracts.contracts import CallSlot + + +def method_with_call_slot_inside() -> None: + + #:: ExpectedOutput(invalid.program:call_slots.nested.declaration) + @CallSlot + def call_slot(f: Callable[[int], None]) -> None: + f(2) diff --git a/tests/closures/translation/test_call_slot_simple.py b/tests/closures/translation/test_call_slot_simple.py new file mode 100644 index 000000000..c0c639837 --- /dev/null +++ b/tests/closures/translation/test_call_slot_simple.py @@ -0,0 +1,17 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) diff --git a/tests/closures/translation/test_call_slot_simple_no_uq.py b/tests/closures/translation/test_call_slot_simple_no_uq.py new file mode 100644 index 000000000..a50236ff4 --- /dev/null +++ b/tests/closures/translation/test_call_slot_simple_no_uq.py @@ -0,0 +1,13 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + Requires, + Ensures +) + + +@CallSlot +def call_slot(f: Callable[[int], int], x: int) -> None: + Requires(x >= 0) + z = f(x) + Ensures(z >= x) diff --git a/tests/closures/translation/test_call_slot_simple_with_tuple.py b/tests/closures/translation/test_call_slot_simple_with_tuple.py new file mode 100644 index 000000000..7aebbc471 --- /dev/null +++ b/tests/closures/translation/test_call_slot_simple_with_tuple.py @@ -0,0 +1,17 @@ +from typing import Callable, Tuple +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures +) + + +@CallSlot +def call_slot(f: Callable[[int, int], Tuple[int, int, int, int]], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + a, b, c, z = f(x, y) + Ensures(z == x + y and b == c and a <= 0) diff --git a/tests/closures/translation/test_call_slot_uq_shadowing.py b/tests/closures/translation/test_call_slot_uq_shadowing.py new file mode 100644 index 000000000..081724d29 --- /dev/null +++ b/tests/closures/translation/test_call_slot_uq_shadowing.py @@ -0,0 +1,15 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified +) + + +@CallSlot +def some_slot(f: Callable[[int], None], x: int) -> None: + + @UniversallyQuantified + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_shadowing) + def uq(x: int) -> None: + f(x) + From 273d9266e693dd1fa920d492dd5592e0e71570e7 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Wed, 11 Oct 2017 11:36:46 +0200 Subject: [PATCH 13/31] Added CallSlotProofAnalyzer & check_closure_call Progressed with CallSlotAnalyzer --- src/nagini_translation/analyzer.py | 19 +- src/nagini_translation/call_slot_analyzers.py | 679 ++++++++++++------ src/nagini_translation/lib/program_nodes.py | 73 +- 3 files changed, 551 insertions(+), 220 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 8e73c1ef8..2d790f6a1 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -56,7 +56,9 @@ CallSlotAnalyzer, is_call_slot, CallSlotProofAnalyzer, - is_call_slot_proof + is_call_slot_proof, + is_closure_call, + check_closure_call ) @@ -731,6 +733,8 @@ def visit_Call(self, node: ast.Call) -> None: Collects preconditions, postconditions, raised exceptions and invariants. """ + if is_closure_call(node): + check_closure_call(node) if (isinstance(node.func, ast.Name) and node.func.id in CONTRACT_WRAPPER_FUNCS): if not self.current_function or self.current_function.predicate: @@ -843,8 +847,15 @@ def visit_Name(self, node: ast.Name) -> None: raise UnsupportedException(assign, msg) var.value = assign.value self.module.global_vars[node.id] = var - var = self.module.global_vars[node.id] - self.track_access(node, var) + if node.id in self.module.global_vars: + var = self.module.global_vars[node.id] + self.track_access(node, var) + elif not node.id in self.module.methods: + # Node is neither a global variable nor a global method + raise UnsupportedException( + node, + "Unsupported reference '%s'" % node.id + ) else: # Node is a static field. if isinstance(node.ctx, ast.Load): @@ -868,7 +879,7 @@ def visit_Name(self, node: ast.Name) -> None: # again now that we now it's actually static. del self.current_class.fields[node.id] return - if not isinstance(self.get_target(node, self.module), PythonGlobalVar): + if not isinstance(self.get_target(node, self.module), (PythonGlobalVar, PythonMethod)): # Node is a local variable, lambda argument, or a global variable # that hasn't been encountered yet var = None diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index df837f446..68db28fb9 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -1,13 +1,11 @@ import ast -from typing import Union, List, Dict, Set +from typing import Union, List, Set from nagini_contracts.contracts import ( CONTRACT_WRAPPER_FUNCS, CONTRACT_FUNCS ) from nagini_translation.lib.program_nodes import ( - PythonModule, - PythonMethod, - PythonVar + PythonModule ) from nagini_translation.lib.util import ( UnsupportedException, @@ -15,106 +13,193 @@ ) -class CallSlotAnalyzer: +class _CallSlotBaseAnalyzer: + + # TODO: pure call slots + + __ILLEGAL_VARIABLE_NAMES = set(CONTRACT_FUNCS + CONTRACT_WRAPPER_FUNCS) def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer - self.call_slot = None # type: CallSlot - self.allowed_variables_checker = _AllowedVariablesChecker() + self.call_slot = None # type: CallSlotBase + self.allowed_variables_checker = _CallSlotBaseAnalyzer._AllowedVariablesChecker() def analyze(self, node: ast.FunctionDef) -> None: """ Preprocess the call slot `node'. """ - # FIXME: refactor this method - # preferably we can refactor it so that we can have a base class - # for both the CallSlotAnalyzer and CallSlotProofAnalyzer - # Basically, the CallSlotProofAnalyzer has to do almost the same as - # the CallSlotAnalyzer, except that more statements are allowed - # in its body - assert is_call_slot(node) + old_current_function = self.analyzer.current_function + if old_current_function is not None: + self.analyzer.outer_functions.append(old_current_function) + self._pre_process(node) + + self.analyzer.current_function = self.call_slot + self._check_method_declaration(self.call_slot) + + body_node = node + + has_uq_vars = _is_uq_vars(node.body) + if has_uq_vars: + body_node = node.body[0] + mock_call_slot = self.analyzer.node_factory.create_call_slot( + body_node.name, + body_node, + self.call_slot, + self.analyzer.node_factory + ) + + self.analyzer.outer_functions.append(self.call_slot) + self.analyzer.current_function = mock_call_slot + + self._check_method_declaration(mock_call_slot) + self.call_slot.uq_variables = mock_call_slot.args + + body_node._parent = node + + for child in body_node.body: + self.analyzer.visit(child, body_node) + + self._check_body(body_node.body) + + self._check_variables() + + # cleanup + if has_uq_vars: + self.call_slot.precondition = mock_call_slot.precondition + self.call_slot.postcondition = mock_call_slot.postcondition + self.analyzer.outer_functions.pop() + self.analyzer.current_function = old_current_function + if old_current_function is not None: + self.analyzer.outer_functions.pop() + + def _check_method_declaration(self, call_slot: 'CallSlotBase') -> None: + """ + Checks whether `node' is a method declaration valid for a call slot or + universally quantified variables. If not raises an appropriate + exception. Expects analyzer.{current_function, outer_functions} to be + set correctly. + + * No magic name ('__...__') + * Return type = None + * No *args + * No **kwargs + """ analyzer = self.analyzer - scope = analyzer.module - assert isinstance(scope, PythonModule) - if analyzer.current_function is not None: + if analyzer._is_illegal_magic_method_name(call_slot.node.name): + raise InvalidProgramException(call_slot.node, 'illegal.magic.method') + + call_slot.type = analyzer.convert_type( + analyzer.module.get_func_type(call_slot.scope_prefix)) + + if call_slot.type is not None: raise InvalidProgramException( - node, - 'call_slots.nested.declaration', - "Callslot '%s' occurs inside a method" % node.name + call_slot.node, + 'call_slots.return.not_none', + "Method '%s' doesn't return 'None'" % call_slot.node.name ) - if analyzer.current_class is not None: + + if 0 < len(call_slot.node.args.defaults): raise InvalidProgramException( - node, - 'call_slots.nested.declaration', - "Callslot '%s' occurs inside a class" % node.name + call_slot.node.args.defaults[0], + 'call_slots.parameters.default', + "Method '%s' has a default parameter" % call_slot.node.name ) - analyzer.define_new(scope, node.name, node) + analyzer.visit(call_slot.node.args, call_slot.node) - self.call_slot = analyzer.node_factory.create_call_slot( - node.name, - node, - scope, - analyzer.node_factory - ) + if call_slot.var_arg is not None: + raise InvalidProgramException( + call_slot.node, + 'call_slots.parameters.var_args', + ("Method '%s' contains illegal variadic parameters" + % call_slot.node.name) + ) - scope.call_slots[node.name] = self.call_slot + if call_slot.kw_arg is not None: + raise InvalidProgramException( + call_slot.node, + 'call_slots.parameters.kw_args', + ("Method '%s' contains illegal keyword parameters" + % call_slot.node.name) + ) - analyzer.current_function = self.call_slot - _check_method_declaration(self.call_slot, analyzer) + def _pre_process(self, node: ast.FunctionDef) -> None: + """ + Abstract method for pre processing. + Has to initialize self.call_slot + """ + raise NotImplementedError() - body_node = node + def _check_body(self, body: List[ast.stmt]) -> None: + """ + Abstract method to check whether the body is valid. + """ + raise NotImplementedError() - has_uq_vars = _is_uq_vars(node.body) - if has_uq_vars: - body_node = node.body[0] - mock_call_slot = analyzer.node_factory.create_call_slot( - body_node.name, - body_node, - self.call_slot, - analyzer.node_factory + def _check_variables(self) -> None: + + # argument variables + argv = self.call_slot.args + # universally quantified variables + uqv = self.call_slot.uq_variables + # return variables + rtv = self.call_slot.return_variables + + shadowed_variables = argv.keys() & uqv.keys() + + if 0 < len(shadowed_variables): + shadowed_variable_name = next(iter(shadowed_variables)) + raise InvalidProgramException( + uqv[shadowed_variable_name].node, + "call_slots.parameters.illegal_shadowing", + "UQ variable '%s' illegally shadows an outer variable" % shadowed_variable_name ) - analyzer.outer_functions.append(self.call_slot) - analyzer.current_function = mock_call_slot + all_variable_names = argv.keys() | uqv.keys() - _check_method_declaration(mock_call_slot, analyzer) - self.call_slot.uq_variables = mock_call_slot.args + assert rtv is not None + if 0 < len(rtv): - body_node._parent = node + assert len(rtv) == 1 + return_variable = rtv[0] + assert isinstance(return_variable, ast.Name) - # FIXME: disallow shadowing through return variables? - _check_variables( - self.call_slot.args, - self.call_slot.uq_variables, - ILLEGAL_VARIABLE_NAMES - ) + if return_variable.id in all_variable_names: + raise InvalidProgramException( + return_variable, + "call_slots.parameters.illegal_shadowing", + "return variable '%s' illegally shadows an outer variable" % return_variable.id + ) - for child in body_node.body: - analyzer.visit(child, body_node) + all_variable_names.add(return_variable.id) - # TODO: pure call slots - # TODO: other call slot preprocessing/checks - # - body consists only of - # - Preconditions (done) - # - Postconditions (done) - # - One single call declaration - # - check 'well-formedness' of call - # - gather return values (save in CallSlot?) + invalid_variable_names = all_variable_names & _CallSlotBaseAnalyzer.__ILLEGAL_VARIABLE_NAMES - self._check_body(body_node.body) + if 0 < len(invalid_variable_names): + illegal_variable_name = next(iter(invalid_variable_names)) + + if illegal_variable_name in argv: + illegal_variable = argv[illegal_variable_name].node + elif illegal_variable_name in uqv: + illegal_variable = uqv[illegal_variable_name].node + else: + illegal_variable = rtv[0] + + raise InvalidProgramException( + illegal_variable, + "call_slots.parameters.illegal_name", + "Variable '%s' has an illegal name" % illegal_variable_name + ) - valid_varible_uses = ( - self.call_slot.args.keys() | - self.call_slot.uq_variables.keys() | - ILLEGAL_VARIABLE_NAMES | - set(map(lambda name: name.id, self.call_slot.return_variables)) + valid_variable_uses = ( + all_variable_names | + _CallSlotBaseAnalyzer.__ILLEGAL_VARIABLE_NAMES ) - self.allowed_variables_checker.reset(valid_varible_uses) - illegal_variable_uses = self.allowed_variables_checker.check(node) + self.allowed_variables_checker.reset(valid_variable_uses) + illegal_variable_uses = self.allowed_variables_checker.check(self.call_slot.node) if 0 < len(illegal_variable_uses): raise InvalidProgramException( illegal_variable_uses[0], @@ -122,15 +207,86 @@ def analyze(self, node: ast.FunctionDef) -> None: "Illegal reference to non-local name '%s'" % illegal_variable_uses[0].id ) - # cleanup - if has_uq_vars: - self.call_slot.precondition = mock_call_slot.precondition - self.call_slot.postcondition = mock_call_slot.postcondition - analyzer.outer_functions.pop() - analyzer.current_function = None + class _AllowedVariablesChecker(ast.NodeVisitor): + + # NOTE: might wanna push this to the translation stage + # FIXME: we don't support refering to global methods as variables + # NOTE: technically global variables/names would be ok (they're constants atm) + # FIXME: support nested call slot proofs + + def __init__(self, allowed_variables: Set[str] = set()) -> None: + self.reset(allowed_variables) - def _check_body(self, body: List[ast.stmt]): - self.has_call = False + def reset(self, allowed_variables: Set[str]) -> None: + self.offending_nodes = [] # type: List[ast.Name] + self.allowed_variables = allowed_variables + + def check(self, node: ast.AST) -> List[ast.Name]: + self.visit(node) + return self.offending_nodes + + def visit_Name(self, name: ast.Name) -> None: + if name.id not in self.allowed_variables: + self.offending_nodes.append(name) + + def visit_arg(self, arg: ast.arg) -> None: + return # ignore annotations + + def visit_Call(self, call: ast.Call) -> None: + # NOTE: what if call.func isn't an instance of ast.Name? + if isinstance(call.func, ast.Name): + if call.func.id == 'ClosureCall': + # FIXME: remove if references to global methods are supported + assert len(call.args) == 2 # should be guaranteed by type checker + self.visit(call.args[0]) + # ignore second argument + assert len(call.keywords) == 0 + for kw in call.keywords: + self.visit(kw) + return + + # ignore call.func, because it could be a valid predicate or + # function (if it's an illegal method, we'll catch it during + # translation) + for arg in call.args: + self.visit(arg) + for kw in call.keywords: + self.visit(kw) + + +class CallSlotAnalyzer(_CallSlotBaseAnalyzer): + + def _pre_process(self, node: ast.FunctionDef) -> None: + + assert is_call_slot(node) + scope = self.analyzer.module + assert isinstance(scope, PythonModule) + + if self.analyzer.current_function is not None: + raise InvalidProgramException( + node, + 'call_slots.nested.declaration', + "Callslot '%s' occurs inside a method" % node.name + ) + if self.analyzer.current_class is not None: + raise InvalidProgramException( + node, + 'call_slots.nested.declaration', + "Callslot '%s' occurs inside a class" % node.name + ) + + self.analyzer.define_new(scope, node.name, node) + + self.call_slot = self.analyzer.node_factory.create_call_slot( + node.name, + node, + scope, + self.analyzer.node_factory + ) + + scope.call_slots[node.name] = self.call_slot + + def _check_body(self, body: List[ast.stmt]) -> None: for child in body: if isinstance(child, ast.Expr) and isinstance(child.value, ast.Call): @@ -148,7 +304,7 @@ def _check_body(self, body: List[ast.stmt]): 'Callslot declarations must only consist of contracts and a single call' ) - if not self.has_call: + if self.call_slot.return_variables is None: raise InvalidProgramException( self.call_slot.node, 'call_slots.no.call', @@ -157,6 +313,13 @@ def _check_body(self, body: List[ast.stmt]): def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: + if self.call_slot.return_variables is not None: + raise InvalidProgramException( + node, + 'call_slots.multiple.calls', + "Callslot '%s' declares more than one call" % self.call_slot.node.name + ) + if isinstance(node, ast.Assign): if not isinstance(node.value, ast.Call): @@ -177,171 +340,240 @@ def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: if isinstance(node.targets[0], ast.Name): self.call_slot.return_variables = [node.targets[0]] - elif isinstance(node.targets[0], ast.Tuple): - - # NOTE: could add support for nested destructuring - # e.g., `a, (b, c), d = f(1, 2, 3)` - # currently we only support simple tuple assignments - for target in node.targets[0].elts: - if not isinstance(target, ast.Name): - raise UnsupportedException( - target, - "Callslots only support simple tuple assignments" - ) - - self.call_slot.return_variables = node.targets[0].elts else: raise UnsupportedException( node, - "Callslot's call has an unsupported return target" + "Callslot's call supports only a single variable as return target" ) else: + self.call_slot.return_variables = [] call = node self._check_call(call) - def _check_call(self, node: ast.Call) -> None: - if self.has_call: + def _check_call(self, call: ast.Call) -> None: + self.call_slot.call = call + + if not isinstance(call.func, ast.Name): + raise InvalidProgramException( + call.func, + 'call_slots.call_declaration.invalid_target', + "Callslot '%s' has an invalid call target" % self.call_slot.node.name + ) + if call.func.id not in self.call_slot.args: + raise InvalidProgramException( + call.func, + 'call_slots.call_declaration.invalid_target', + ("Callslot '%s' has an invalid call target (target must be a normal variable)" % + self.call_slot.node.name) + ) + + +class CallSlotProofAnalyzer(_CallSlotBaseAnalyzer): + + def _pre_process(self, node: ast.FunctionDef) -> None: + assert is_call_slot_proof(node) + + if self.analyzer.current_function is None: raise InvalidProgramException( node, - 'call_slots.multiple.calls', - "Callslot '%s' declares more than one call" % self.call_slot.node.name + 'call_slots.proof.outside_method', + "Callslotproof '%s' occurs outside a method" % node.name ) - self.has_call = True - self.call_slot.call = node + assert len(node.decorator_list) == 1 + assert isinstance(node.decorator_list[0], ast.Call) + assert isinstance(node.decorator_list[0].func, ast.Name) + assert node.decorator_list[0].func.id == 'CallSlotProof' -# FIXME: check call slot application + proof_annotation = node.decorator_list[0] # type: ast.Call + assert len(proof_annotation.args) == 1 -class CallSlotProofAnalyzer: + call_slot_instantiation = proof_annotation.args[0] + if not isinstance(call_slot_instantiation, ast.Call): + raise InvalidProgramException( + proof_annotation, + 'call_slots.proof_annotation.invalid_arg', + "Callslot proof '%s' doesn't have a valid call slot instantiation" + ) - # NOTE: we can probably reuse a lot from CallSlotAnalyzer + if not isinstance(call_slot_instantiation.func, ast.Name): + raise InvalidProgramException( + proof_annotation, + 'call_slots.proof_annotation.invalid_arg', + "Callslot proof '%s' doesn't have a valid call slot instantiation" + ) - def __init__(self, analyzer: 'Analyzer') -> None: - self.analyzer = analyzer + self.call_slot = self.analyzer.node_factory.create_call_slot_proof( + node.name, + node, + self.analyzer.current_function, + self.analyzer.node_factory, + call_slot_instantiation + ) - def analyze(self, node: ast.FunctionDef) -> None: - """ - Preprocess the call slot `node'. - """ - pass # FIXME: implement + def _check_body(self, body: List[ast.stmt]) -> None: + # Possible extensions: + # - local variables with assignments + # - while loops + # - new statements (for local variables) + # - restricted method calls (only for 'local state') -ILLEGAL_VARIABLE_NAMES = set(CONTRACT_FUNCS + CONTRACT_WRAPPER_FUNCS) + for child in body: + if isinstance(child, ast.Expr) and isinstance(child.value, ast.Call): + if is_precondition(child.value) or is_postcondition(child.value): + continue + if is_fold(child.value) or is_unfold(child.value): + continue + if is_assume(child.value): + continue + self._check_call_declaration(child.value) -def _check_variables( - normal_variables: Dict[str, PythonVar], - uq_variables: Dict[str, PythonVar], - illegal_variable_names: Set[str] -) -> None: + elif isinstance(child, ast.Assign): + self._check_call_declaration(child) - shadowed_variables = normal_variables.keys() & uq_variables.keys() + elif isinstance(child, ast.Assert): + continue - if 0 < len(shadowed_variables): - shadowed_variable_name = next(iter(shadowed_variables)) - raise InvalidProgramException( - uq_variables[shadowed_variable_name].node, - "call_slots.parameters.illegal_shadowing", - "UQ variable '%s' illegally shadows an outer variable" % shadowed_variable_name - ) + elif isinstance(child, ast.FunctionDef): + if not is_call_slot_proof(child): + # NOTE: dead code, Analyzer will throw before we can reach this + raise InvalidProgramException( + child, + 'call_slots.proof_body.invalid_stmt', + "Illegal statement in call slot proof '%s'" % self.call_slot.node.name + ) + # other call slot proof checks are done elsewhere - all_variable_names = normal_variables.keys() | uq_variables.keys() - illegal_variable_names = all_variable_names & illegal_variable_names + elif isinstance(child, ast.If): + # check purity of condition later + self._check_body(child.body) + self._check_body(child.orelse) - if 0 < len(illegal_variable_names): - illegal_variable_name = next(iter(illegal_variable_names)) + else: + raise InvalidProgramException( + child, + 'call_slots.proof_body.invalid_stmt', + "Illegal statement in call slot proof '%s'" % self.call_slot.node.name + ) - illegal_variable = ( - normal_variables[illegal_variable_name] if - illegal_variable_name in normal_variables else - uq_variables[illegal_variable_name] - ) - raise InvalidProgramException( - illegal_variable.node, - "call_slots.parameters.illegal_name", - "Variable '%s' has an illegal name" % illegal_variable_name - ) + def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: + if isinstance(node, ast.Assign): -class _AllowedVariablesChecker(ast.NodeVisitor): + if not isinstance(node.value, ast.Call): + raise InvalidProgramException( + node, + 'call_slots.proof_body.invalid_stmt', + "Callslot proof '%s' contains an illegal assignment" % self.call_slot.node.name + ) + call = node.value - # NOTE: CallSlotProofAnalyzer will require adjustments: - # In callslot proofs there can be nested call slot proofs which introduce - # new valid variables. + if len(node.targets) > 1: + raise UnsupportedException( + node, + "Callslot proof's call can't have more than one return target" + ) - def __init__(self, allowed_variables: Set[str] = set()) -> None: - self.reset(allowed_variables) + assert len(node.targets) == 1 - def reset(self, allowed_variables: Set[str]) -> None: - self.offending_nodes = [] # type: List[ast.Name] - self.allowed_variables = allowed_variables + if isinstance(node.targets[0], ast.Name): + if self.call_slot.return_variables is None: + self.call_slot.return_variables = [node.targets[0]] + elif ( + len(self.call_slot.return_variables) != 1 or + self.call_slot.return_variables[0].id != node.targets[0].id + ): + raise InvalidProgramException( + node, + 'call_slots.proof_body.different_return_variables', + "Callslot proof '%s' uses different return variables" % self.call_slot.node.name + ) + else: + raise UnsupportedException( + node, + "Callslot proof's call supports only a single variable as return target" + ) + else: + if self.call_slot.return_variables is None: + self.call_slot.return_variables = [] + elif len(self.call_slot.return_variables) != 0: + raise InvalidProgramException( + node, + 'call_slots.proof_body.different_return_variables', + "Callslot proof '%s' uses different return variables" % self.call_slot.node.name + ) + call = node - def check(self, node: ast.AST) -> List[ast.Name]: - self.visit(node) - return self.offending_nodes + self._check_closure_call(call) - def visit_Name(self, name: ast.Name) -> None: - if name.id not in self.allowed_variables: - self.offending_nodes.append(name) + def _check_closure_call(self, closureCall: ast.Call) -> None: - def visit_arg(self, arg: ast.arg) -> None: - return # ignore annotations + if not is_closure_call(closureCall): + raise InvalidProgramException( + closureCall, + 'call_slots.proof_call.not_closure_call', + "Callslot proof '%s' has a call which is not a ClosureCall" % self.call_slot.node.name + ) + assert len(closureCall.args) == 2 # guaranteed by type checker + assert isinstance(closureCall.args[0], ast.Call) -def _check_method_declaration(method: PythonMethod, analyzer: 'Analyzer') -> None: - """ - Checks whether `node' is a method declaration valid for a call slot or - universally quantified variables. If not raises an appropriate - exception. Expects analyzer.{current_function, outer_functions} to be - set correctly. - - * No magic name ('__...__') - * Return type = None - * No *args - * No **kwargs - """ + self._check_call(closureCall.args[0]) - # NOTE: needs tests + def _check_call(self, call: ast.Call) -> None: - if analyzer._is_illegal_magic_method_name(method.node.name): - raise InvalidProgramException(method.node, 'illegal.magic.method') + if not isinstance(call.func, ast.Name): + raise InvalidProgramException( + call.func, + 'call_slots.proof_call.invalid_target', + "Callslot proof '%s' has an invalid call target" % self.call_slot.node.name + ) + if call.func.id not in self.call_slot.args: + raise InvalidProgramException( + call.func, + 'call_slots.proof_call.invalid_target', + "Callslot '%s' has an invalid call target" % self.call_slot.node.name + ) - method.type = analyzer.convert_type( - analyzer.module.get_func_type(method.scope_prefix)) - if method.type is not None: - raise InvalidProgramException( - method.node, - 'call_slots.return.not_none', - "Method '%s' doesn't return 'None'" % method.node.name - ) +def check_closure_call(closureCall: ast.Call) -> None: + assert is_closure_call(closureCall) + assert len(closureCall.args) == 2 # guaranteed by type checker - if 0 < len(method.node.args.defaults): + if not isinstance(closureCall.args[0], ast.Call): raise InvalidProgramException( - method.node.args.defaults[0], - 'call_slots.parameters.default', - "Method '%s' has a default parameter" % method.node.name + closureCall.args[0], + 'call_slots.closure_call.invalid_call', + "ClosureCall's first argument has to be a call of a closure" ) - analyzer.visit(method.node.args, method.node) + justification = closureCall.args[1] - if method.var_arg is not None: + if not isinstance(justification, (ast.Name, ast.Call)): raise InvalidProgramException( - method.node, - 'call_slots.parameters.var_args', - ("Method '%s' contains illegal variadic parameters" - % method.node.name) + justification, + 'call_slots.closure_call.invalid_justification', + "ClosureCall's justification has to be either a call slot or static dispatch" ) - if method.kw_arg is not None: - raise InvalidProgramException( - method.node, - 'call_slots.parameters.kw_args', - ("Method '%s' contains illegal keyword parameters" - % method.node.name) - ) + if isinstance(justification, ast.Call): + if not isinstance(justification.func, ast.Call): + raise InvalidProgramException( + justification, + 'call_slots.closure_call.invalid_justification', + "ClosureCall's justification has to instatiate uq variables if it's a call slot" + ) + + if not isinstance(justification.func.func, ast.Name): + raise InvalidProgramException( + justification, + 'call_slots.closure_call.invalid_justification', + "ClosureCall's justification has to be a named if it's a call slot" + ) def _is_uq_vars(body) -> bool: @@ -356,45 +588,74 @@ def is_call_slot(node: ast.FunctionDef) -> bool: """ Whether node is a call slot declaration. """ - return _has_single_decorator(node, 'CallSlot') + return _has_single_decorator_name(node, 'CallSlot') def is_universally_quantified(node: ast.FunctionDef) -> bool: """ Whether a function introduces universally quantified variables """ - return _has_single_decorator(node, 'UniversallyQuantified') + return _has_single_decorator_name(node, 'UniversallyQuantified') def is_call_slot_proof(node: ast.FunctionDef) -> bool: """ Whether a function introduces universally quantified variables """ - return _has_single_decorator(node, 'CallSlotProof') + return _has_single_decorator_call(node, 'CallSlotProof') -def _has_single_decorator(node: ast.FunctionDef, decorator_name: str) -> bool: +def _has_single_decorator_name(node: ast.FunctionDef, decorator_name: str) -> bool: """ Whether `node' has only one decorator that equals to `decorator' """ - # NOTE: could be refactored out into a nagini 'util' package if len(node.decorator_list) != 1: return False + decorator = node.decorator_list[0] + if not isinstance(decorator, ast.Name): + return False - if isinstance(decorator, ast.Name): - return decorator.id == decorator_name + return decorator.id == decorator_name - if isinstance(decorator, ast.Call): - return decorator.func.id == decorator_name - # FIXME: should probably raise instead - return False +def _has_single_decorator_call(node: ast.FunctionDef, decorator_name: str) -> bool: + """ + Whether `node' has only one decorator that equals to `decorator' + """ + if len(node.decorator_list) != 1: + return False + decorator = node.decorator_list[0] + + if not isinstance(decorator, ast.Call): + return False + + return isinstance(decorator.func, ast.Name) and decorator.func.id == decorator_name + + +def is_closure_call(call: ast.Call) -> bool: + return is_named_call(call, 'ClosureCall') def is_precondition(call: ast.Call) -> bool: - return call.func.id == 'Requires' + return is_named_call(call, 'Requires') def is_postcondition(call: ast.Call) -> bool: - return call.func.id == 'Ensures' + return is_named_call(call, 'Ensures') + + +def is_fold(call: ast.Call) -> bool: + return is_named_call(call, 'Unfold') + + +def is_unfold(call: ast.Call) -> bool: + return is_named_call(call, 'Fold') + + +def is_assume(call: ast.Call) -> bool: + return is_named_call(call, 'Assume') + + +def is_named_call(call: ast.Call, name: str) -> bool: + return isinstance(call.func, ast.Name) and call.func.id == name diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 71d8429ae..251afe5b6 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1562,6 +1562,22 @@ def create_call_slot( container_factory ) + def create_call_slot_proof( + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + container_factory: 'ProgramNodeFactory', + call_slot_instantiation: ast.Call + ) -> 'CallSlotProof': + return CallSlotProof( + name, + node, + superscope, + container_factory, + call_slot_instantiation + ) + def create_python_io_operation(self, name: str, node: ast.AST, superscope: PythonScope, container_factory: 'ProgramNodeFactory', @@ -1581,14 +1597,14 @@ def create_python_class(self, name: str, superscope: PythonScope, superclass, interface) -class CallSlot(PythonMethod): +class CallSlotBase(PythonMethod): def __init__( - self, - name: str, - node: ast.FunctionDef, - superscope: PythonScope, - node_factory: 'ProgramNodeFactory', + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + node_factory: 'ProgramNodeFactory', ) -> None: PythonMethod.__init__( @@ -1601,8 +1617,51 @@ def __init__( False, # contract_only: bool node_factory # node_factory: 'ProgramNodeFactory' ) + # universally quantified variables self.uq_variables = OrderedDict() # type: Dict[str, PythonVar] + # NOTE: currently we only support one single return variable + self.return_variables = None # type: List[ast.Name] + + +class CallSlot(CallSlotBase): + + def __init__( + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + node_factory: 'ProgramNodeFactory', + ) -> None: + + CallSlotBase.__init__( + self, + name, + node, + superscope, + node_factory, + ) self.call = None # type: ast.Call - self.return_variables = [] # type: List[ast.Name] + + +class CallSlotProof(CallSlotBase): + + def __init__( + self, + name: str, + node: ast.FunctionDef, + superscope: PythonScope, + node_factory: 'ProgramNodeFactory', + call_slot_instantiation: ast.Call + ) -> None: + + CallSlotBase.__init__( + self, + name, + node, + superscope, + node_factory, + ) + + self.call_slot_instantiation = call_slot_instantiation From 972011962be4e862fd9153a1906f8b43933daf9d Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Wed, 11 Oct 2017 11:40:05 +0200 Subject: [PATCH 14/31] Added more tests for call slots --- .../test_arg_contract_shadowing.py} | 0 .../call_slot_declaration/test_double.py | 26 +++++++++ .../test_global_method.py} | 4 +- .../test_global_var.py} | 4 +- .../test_illegal_assign.py} | 0 .../test_illegal_return_name.py | 18 ++++++ .../test_magic_name.py} | 0 .../test_method_in_contract.py | 33 +++++++++++ .../test_method_name_collision.py} | 0 .../test_nested_class.py} | 0 .../test_nested_method.py} | 0 .../test_predicate_in_contract.py | 33 +++++++++++ .../test_return_shadowing.py | 19 +++++++ .../test_simple.py} | 0 .../test_simple_no_uq.py} | 0 .../test_uq_shadowing.py} | 0 .../test_arg_contract_shadowing.py | 42 ++++++++++++++ .../call_slot_proof/test_double.py | 57 +++++++++++++++++++ .../test_empty_instantiation.py | 49 ++++++++++++++++ .../call_slot_proof/test_global_method.py | 35 ++++++++++++ .../call_slot_proof/test_global_var.py | 27 +++++++++ .../call_slot_proof/test_illegal_assign.py | 50 ++++++++++++++++ .../test_invalid_instantiation.py | 49 ++++++++++++++++ .../call_slot_proof/test_magic_name.py | 49 ++++++++++++++++ .../test_method_name_collision.py | 52 +++++++++++++++++ .../test_proof_outside_function.py | 48 ++++++++++++++++ .../call_slot_proof/test_return_shadowing.py | 49 ++++++++++++++++ .../call_slot_proof/test_simple.py | 52 +++++++++++++++++ .../call_slot_proof/test_simple_no_uq.py | 48 ++++++++++++++++ .../call_slot_proof/test_uq_shadowing.py | 49 ++++++++++++++++ .../closure_call/test_bad_justification.py | 39 +++++++++++++ .../translation/closure_call/test_no_call.py | 39 +++++++++++++ .../closure_call/test_no_uq_vars.py | 39 +++++++++++++ .../closure_call/test_not_named_call_slot.py | 48 ++++++++++++++++ .../translation/closure_call/test_simple.py | 42 ++++++++++++++ .../test_call_slot_simple_with_tuple.py | 17 ------ 36 files changed, 996 insertions(+), 21 deletions(-) rename tests/closures/translation/{test_call_slot_arg_contract_shadowing.py => call_slot_declaration/test_arg_contract_shadowing.py} (100%) create mode 100644 tests/closures/translation/call_slot_declaration/test_double.py rename tests/closures/translation/{test_call_slot_global_method.py => call_slot_declaration/test_global_method.py} (79%) rename tests/closures/translation/{test_call_slot_global_var.py => call_slot_declaration/test_global_var.py} (78%) rename tests/closures/translation/{test_call_slot_illegal_assing.py => call_slot_declaration/test_illegal_assign.py} (100%) create mode 100644 tests/closures/translation/call_slot_declaration/test_illegal_return_name.py rename tests/closures/translation/{test_call_slot_magic_name.py => call_slot_declaration/test_magic_name.py} (100%) create mode 100644 tests/closures/translation/call_slot_declaration/test_method_in_contract.py rename tests/closures/translation/{test_call_slot_method_name_collision.py => call_slot_declaration/test_method_name_collision.py} (100%) rename tests/closures/translation/{test_call_slot_nested_class.py => call_slot_declaration/test_nested_class.py} (100%) rename tests/closures/translation/{test_call_slot_nested_method.py => call_slot_declaration/test_nested_method.py} (100%) create mode 100644 tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py create mode 100644 tests/closures/translation/call_slot_declaration/test_return_shadowing.py rename tests/closures/translation/{test_call_slot_simple.py => call_slot_declaration/test_simple.py} (100%) rename tests/closures/translation/{test_call_slot_simple_no_uq.py => call_slot_declaration/test_simple_no_uq.py} (100%) rename tests/closures/translation/{test_call_slot_uq_shadowing.py => call_slot_declaration/test_uq_shadowing.py} (100%) create mode 100644 tests/closures/translation/call_slot_proof/test_arg_contract_shadowing.py create mode 100644 tests/closures/translation/call_slot_proof/test_double.py create mode 100644 tests/closures/translation/call_slot_proof/test_empty_instantiation.py create mode 100644 tests/closures/translation/call_slot_proof/test_global_method.py create mode 100644 tests/closures/translation/call_slot_proof/test_global_var.py create mode 100644 tests/closures/translation/call_slot_proof/test_illegal_assign.py create mode 100644 tests/closures/translation/call_slot_proof/test_invalid_instantiation.py create mode 100644 tests/closures/translation/call_slot_proof/test_magic_name.py create mode 100644 tests/closures/translation/call_slot_proof/test_method_name_collision.py create mode 100644 tests/closures/translation/call_slot_proof/test_proof_outside_function.py create mode 100644 tests/closures/translation/call_slot_proof/test_return_shadowing.py create mode 100644 tests/closures/translation/call_slot_proof/test_simple.py create mode 100644 tests/closures/translation/call_slot_proof/test_simple_no_uq.py create mode 100644 tests/closures/translation/call_slot_proof/test_uq_shadowing.py create mode 100644 tests/closures/translation/closure_call/test_bad_justification.py create mode 100644 tests/closures/translation/closure_call/test_no_call.py create mode 100644 tests/closures/translation/closure_call/test_no_uq_vars.py create mode 100644 tests/closures/translation/closure_call/test_not_named_call_slot.py create mode 100644 tests/closures/translation/closure_call/test_simple.py delete mode 100644 tests/closures/translation/test_call_slot_simple_with_tuple.py diff --git a/tests/closures/translation/test_call_slot_arg_contract_shadowing.py b/tests/closures/translation/call_slot_declaration/test_arg_contract_shadowing.py similarity index 100% rename from tests/closures/translation/test_call_slot_arg_contract_shadowing.py rename to tests/closures/translation/call_slot_declaration/test_arg_contract_shadowing.py diff --git a/tests/closures/translation/call_slot_declaration/test_double.py b/tests/closures/translation/call_slot_declaration/test_double.py new file mode 100644 index 000000000..2c45df7b5 --- /dev/null +++ b/tests/closures/translation/call_slot_declaration/test_double.py @@ -0,0 +1,26 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures +) + + +@CallSlot +def call_slot1(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + +@CallSlot +def call_slot2(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) diff --git a/tests/closures/translation/test_call_slot_global_method.py b/tests/closures/translation/call_slot_declaration/test_global_method.py similarity index 79% rename from tests/closures/translation/test_call_slot_global_method.py rename to tests/closures/translation/call_slot_declaration/test_global_method.py index c38b786dc..0dadf5523 100644 --- a/tests/closures/translation/test_call_slot_global_method.py +++ b/tests/closures/translation/call_slot_declaration/test_global_method.py @@ -2,11 +2,11 @@ from nagini_contracts.contracts import CallSlot -def some_gloval_method() -> None: +def some_global_method() -> None: pass @CallSlot def some_call_slot(f: Callable[[Callable[[], None]], None]) -> None: #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - f(some_gloval_method) + f(some_global_method) diff --git a/tests/closures/translation/test_call_slot_global_var.py b/tests/closures/translation/call_slot_declaration/test_global_var.py similarity index 78% rename from tests/closures/translation/test_call_slot_global_var.py rename to tests/closures/translation/call_slot_declaration/test_global_var.py index 8326d0ea0..7114174e2 100644 --- a/tests/closures/translation/test_call_slot_global_var.py +++ b/tests/closures/translation/call_slot_declaration/test_global_var.py @@ -1,9 +1,9 @@ from typing import Callable from nagini_contracts.contracts import CallSlot -some_gloval_var = 5 # type: int +some_global_var = 5 # type: int @CallSlot def some_call_slot(f: Callable[[int], None]) -> None: #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - f(some_gloval_var) + f(some_global_var) diff --git a/tests/closures/translation/test_call_slot_illegal_assing.py b/tests/closures/translation/call_slot_declaration/test_illegal_assign.py similarity index 100% rename from tests/closures/translation/test_call_slot_illegal_assing.py rename to tests/closures/translation/call_slot_declaration/test_illegal_assign.py diff --git a/tests/closures/translation/call_slot_declaration/test_illegal_return_name.py b/tests/closures/translation/call_slot_declaration/test_illegal_return_name.py new file mode 100644 index 000000000..553ab03f8 --- /dev/null +++ b/tests/closures/translation/call_slot_declaration/test_illegal_return_name.py @@ -0,0 +1,18 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_name) + Acc = f(x, y) + Ensures(Acc == x + y) diff --git a/tests/closures/translation/test_call_slot_magic_name.py b/tests/closures/translation/call_slot_declaration/test_magic_name.py similarity index 100% rename from tests/closures/translation/test_call_slot_magic_name.py rename to tests/closures/translation/call_slot_declaration/test_magic_name.py diff --git a/tests/closures/translation/call_slot_declaration/test_method_in_contract.py b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py new file mode 100644 index 000000000..7238c8a96 --- /dev/null +++ b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py @@ -0,0 +1,33 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures, + Acc, + Fold +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], arg: 'Arg') -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + #:: IgnoreFile(42) + # will be supported during translation + #:: ExpectedOutput(invalid.program:purity.violated) + Requires(is_arg(arg) and y > 0) + z = f(arg.val, y) + Ensures(z >= y) + + +class Arg: + + def __init__(self) -> None: + Ensures(Acc(self.val)) + self.val = 1 # type: int + + +def is_arg(arg: Arg) -> bool: + return True diff --git a/tests/closures/translation/test_call_slot_method_name_collision.py b/tests/closures/translation/call_slot_declaration/test_method_name_collision.py similarity index 100% rename from tests/closures/translation/test_call_slot_method_name_collision.py rename to tests/closures/translation/call_slot_declaration/test_method_name_collision.py diff --git a/tests/closures/translation/test_call_slot_nested_class.py b/tests/closures/translation/call_slot_declaration/test_nested_class.py similarity index 100% rename from tests/closures/translation/test_call_slot_nested_class.py rename to tests/closures/translation/call_slot_declaration/test_nested_class.py diff --git a/tests/closures/translation/test_call_slot_nested_method.py b/tests/closures/translation/call_slot_declaration/test_nested_method.py similarity index 100% rename from tests/closures/translation/test_call_slot_nested_method.py rename to tests/closures/translation/call_slot_declaration/test_nested_method.py diff --git a/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py b/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py new file mode 100644 index 000000000..b6f16935f --- /dev/null +++ b/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py @@ -0,0 +1,33 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures, + Acc, + Predicate, + Fold +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], arg: 'Arg') -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(is_arg(arg) and y > 0) + z = f(arg.val, y) + Ensures(z >= y) + + +class Arg: + + def __init__(self) -> None: + Ensures(is_arg(self)) + self.val = 1 # type: int + Fold(is_arg(self)) + + +@Predicate +def is_arg(arg: Arg) -> bool: + return Acc(arg.val) diff --git a/tests/closures/translation/call_slot_declaration/test_return_shadowing.py b/tests/closures/translation/call_slot_declaration/test_return_shadowing.py new file mode 100644 index 000000000..8766882c2 --- /dev/null +++ b/tests/closures/translation/call_slot_declaration/test_return_shadowing.py @@ -0,0 +1,19 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + UniversallyQuantified, + Requires, + Ensures, + Old +) + + +@CallSlot +def call_slot(f: Callable[[int, int], int], x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_shadowing) + y = f(x, y) + Ensures(y == x + Old(y)) diff --git a/tests/closures/translation/test_call_slot_simple.py b/tests/closures/translation/call_slot_declaration/test_simple.py similarity index 100% rename from tests/closures/translation/test_call_slot_simple.py rename to tests/closures/translation/call_slot_declaration/test_simple.py diff --git a/tests/closures/translation/test_call_slot_simple_no_uq.py b/tests/closures/translation/call_slot_declaration/test_simple_no_uq.py similarity index 100% rename from tests/closures/translation/test_call_slot_simple_no_uq.py rename to tests/closures/translation/call_slot_declaration/test_simple_no_uq.py diff --git a/tests/closures/translation/test_call_slot_uq_shadowing.py b/tests/closures/translation/call_slot_declaration/test_uq_shadowing.py similarity index 100% rename from tests/closures/translation/test_call_slot_uq_shadowing.py rename to tests/closures/translation/call_slot_declaration/test_uq_shadowing.py diff --git a/tests/closures/translation/call_slot_proof/test_arg_contract_shadowing.py b/tests/closures/translation/call_slot_proof/test_arg_contract_shadowing.py new file mode 100644 index 000000000..35b6238d1 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_arg_contract_shadowing.py @@ -0,0 +1,42 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + Requires(x >= 0) + z = f(x) + Ensures(z >= x) + + +def method() -> None: + + x = 1 + f = add + + @CallSlotProof(call_slot(f, x)) + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_name) + def call_slot_proof(Acc: F_Type, x: int) -> None: + Requires(x >= 0) + + # justified because f == add + z = ClosureCall(Acc(x), add) # type: int + Ensures(z >= x) diff --git a/tests/closures/translation/call_slot_proof/test_double.py b/tests/closures/translation/call_slot_proof/test_double.py new file mode 100644 index 000000000..a63e58fad --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_double.py @@ -0,0 +1,57 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], None] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + f(x, y) + + +def method() -> None: + + x = 1 + f = add + + #:: IgnoreFile(42) + # Implementation not far enough yet + @CallSlotProof(call_slot(f, x)) + def call_slot_proof1(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + ClosureCall(f(x, y), add) # type: int + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof2(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + ClosureCall(f(x, y), add) # type: int diff --git a/tests/closures/translation/call_slot_proof/test_empty_instantiation.py b/tests/closures/translation/call_slot_proof/test_empty_instantiation.py new file mode 100644 index 000000000..0382d2f67 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_empty_instantiation.py @@ -0,0 +1,49 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + #:: ExpectedOutput(type.error:Too few arguments for "CallSlotProof") + @CallSlotProof() + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_global_method.py b/tests/closures/translation/call_slot_proof/test_global_method.py new file mode 100644 index 000000000..262b69ffd --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_global_method.py @@ -0,0 +1,35 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +@CallSlot +def some_call_slot(f: Callable[[Callable[[], None]], None], g: Callable[[], None]) -> None: + f(g) + + +def some_global_hof_method(g: Callable[[], None]) -> None: + pass + + +def some_global_method() -> None: + pass + + +def method() -> None: + + f = some_global_hof_method + g = some_global_method + + @CallSlotProof(some_call_slot(f, g)) + def call_slot_proof(f: Callable[[Callable[[], None]], None], g: Callable[[], None]) -> None: + + # justified because f == some_global_hof_method + #:: ExpectedOutput(invalid.program:call_slot.names.non_local) + ClosureCall(f(some_global_method), some_global_hof_method) # type: int diff --git a/tests/closures/translation/call_slot_proof/test_global_var.py b/tests/closures/translation/call_slot_proof/test_global_var.py new file mode 100644 index 000000000..fc4ec7b9d --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_global_var.py @@ -0,0 +1,27 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + ClosureCall, +) + +some_global_var = 5 # type: int + + +def some_method(x: int) -> None: + pass + + +@CallSlot +def some_call_slot(f: Callable[[int], None]) -> None: + f(5) + + +def test_method() -> None: + + f = some_method + + @CallSlotProof(some_call_slot(f)) + def some_call_slot_proof(f: Callable[[int], None]) -> None: + #:: ExpectedOutput(invalid.program:call_slot.names.non_local) + ClosureCall(f(some_global_var), some_method) diff --git a/tests/closures/translation/call_slot_proof/test_illegal_assign.py b/tests/closures/translation/call_slot_proof/test_illegal_assign.py new file mode 100644 index 000000000..5a3d2f8f0 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_illegal_assign.py @@ -0,0 +1,50 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + #:: ExpectedOutput(invalid.program:call_slots.proof_body.invalid_stmt) + x = 2 + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_invalid_instantiation.py b/tests/closures/translation/call_slot_proof/test_invalid_instantiation.py new file mode 100644 index 000000000..691d3bf53 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_invalid_instantiation.py @@ -0,0 +1,49 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + #:: ExpectedOutput(invalid.program:call_slots.proof_annotation.invalid_arg) + @CallSlotProof(call_slot) + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_magic_name.py b/tests/closures/translation/call_slot_proof/test_magic_name.py new file mode 100644 index 000000000..ecfdd637c --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_magic_name.py @@ -0,0 +1,49 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + #:: ExpectedOutput(invalid.program:illegal.magic.method) + @CallSlotProof(call_slot(f, x)) + def __call_slot_proof__(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_method_name_collision.py b/tests/closures/translation/call_slot_proof/test_method_name_collision.py new file mode 100644 index 000000000..c2f5b77f0 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_method_name_collision.py @@ -0,0 +1,52 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + def some_name() -> None: + pass + + #:: ExpectedOutput(type.error:Name 'some_name' already defined) + @CallSlotProof(call_slot(f, x)) + def some_name(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_proof_outside_function.py b/tests/closures/translation/call_slot_proof/test_proof_outside_function.py new file mode 100644 index 000000000..0399fb97a --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_proof_outside_function.py @@ -0,0 +1,48 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +x = 1 +f = add + + +#:: ExpectedOutput(invalid.program:call_slots.proof.outside_method) +@CallSlotProof(call_slot(f, x)) +def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_return_shadowing.py b/tests/closures/translation/call_slot_proof/test_return_shadowing.py new file mode 100644 index 000000000..3cb3d9ea6 --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_return_shadowing.py @@ -0,0 +1,49 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == y) + + +def method() -> None: + + x = 1 + f = add + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_shadowing) + x = ClosureCall(f(x, y), add) # type: int + + Ensures(x == y) diff --git a/tests/closures/translation/call_slot_proof/test_simple.py b/tests/closures/translation/call_slot_proof/test_simple.py new file mode 100644 index 000000000..61c2932eb --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_simple.py @@ -0,0 +1,52 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +#:: IgnoreFile(42) +# Call slot proof support isn't implemented enough at the moment. + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + + # justified because f == add + z = ClosureCall(f(x, y), add) # type: int + + Ensures(z == x + y) diff --git a/tests/closures/translation/call_slot_proof/test_simple_no_uq.py b/tests/closures/translation/call_slot_proof/test_simple_no_uq.py new file mode 100644 index 000000000..55100d78c --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_simple_no_uq.py @@ -0,0 +1,48 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def inc(x: int) -> int: + return x + 1 + + +def twice(x: int) -> int: + return 2 * x + + +F_Type = Callable[[int], int] + + +#:: IgnoreFile(42) +# Call slot proof support isn't implemented enough at the moment. + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + Requires(x >= 0) + z = f(x) + Ensures(z >= x) + + +def method() -> None: + + x = 1 + f = inc + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof(f: F_Type, x: int) -> None: + + Requires(x >= 0) + + # justified because f == add + z = ClosureCall(f(x), inc) # type: int + + Ensures(z >= x) diff --git a/tests/closures/translation/call_slot_proof/test_uq_shadowing.py b/tests/closures/translation/call_slot_proof/test_uq_shadowing.py new file mode 100644 index 000000000..59155e9ed --- /dev/null +++ b/tests/closures/translation/call_slot_proof/test_uq_shadowing.py @@ -0,0 +1,49 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method() -> None: + + x = 1 + f = add + + @CallSlotProof(call_slot(f, x)) + def call_slot_proof(f: F_Type, x: int) -> None: + + @UniversallyQuantified + #:: ExpectedOutput(invalid.program:call_slots.parameters.illegal_shadowing) + def uq(x: int) -> None: + Requires(x >= 0 and x > x) + + # justified because f == add + z = ClosureCall(f(x, x), add) # type: int + + Ensures(z == x + x) diff --git a/tests/closures/translation/closure_call/test_bad_justification.py b/tests/closures/translation/closure_call/test_bad_justification.py new file mode 100644 index 000000000..df6a87330 --- /dev/null +++ b/tests/closures/translation/closure_call/test_bad_justification.py @@ -0,0 +1,39 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method(f: F_Type, x: int) -> None: + Requires(call_slot(f, x)) + + y = 1 + + #:: ExpectedOutput(invalid.program:call_slots.closure_call.invalid_justification) + z = ClosureCall(f(x, y), x + y) # type: int diff --git a/tests/closures/translation/closure_call/test_no_call.py b/tests/closures/translation/closure_call/test_no_call.py new file mode 100644 index 000000000..194ef894b --- /dev/null +++ b/tests/closures/translation/closure_call/test_no_call.py @@ -0,0 +1,39 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method(f: F_Type, x: int) -> None: + Requires(call_slot(f, x)) + + y = 1 + + #:: ExpectedOutput(invalid.program:call_slots.closure_call.invalid_call) + z = ClosureCall(x, call_slot(f, x)(y)) # type: int diff --git a/tests/closures/translation/closure_call/test_no_uq_vars.py b/tests/closures/translation/closure_call/test_no_uq_vars.py new file mode 100644 index 000000000..cbfd34862 --- /dev/null +++ b/tests/closures/translation/closure_call/test_no_uq_vars.py @@ -0,0 +1,39 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method(f: F_Type, x: int) -> None: + Requires(call_slot(f, x)) + + y = 1 + + #:: ExpectedOutput(invalid.program:call_slots.closure_call.invalid_justification) + z = ClosureCall(f(x, y), call_slot(f, x)) # type: int diff --git a/tests/closures/translation/closure_call/test_not_named_call_slot.py b/tests/closures/translation/closure_call/test_not_named_call_slot.py new file mode 100644 index 000000000..d0f249d5e --- /dev/null +++ b/tests/closures/translation/closure_call/test_not_named_call_slot.py @@ -0,0 +1,48 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures, + Acc +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +class A: + + def __init__(self) -> None: + self.call_slot = call_slot # type: Callable + Ensures(Acc(self.call_slot)) + + +def method(f: F_Type, x: int) -> None: + Requires(call_slot(f, x)) + + y = 1 + a = A() + + #:: ExpectedOutput(invalid.program:call_slots.closure_call.invalid_justification) + z = ClosureCall(f(x, y), (a.call_slot)(f, x)(y)) # type: int diff --git a/tests/closures/translation/closure_call/test_simple.py b/tests/closures/translation/closure_call/test_simple.py new file mode 100644 index 000000000..e63a939d4 --- /dev/null +++ b/tests/closures/translation/closure_call/test_simple.py @@ -0,0 +1,42 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Requires, + Ensures +) + + +def add(x: int, y: int) -> int: + return x + y + + +def mul(x: int, y: int) -> int: + return x * y + + +F_Type = Callable[[int, int], int] + + +#:: IgnoreFile(42) +# Call slot proof support isn't implemented enough at the moment. + + +@CallSlot +def call_slot(f: F_Type, x: int) -> None: + + @UniversallyQuantified + def uq(y: int) -> None: + Requires(x >= 0 and y > x) + z = f(x, y) + Ensures(z == x + y) + + +def method(f: F_Type, x: int) -> None: + Requires(call_slot(f, x)) + + y = 1 + + z = ClosureCall(f(x, y), call_slot(f, x)(y)) # type: int diff --git a/tests/closures/translation/test_call_slot_simple_with_tuple.py b/tests/closures/translation/test_call_slot_simple_with_tuple.py deleted file mode 100644 index 7aebbc471..000000000 --- a/tests/closures/translation/test_call_slot_simple_with_tuple.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Callable, Tuple -from nagini_contracts.contracts import ( - CallSlot, - UniversallyQuantified, - Requires, - Ensures -) - - -@CallSlot -def call_slot(f: Callable[[int, int], Tuple[int, int, int, int]], x: int) -> None: - - @UniversallyQuantified - def uq(y: int) -> None: - Requires(x >= 0 and y > x) - a, b, c, z = f(x, y) - Ensures(z == x + y and b == c and a <= 0) From 01d319326e985fc3051d972f4ee861c5487731c5 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Fri, 13 Oct 2017 15:54:51 +0200 Subject: [PATCH 15/31] Removed local variables check inside call slot proofs --- src/nagini_translation/call_slot_analyzers.py | 60 ------------------- .../test_global_method.py | 12 ---- .../call_slot_declaration/test_global_var.py | 9 --- .../call_slot_proof/test_global_method.py | 35 ----------- .../call_slot_proof/test_global_var.py | 27 --------- 5 files changed, 143 deletions(-) delete mode 100644 tests/closures/translation/call_slot_declaration/test_global_method.py delete mode 100644 tests/closures/translation/call_slot_declaration/test_global_var.py delete mode 100644 tests/closures/translation/call_slot_proof/test_global_method.py delete mode 100644 tests/closures/translation/call_slot_proof/test_global_var.py diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 68db28fb9..b0d564211 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -22,7 +22,6 @@ class _CallSlotBaseAnalyzer: def __init__(self, analyzer: 'Analyzer') -> None: self.analyzer = analyzer self.call_slot = None # type: CallSlotBase - self.allowed_variables_checker = _CallSlotBaseAnalyzer._AllowedVariablesChecker() def analyze(self, node: ast.FunctionDef) -> None: """ @@ -194,65 +193,6 @@ def _check_variables(self) -> None: "Variable '%s' has an illegal name" % illegal_variable_name ) - valid_variable_uses = ( - all_variable_names | - _CallSlotBaseAnalyzer.__ILLEGAL_VARIABLE_NAMES - ) - self.allowed_variables_checker.reset(valid_variable_uses) - illegal_variable_uses = self.allowed_variables_checker.check(self.call_slot.node) - if 0 < len(illegal_variable_uses): - raise InvalidProgramException( - illegal_variable_uses[0], - 'call_slot.names.non_local', - "Illegal reference to non-local name '%s'" % illegal_variable_uses[0].id - ) - - class _AllowedVariablesChecker(ast.NodeVisitor): - - # NOTE: might wanna push this to the translation stage - # FIXME: we don't support refering to global methods as variables - # NOTE: technically global variables/names would be ok (they're constants atm) - # FIXME: support nested call slot proofs - - def __init__(self, allowed_variables: Set[str] = set()) -> None: - self.reset(allowed_variables) - - def reset(self, allowed_variables: Set[str]) -> None: - self.offending_nodes = [] # type: List[ast.Name] - self.allowed_variables = allowed_variables - - def check(self, node: ast.AST) -> List[ast.Name]: - self.visit(node) - return self.offending_nodes - - def visit_Name(self, name: ast.Name) -> None: - if name.id not in self.allowed_variables: - self.offending_nodes.append(name) - - def visit_arg(self, arg: ast.arg) -> None: - return # ignore annotations - - def visit_Call(self, call: ast.Call) -> None: - # NOTE: what if call.func isn't an instance of ast.Name? - if isinstance(call.func, ast.Name): - if call.func.id == 'ClosureCall': - # FIXME: remove if references to global methods are supported - assert len(call.args) == 2 # should be guaranteed by type checker - self.visit(call.args[0]) - # ignore second argument - assert len(call.keywords) == 0 - for kw in call.keywords: - self.visit(kw) - return - - # ignore call.func, because it could be a valid predicate or - # function (if it's an illegal method, we'll catch it during - # translation) - for arg in call.args: - self.visit(arg) - for kw in call.keywords: - self.visit(kw) - class CallSlotAnalyzer(_CallSlotBaseAnalyzer): diff --git a/tests/closures/translation/call_slot_declaration/test_global_method.py b/tests/closures/translation/call_slot_declaration/test_global_method.py deleted file mode 100644 index 0dadf5523..000000000 --- a/tests/closures/translation/call_slot_declaration/test_global_method.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Callable -from nagini_contracts.contracts import CallSlot - - -def some_global_method() -> None: - pass - - -@CallSlot -def some_call_slot(f: Callable[[Callable[[], None]], None]) -> None: - #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - f(some_global_method) diff --git a/tests/closures/translation/call_slot_declaration/test_global_var.py b/tests/closures/translation/call_slot_declaration/test_global_var.py deleted file mode 100644 index 7114174e2..000000000 --- a/tests/closures/translation/call_slot_declaration/test_global_var.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Callable -from nagini_contracts.contracts import CallSlot - -some_global_var = 5 # type: int - -@CallSlot -def some_call_slot(f: Callable[[int], None]) -> None: - #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - f(some_global_var) diff --git a/tests/closures/translation/call_slot_proof/test_global_method.py b/tests/closures/translation/call_slot_proof/test_global_method.py deleted file mode 100644 index 262b69ffd..000000000 --- a/tests/closures/translation/call_slot_proof/test_global_method.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Callable -from nagini_contracts.contracts import ( - CallSlot, - CallSlotProof, - UniversallyQuantified, - ClosureCall, - Requires, - Ensures -) - - -@CallSlot -def some_call_slot(f: Callable[[Callable[[], None]], None], g: Callable[[], None]) -> None: - f(g) - - -def some_global_hof_method(g: Callable[[], None]) -> None: - pass - - -def some_global_method() -> None: - pass - - -def method() -> None: - - f = some_global_hof_method - g = some_global_method - - @CallSlotProof(some_call_slot(f, g)) - def call_slot_proof(f: Callable[[Callable[[], None]], None], g: Callable[[], None]) -> None: - - # justified because f == some_global_hof_method - #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - ClosureCall(f(some_global_method), some_global_hof_method) # type: int diff --git a/tests/closures/translation/call_slot_proof/test_global_var.py b/tests/closures/translation/call_slot_proof/test_global_var.py deleted file mode 100644 index fc4ec7b9d..000000000 --- a/tests/closures/translation/call_slot_proof/test_global_var.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Callable -from nagini_contracts.contracts import ( - CallSlot, - CallSlotProof, - ClosureCall, -) - -some_global_var = 5 # type: int - - -def some_method(x: int) -> None: - pass - - -@CallSlot -def some_call_slot(f: Callable[[int], None]) -> None: - f(5) - - -def test_method() -> None: - - f = some_method - - @CallSlotProof(some_call_slot(f)) - def some_call_slot_proof(f: Callable[[int], None]) -> None: - #:: ExpectedOutput(invalid.program:call_slot.names.non_local) - ClosureCall(f(some_global_var), some_method) From b07399f0e36db9d75e8c739c4f8b86cbbcc3068e Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sun, 15 Oct 2017 18:19:15 +0200 Subject: [PATCH 16/31] Added example a from the project description to the tests --- .../verification/examples/static_code.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/closures/verification/examples/static_code.py diff --git a/tests/closures/verification/examples/static_code.py b/tests/closures/verification/examples/static_code.py new file mode 100644 index 000000000..b7a7cf2be --- /dev/null +++ b/tests/closures/verification/examples/static_code.py @@ -0,0 +1,147 @@ +from typing import Callable, Tuple +from nagini_contracts.contracts import ( + Requires, + Ensures, + Invariant, + Acc, + Result, + Pure, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, +) + +#:: IgnoreFile(42) +# Call slot support isn't implemented enough at the moment. + +@Pure +def idiv(x: int, y: int) -> int: + Requires(y != 0) + + +@Pure +def sqrt(x: int) -> int: + Requires(x >= 0) + + +@Pure +def log(x: int) -> int: + Requires(x > 0) + + +def choice() -> bool: + pass + + +class Argument: + + def __init__(self, parameter: int, result: int) -> None: + self.parameter = parameter # type: int + self.result = result # type: int + + Ensures(Acc(self.parameter) and Acc(self.result)) + Ensures(self.parameter == parameter and self.result == result) + + +G_Type = Callable[[Argument, int, int], Tuple[int, int]] + + +@CallSlot +def func_call_slot(g: G_Type, b: int, c: int) -> None: + + @UniversallyQuantified + def uq(a: Argument) -> None: + + Requires(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Requires(a.parameter >= b + c) + + ret = g(a, b, c) + + Ensures(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Ensures(a.result != 0 and ret[0] >= 0 and ret[1] > 0) + + +def func( + g: G_Type, + a: Argument, + b: int, + c: int +) -> Tuple[int, int, int]: + + Requires(func_call_slot(g, b, c)) + Requires(Acc(a.parameter)) + Requires(Acc(a.result)) + Ensures(Acc(a.parameter)) + Ensures(Acc(a.result)) + + while a.parameter < b + c: + Invariant(Acc(a.parameter)) + a.parameter = a.parameter * a.parameter + + # g reads a.parameter and writes to a.result + + # closure call justified because the call slot holds: + # func_call_slot(g, b, c) + d, e = ClosureCall(g(a, b, c), func_call_slot(g, b, c)(a)) # type: Tuple[int, int] + + return idiv(1, a.result), sqrt(d), log(e) + + +def concrete_g_1(a: Argument, b: int, c: int) -> Tuple[int, int]: + Requires(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Requires(a.parameter >= b + c) + Ensures(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Ensures(a.result == -1 and Result()[0] == 0 and Result()[1] >= 1) + + a.result = -1 + return 0, a.parameter - b - c + 1 + + +def concrete_g_2(a: Argument, b: int, c: int) -> Tuple[int, int]: + Requires(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Requires(a.parameter >= b + c) + Ensures(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Ensures(a.result == 1 and Result()[0] >= 0 and Result()[1] == 1) + + a.result = 1 + return a.parameter - b - c, 1 + + +def client() -> None: + + if choice(): # non-deterministic choice + concrete_g = concrete_g_1 + else: + concrete_g = concrete_g_2 + + a = Argument(2, 2) + b, c = 2, 3 + + @CallSlotProof(func_call_slot(concrete_g, b, c)) + def func_call_slot_proof(concrete_g: G_Type, b: int, c: int) -> None: + + @UniversallyQuantified + def uq(a: Argument, d: int, e: int) -> None: + + Requires(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Requires(a.parameter >= b + c) + + if concrete_g == concrete_g_1: + # closure call justified, because we can prove static dispatch: + # concrete_g == concrete_g_1 + # and concrete_g_1 is a method whose contracts we can look up + # statically/in nagini + ret = ClosureCall(concrete_g(a, b, c), concrete_g_1) # type: Tuple[int, int] + else: + assert concrete_g == concrete_g_2 + # closure call justified, because we can prove static dispatch: + # concrete_g == concrete_g_2 + # and concrete_g_2 is a method whose contracts we can look up + # statically/in nagini + ret = ClosureCall(concrete_g(a, b, c), concrete_g_2) + + Ensures(Acc(a.parameter, 1 / 2) and Acc(a.result)) + Ensures(a.result != 0 and ret[0] >= 0 and ret[1] > 0) + + func(concrete_g, a, b, c) From b74779d4fe1c7362628fe06437ca0e8ec071b2ce Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sun, 15 Oct 2017 18:19:38 +0200 Subject: [PATCH 17/31] Fixed a typing error in a test case --- .../translation/closure_call/test_not_named_call_slot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/closures/translation/closure_call/test_not_named_call_slot.py b/tests/closures/translation/closure_call/test_not_named_call_slot.py index d0f249d5e..43243478f 100644 --- a/tests/closures/translation/closure_call/test_not_named_call_slot.py +++ b/tests/closures/translation/closure_call/test_not_named_call_slot.py @@ -34,7 +34,7 @@ def uq(y: int) -> None: class A: def __init__(self) -> None: - self.call_slot = call_slot # type: Callable + self.call_slot = call_slot # type: Callable[[F_Type, int], Callable[[int], int]] Ensures(Acc(self.call_slot)) From 6f7c15691c6e6b4d27b0334a9da935c2f7232dc6 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sun, 15 Oct 2017 18:23:33 +0200 Subject: [PATCH 18/31] Progressed with translation of call slots --- src/nagini_translation/analyzer.py | 8 +- src/nagini_translation/call_slot_analyzers.py | 10 + src/nagini_translation/lib/program_nodes.py | 5 +- src/nagini_translation/lib/resolver.py | 4 +- src/nagini_translation/translator.py | 3 + .../translators/abstract.py | 14 ++ src/nagini_translation/translators/call.py | 6 + .../call_slots/call_slot_translator.py | 196 ++++++++++++++++++ .../translators/statement.py | 7 + 9 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 src/nagini_translation/translators/call_slots/call_slot_translator.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 2d790f6a1..5f08cf9bd 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -988,7 +988,13 @@ def _convert_normal_type(self, mypy_type) -> PythonType: return result def _convert_callable_type(self, mypy_type, node) -> PythonType: - return self.find_or_create_class(CALLABLE_TYPE, module=self.module.global_module) + return GenericType( + self.find_or_create_class(CALLABLE_TYPE, module=self.module.global_module), + [ + [self.convert_type(arg_type, node) for arg_type in mypy_type.arg_types], + self.convert_type(mypy_type.ret_type, node) + ] + ) def _convert_union_type(self, mypy_type, node) -> PythonType: args = [self.convert_type(arg_type, node) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index b0d564211..105588445 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -65,6 +65,7 @@ def analyze(self, node: ast.FunctionDef) -> None: # cleanup if has_uq_vars: + self.call_slot._locals = mock_call_slot._locals self.call_slot.precondition = mock_call_slot.precondition self.call_slot.postcondition = mock_call_slot.postcondition self.analyzer.outer_functions.pop() @@ -280,6 +281,7 @@ def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: if isinstance(node.targets[0], ast.Name): self.call_slot.return_variables = [node.targets[0]] + self.call_slot.return_type = self.analyzer.typeof(node.targets[0]) else: raise UnsupportedException( node, @@ -345,6 +347,12 @@ def _pre_process(self, node: ast.FunctionDef) -> None: "Callslot proof '%s' doesn't have a valid call slot instantiation" ) + if len(call_slot_instantiation.args) != len(node.args.args): + raise InvalidProgramException( + proof_annotation, + 'call_slots.proof_annotation.invalid_arg', + ) + self.call_slot = self.analyzer.node_factory.create_call_slot_proof( node.name, node, @@ -353,6 +361,8 @@ def _pre_process(self, node: ast.FunctionDef) -> None: call_slot_instantiation ) + self.analyzer.current_function.call_slot_proofs[node] = self.call_slot + def _check_body(self, body: List[ast.stmt]) -> None: # Possible extensions: diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 251afe5b6..018283b37 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -220,7 +220,8 @@ def get_contents(self, only_top: bool) -> Dict: elements that can be accessed without a receiver. """ dicts = [self.classes, self.functions, self.global_vars, self.methods, - self.predicates, self.io_operations, self.namespaces] + self.predicates, self.io_operations, self.namespaces, + self.call_slots] return CombinedDict([], dicts) @@ -775,6 +776,7 @@ def __init__(self, name: str, node: ast.AST, cls: PythonClass, self.type_vars = OrderedDict() self.setter = None self.func_constant = None + self.call_slot_proofs: Dict[ast.FunctionDef, CallSlotProof] = {} def process(self, sil_name: str, translator: 'Translator') -> None: """ @@ -1643,6 +1645,7 @@ def __init__( ) self.call = None # type: ast.Call + self.return_type: PythonType = None class CallSlotProof(CallSlotBase): diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 77bd410cd..169b97d6d 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -355,6 +355,8 @@ def _get_call_type(node: ast.Call, module: PythonModule, arg_type = get_type(node.args[0], containers, container) list_class = module.global_module.classes[LIST_TYPE] return GenericType(list_class, [arg_type]) + elif node.func.id == 'ClosureCall': + return get_type(node.args[0], containers, container).type_args[1] else: raise UnsupportedException(node) elif node.func.id in BUILTINS: @@ -449,4 +451,4 @@ def pairwise_supertype(t1: PythonType, t2: PythonType) -> Optional[PythonType]: return None if not t1.superclass: return pairwise_supertype(t2.superclass, t1) - return pairwise_supertype(t2, t1.superclass) \ No newline at end of file + return pairwise_supertype(t2, t1.superclass) diff --git a/src/nagini_translation/translator.py b/src/nagini_translation/translator.py index 9a12dfa05..eac935ff3 100644 --- a/src/nagini_translation/translator.py +++ b/src/nagini_translation/translator.py @@ -16,6 +16,7 @@ TranslatorConfig, ) from nagini_translation.translators.call import CallTranslator +from nagini_translation.translators.call_slots.call_slot_translator import CallSlotTranslator from nagini_translation.translators.contract import ContractTranslator from nagini_translation.translators.expression import ExpressionTranslator from nagini_translation.translators.io_operation import ( @@ -53,6 +54,8 @@ def __init__(self, jvm: JVM, source_file: str, type_info: TypeInfo, type_info, viper_ast) config.call_translator = CallTranslator(config, jvm, source_file, type_info, viper_ast) + config.call_slot_translator = CallSlotTranslator(config, jvm, source_file, + type_info, viper_ast) config.contract_translator = ContractTranslator(config, jvm, source_file, type_info, viper_ast) diff --git a/src/nagini_translation/translators/abstract.py b/src/nagini_translation/translators/abstract.py index ddfd0b1a4..f4267f44e 100644 --- a/src/nagini_translation/translators/abstract.py +++ b/src/nagini_translation/translators/abstract.py @@ -43,6 +43,7 @@ def __init__(self, translator: 'Translator'): self.expr_translator = None self.stmt_translator = None self.call_translator = None + self.call_slot_translator = None self.contract_translator = None self.perm_translator = None self.pure_translator = None @@ -333,3 +334,16 @@ def translate_args(self, target: PythonMethod, arg_nodes: List, keywords, node, ctx, implicit_receiver) + def translate_call_slot_check(self, target: PythonMethod, args: List[Expr], + formal_args: List[Expr], arg_stmts: List[Stmt], + position: 'silver.ast.Position', node: ast.AST, + ctx: Context) -> StmtsAndExpr: + return self.config.call_slot_translator.translate_call_slot_check( + target, args, formal_args, arg_stmts, position, node, ctx + ) + + def translate_call_slot_application(self, call: ast.Call, ctx: Context) -> StmtsAndExpr: + return self.config.call_slot_translator.translate_call_slot_application(call, ctx) + + def translate_call_slot_proof(self, proof: ast.FunctionDef, ctx: Context) -> List[Stmt]: + return self.config.call_slot_translator.translate_call_slot_proof(proof, ctx) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 37d0e445c..f12d1cfd5 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -20,6 +20,7 @@ TUPLE_TYPE, ) from nagini_translation.lib.program_nodes import ( + CallSlot, GenericType, MethodType, PythonClass, @@ -766,6 +767,9 @@ def translate_normal_call(self, target: PythonMethod, arg_stmts: List[Stmt], raise InvalidProgramException(node, 'invalid.contract.position') return arg_stmts, self.create_predicate_access(target_name, args, perm, node, ctx) + elif isinstance(target, CallSlot): + return self.translate_call_slot_check(target, args, formal_args, + arg_stmts, position, node, ctx) elif target.pure: return self._translate_function_call(target, args, formal_args, arg_stmts, position, node, ctx) @@ -785,6 +789,8 @@ def translate_Call(self, node: ast.Call, ctx: Context, impure=False) -> StmtsAnd if is_name: if func_name in CONTRACT_WRAPPER_FUNCS: raise InvalidProgramException(node, 'invalid.contract.position') + elif func_name == 'ClosureCall': + return self.translate_call_slot_application(node, ctx) elif func_name in CONTRACT_FUNCS: return self.translate_contractfunc_call(node, ctx, impure) elif func_name in IO_CONTRACT_FUNCS: diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py new file mode 100644 index 000000000..f0db24da4 --- /dev/null +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -0,0 +1,196 @@ +import ast +from copy import deepcopy +from typing import Dict, List, Tuple, Union +from nagini_translation.lib.context import Context +from nagini_translation.lib.program_nodes import ( + CallSlot, + CallSlotProof, + PythonMethod, + PythonVar, + TypeInfo +) +from nagini_translation.lib.typedefs import ( + Expr, + Stmt, + StmtsAndExpr +) +from nagini_translation.lib.util import UnsupportedException +from nagini_translation.translators.common import CommonTranslator +from nagini_translation.call_slot_analyzers import is_call_slot_proof + + +class CallSlotTranslator(CommonTranslator): + + def __init__(self, config: 'TranslatorConfig', jvm: 'JVM', source_file: str, + type_info: TypeInfo, viper_ast: 'ViperAST') -> None: + super().__init__(config, jvm, source_file, type_info, viper_ast) + self.arg_replacer = _ArgReplacer() + + def translate_call_slot_check( + self, target: CallSlot, args: List[Expr], + formal_args: List[Expr], arg_stmts: List[Stmt], + position: 'silver.ast.Position', node: ast.AST, + ctx: Context + ) -> StmtsAndExpr: + check = self.viper.FuncApp( + target.sil_name, + args, + position, + self.no_info(ctx), + self.viper.Bool, + formal_args + ) + return arg_stmts, check + + def translate_call_slot_application(self, closureCall: ast.Call, ctx: Context) -> StmtsAndExpr: + call, justification = closureCall.args + + assert isinstance(call, ast.Call) + assert isinstance(justification, (ast.Call, ast.Name)) + if isinstance(justification, ast.Call): + return self._application_call_slot(call, justification, ctx) + else: + return self._application_static_dispatch(call, justification, ctx) + + def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: Context) -> StmtsAndExpr: + assert isinstance(call.func, ast.Name) + assert isinstance(justification.func, ast.Call) + assert isinstance(justification.func.func, ast.Name) + + call_slot = ctx.module.call_slots[justification.func.func.id] + + stmts = self._application_call_slot_justification(justification, ctx) + + assert len(call.args) == len(justification.func.args) + arg_map = { + py_var.name: arg + for py_var, arg in zip(call_slot.get_args(), call_slot.call.args) + } + + for arg_call, arg_slot in zip(call.args, justification.func.args): + stmts.extend(self._application_call_slot_arg_match( + arg_call, arg_slot, arg_map, ctx)) + + stmts.extend(self._application_call_slot_arg_match( + call.func, call_slot.call.func, arg_map, ctx)) + + call_stmts, call_expr = self._translate_normal_call( + call_slot, call.args + justification.func.args, ctx + ) + + return stmts + call_stmts, call_expr + + def _application_call_slot_justification(self, justification: ast.Call, ctx: Context) -> List[Stmt]: + + assert isinstance(justification.func, ast.Call) # uq vars + stmts, expr = self.translate_expr(justification.func, ctx, self.viper.Bool) + return stmts + [ + self.viper.Assert(expr, self.to_position(justification.func, ctx), self.no_info(ctx)) ] + + def _application_call_slot_arg_match(self, call_arg: ast.expr, + slot_arg: ast.expr, arg_map: Dict[str, ast.expr], + ctx: Context) -> List[Stmt]: + slot_arg = deepcopy(slot_arg) + slot_arg = self.arg_replacer.replace(slot_arg, arg_map) + + call_arg_stmts, viper_call_arg = self.translate_expr(call_arg, ctx) + + slot_arg_stmts, viper_slot_arg = self.translate_expr(slot_arg, ctx) + + return call_arg_stmts + slot_arg_stmts + [ + self.viper.Assert( + self.viper.EqCmp( + viper_call_arg, + viper_slot_arg, + self.to_position(call_arg, ctx), + self.no_info(ctx) + ), + self.to_position(call_arg, ctx), + self.no_info(ctx) + ) + ] + + def _translate_normal_call(self, target: PythonMethod, args: List[ast.expr], ctx: Context) -> StmtsAndExpr: + + result_var = None + if target.return_type is not None: + result_var = ctx.current_function.create_variable( + target.name + '_res', target.return_type, self.translator).ref() + + arg_stmts: List[Stmt] = [] + arg_exprs: List[Expr] = [] + + for arg in args: + stmts, expr = self.translate_expr(arg, ctx) + arg_stmts.extend(stmts) + arg_exprs.append(expr) + + call = self.viper.MethodCall( + target.name + '_apply', arg_exprs, [result_var], + self.to_position(target.node, ctx), self.no_info(ctx) + ) + + return arg_stmts + [call], result_var + + def _application_static_dispatch(self, call: ast.Call, justification: ast.Name, ctx: Context) -> StmtsAndExpr: + # TODO: implement + assert False + + def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) -> List[Stmt]: + assert is_call_slot_proof(proof_node) + + proof = ctx.current_function.call_slot_proofs[proof_node] + vars_stmts, nv_map = self._proof_extract_vars(proof, ctx) + + body_stmts = self._proof_translate_body(proof, ctx) + + return stmts + + def _proof_extract_vars(self, proof: CallSlotProof, ctx: Context) -> Tuple[List[Stmt], Dict[str, PythonVar]]: + + vars = proof.get_args() + values = proof.call_slot_instantiation.args + + stmts: List[Stmt] = [] + vars_map: Dict[str, PythonVar] = {} + + assert len(vars) == len(values) + for var, value in zip(vars, values): + var_stmts, proof_var = self._proof_extract_var(var, value, ctx) + stmts.extend(var_stmts) + vars_map[var.name] = proof_var + + return stmts, vars_map + + def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Tuple[List[Stmt], PythonVar]: + stmts, viper_val = self.translate_expr(val, ctx) + + proof_var = ctx.current_function.create_variable( + '__proof_' + var.name, var.type, self.config.translator + ) + + stmts.append(self.viper.LocalVarAssign( + proof_var.ref(proof_var.node, ctx), + viper_val, + self.to_position(proof_var.node, ctx), + self.no_info(ctx) + )) + + return stmts, proof_var + + def _proof_translate_body(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: + pass + + def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> Dict[str, PythonVar]: + return { + } + + +class _ArgReplacer(ast.NodeTransformer): + + def replace(self, node: ast.expr, arg_map: Dict[str, ast.expr]) -> ast.expr: + self.arg_map = arg_map + return self.visit(node) + + def visit_Name(self, name: ast.Name) -> ast.expr: + return deepcopy(self.arg_map[name.id]) if name.id in self.arg_map else name diff --git a/src/nagini_translation/translators/statement.py b/src/nagini_translation/translators/statement.py index 91b24cb89..a9dd69214 100644 --- a/src/nagini_translation/translators/statement.py +++ b/src/nagini_translation/translators/statement.py @@ -39,6 +39,7 @@ ) from nagini_translation.translators.abstract import Context from nagini_translation.translators.common import CommonTranslator +from nagini_translation.call_slot_analyzers import is_call_slot_proof from typing import List, Optional, Tuple, Union @@ -625,6 +626,12 @@ def translate_stmt_If(self, node: ast.If, ctx: Context) -> List[Stmt]: return cond_stmt + [self.viper.If(cond, then_block, else_block, position, self.no_info(ctx))] + def translate_stmt_FunctionDef(self, node: ast.FunctionDef, ctx: Context) -> List[Stmt]: + if is_call_slot_proof(node): + return self.translate_call_slot_proof(node, ctx) + + return self.translate_generic(node, ctx) + def assign_to(self, lhs: ast.AST, rhs: Expr, rhs_index: Optional[int], rhs_end: Optional[Expr], rhs_type: PythonType, From e24e4c7650d23919b2becac3a78bd77ec0c980ce Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 17 Oct 2017 23:40:06 +0200 Subject: [PATCH 19/31] Fixed the static code example --- tests/closures/verification/examples/static_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/closures/verification/examples/static_code.py b/tests/closures/verification/examples/static_code.py index b7a7cf2be..5585a533c 100644 --- a/tests/closures/verification/examples/static_code.py +++ b/tests/closures/verification/examples/static_code.py @@ -122,7 +122,7 @@ def client() -> None: def func_call_slot_proof(concrete_g: G_Type, b: int, c: int) -> None: @UniversallyQuantified - def uq(a: Argument, d: int, e: int) -> None: + def uq(a: Argument) -> None: Requires(Acc(a.parameter, 1 / 2) and Acc(a.result)) Requires(a.parameter >= b + c) From 6ce2752d12cb07dc2d053b88ed5eb247bfb08dd1 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Tue, 17 Oct 2017 23:45:30 +0200 Subject: [PATCH 20/31] First version that can translate the static code example --- src/nagini_translation/analyzer.py | 2 + src/nagini_translation/call_slot_analyzers.py | 8 +- src/nagini_translation/lib/constants.py | 2 +- src/nagini_translation/lib/program_nodes.py | 65 +++- src/nagini_translation/lib/resolver.py | 2 + src/nagini_translation/lib/typeinfo.py | 12 +- .../translators/abstract.py | 4 + .../call_slots/call_slot_translator.py | 340 ++++++++++++++++-- .../translators/expression.py | 6 +- src/nagini_translation/translators/program.py | 5 + 10 files changed, 394 insertions(+), 52 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 5f08cf9bd..3ebef3275 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -537,6 +537,8 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: if not is_property_setter: func.type = self.convert_type(functype) + func.callable_type = self.convert_type( + self.module.get_func_type(func.scope_prefix, callable=True)) for child in node.body: if is_io_existential(child): diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 105588445..8527b3ad7 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -56,6 +56,7 @@ def analyze(self, node: ast.FunctionDef) -> None: body_node._parent = node + self.call_slot.body = body_node.body for child in body_node.body: self.analyzer.visit(child, body_node) @@ -69,6 +70,8 @@ def analyze(self, node: ast.FunctionDef) -> None: self.call_slot.precondition = mock_call_slot.precondition self.call_slot.postcondition = mock_call_slot.postcondition self.analyzer.outer_functions.pop() + + self.call_slot.type = self.call_slot.locals[self.call_slot.return_variables[0].id].type self.analyzer.current_function = old_current_function if old_current_function is not None: self.analyzer.outer_functions.pop() @@ -91,10 +94,10 @@ def _check_method_declaration(self, call_slot: 'CallSlotBase') -> None: if analyzer._is_illegal_magic_method_name(call_slot.node.name): raise InvalidProgramException(call_slot.node, 'illegal.magic.method') - call_slot.type = analyzer.convert_type( + _type = analyzer.convert_type( analyzer.module.get_func_type(call_slot.scope_prefix)) - if call_slot.type is not None: + if _type is not None: raise InvalidProgramException( call_slot.node, 'call_slots.return.not_none', @@ -281,7 +284,6 @@ def _check_call_declaration(self, node: Union[ast.Call, ast.Assign]) -> None: if isinstance(node.targets[0], ast.Name): self.call_slot.return_variables = [node.targets[0]] - self.call_slot.return_type = self.analyzer.typeof(node.targets[0]) else: raise UnsupportedException( node, diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index a92852560..4a68bf33b 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -186,7 +186,7 @@ PRIMITIVES = {PRIMITIVE_INT_TYPE, PRIMITIVE_BOOL_TYPE, PRIMITIVE_SEQ_TYPE, CALLABLE_TYPE} -BOXED_PRIMITIVES = {INT_TYPE, BOOL_TYPE} +BOXED_PRIMITIVES = {INT_TYPE, BOOL_TYPE, CALLABLE_TYPE} MYPY_SUPERCLASSES = { 'Sized', diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 018283b37..73bfd691f 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -146,6 +146,8 @@ def process(self, translator: 'Translator') -> None: function.process(self.get_fresh_name(name), translator) for name, method in self.methods.items(): method.process(self.get_fresh_name(name), translator) + for name, call_slot in self.call_slots.items(): + call_slot.process(self.get_fresh_name(name), translator) for name, predicate in self.predicates.items(): predicate.process(self.get_fresh_name(name), translator) for name, var in self.global_vars.items(): @@ -187,7 +189,7 @@ def get_type(self, prefixes: List[str], return module_result return None - def get_func_type(self, path: List[str]): + def get_func_type(self, path: List[str], callable=False): """ Returns the type of the function identified by the given path in the current module (including imported other modules). It is assumed that @@ -196,11 +198,11 @@ def get_func_type(self, path: List[str]): """ actual_prefix = self.type_prefix.split('.') if self.type_prefix else [] actual_prefix.extend(path) - local_result = self.types.get_func_type(actual_prefix) + local_result = self.types.get_func_type(actual_prefix, callable) if local_result is not None: return local_result for module in self.from_imports: - module_result = module.get_func_type(prefix) + module_result = module.get_func_type(prefix, callable) if module_result is not None: return module_result return None @@ -755,6 +757,7 @@ def __init__(self, name: str, node: ast.AST, cls: PythonClass, self.var_arg = None # direct self.kw_arg = None # direct self.type = None # infer + self.callable_type: PythonType = None self.generic_type = -1 self.result = None # infer self.error_var = None # infer @@ -785,8 +788,7 @@ def process(self, sil_name: str, translator: 'Translator') -> None: checks if this method overrides one from a superclass, """ self.sil_name = sil_name - if self.pure: - self.func_constant = self.superscope.get_fresh_name(self.name) + self.func_constant = self.superscope.get_fresh_name(self.name) for name, arg in self.args.items(): arg.process(self.get_fresh_name(name), translator) if self.var_arg: @@ -1624,6 +1626,7 @@ def __init__( self.uq_variables = OrderedDict() # type: Dict[str, PythonVar] # NOTE: currently we only support one single return variable self.return_variables = None # type: List[ast.Name] + self.body: List[ast.stmt] = None class CallSlot(CallSlotBase): @@ -1645,7 +1648,57 @@ def __init__( ) self.call = None # type: ast.Call - self.return_type: PythonType = None + + def process(self, sil_name: str, translator: 'Translator') -> None: + """ + Creates fresh Silver names for all parameters and initializes them, + same for local variables. Also sets the method type and + checks if this method overrides one from a superclass, + """ + self.sil_name = sil_name + for name, arg in self.args.items(): + arg.process(self.get_fresh_name(name), translator) + for name, uq_var in self.uq_variables.items(): + uq_var.process(self.get_fresh_name(name), translator) + for local in self.locals: + self.locals[local].process(self.get_fresh_name(local), translator) + self.obligation_info = translator.create_obligation_info(self) + if self.type is not None: + self.result = self.node_factory.create_python_var(RESULT_NAME, None, + self.type) + self.result.process(RESULT_NAME, translator) + + def get_contents(self, only_top: bool) -> Dict: + """ + Returns the elements that can be accessed from this container (to be + used by get_target). If 'only_top' is true, returns only top level + elements that can be accessed without a receiver. + """ + dicts = [super().get_contents(only_top), self.uq_variables] + return CombinedDict([], dicts) + + def get_variable(self, name: str) -> Optional['PythonVar']: + """ + Returns the variable (local variable or method parameter) with the + given name. + """ + + if name in self.locals: + return self.locals[name] + elif name in self.args: + return self.args[name] + elif name in self.uq_variables: + return self.uq_variables[name] + elif name in self.special_vars: + return self.special_vars[name] + elif name in self.io_existential_vars: + return self.io_existential_vars[name] + elif self.var_arg and self.var_arg.name == name: + return self.var_arg + elif self.kw_arg and self.kw_arg.name == name: + return self.kw_arg + else: + return self.module.global_vars.get(name) class CallSlotProof(CallSlotBase): diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 169b97d6d..095603a87 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -174,6 +174,8 @@ def _do_get_type(node: ast.AST, containers: List[ContainerInterface], rectype = get_type(node.func.value, containers, container) if target.generic_type != -1: return rectype.type_args[target.generic_type] + if isinstance(node, ast.Name) and not isinstance(node._parent, ast.Call): + return target.callable_type return target.type if isinstance(target, PythonField): result = target.type diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 85f86e4ae..952adc452 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -109,7 +109,7 @@ def visit_func_def(self, node: mypy.nodes.FuncDef): oldprefix = self.prefix self.prefix = self.prefix + [node.name()] functype = self.type_of(node) - self.set_type(self.prefix, functype, node.line, col(node), True) + self.set_type(self.prefix, functype, node.line, col(node)) for arg in node.arguments: self.set_type(self.prefix + [arg.variable.name()], arg.variable.type, arg.line, col(arg)) @@ -132,9 +132,7 @@ def visit_class_def(self, node: mypy.nodes.ClassDef): super().visit_class_def(node) self.prefix = oldprefix - def set_type(self, fqn, type, line, col, return_type=False): - if return_type and isinstance(type, mypy.types.CallableType): - type = type.ret_type + def set_type(self, fqn, type, line, col): if not type or isinstance(type, mypy.types.AnyType): if line in self.ignored_lines: return @@ -301,7 +299,7 @@ def get_type(self, prefix: List[str], name: str): else: return result, alts - def get_func_type(self, prefix: List[str]): + def get_func_type(self, prefix: List[str], callable=False): """ Looks up the type of the function which creates the given context """ @@ -310,9 +308,9 @@ def get_func_type(self, prefix: List[str]): if len(prefix) == 0: return None else: - return self.get_func_type(prefix[:len(prefix) - 1]) + return self.get_func_type(prefix[:len(prefix) - 1], callable) else: - if isinstance(result, mypy.types.FunctionLike): + if not callable and isinstance(result, mypy.types.FunctionLike): result = result.ret_type return result diff --git a/src/nagini_translation/translators/abstract.py b/src/nagini_translation/translators/abstract.py index f4267f44e..bbd70ca77 100644 --- a/src/nagini_translation/translators/abstract.py +++ b/src/nagini_translation/translators/abstract.py @@ -347,3 +347,7 @@ def translate_call_slot_application(self, call: ast.Call, ctx: Context) -> Stmts def translate_call_slot_proof(self, proof: ast.FunctionDef, ctx: Context) -> List[Stmt]: return self.config.call_slot_translator.translate_call_slot_proof(proof, ctx) + + def translate_call_slot(self, call_slot: 'CallSlot', ctx: Context + ) -> Tuple['silver.ast.Function', 'silver.ast.Method']: + return self.config.call_slot_translator.translate_call_slot(call_slot, ctx) diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py index f0db24da4..1b9e31bf0 100644 --- a/src/nagini_translation/translators/call_slots/call_slot_translator.py +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -1,6 +1,8 @@ import ast from copy import deepcopy -from typing import Dict, List, Tuple, Union +from functools import reduce +from itertools import chain +from typing import Dict, List, Tuple from nagini_translation.lib.context import Context from nagini_translation.lib.program_nodes import ( CallSlot, @@ -12,11 +14,20 @@ from nagini_translation.lib.typedefs import ( Expr, Stmt, - StmtsAndExpr + StmtsAndExpr, + Function, + Method ) -from nagini_translation.lib.util import UnsupportedException +from nagini_translation.lib.util import InvalidProgramException from nagini_translation.translators.common import CommonTranslator -from nagini_translation.call_slot_analyzers import is_call_slot_proof +from nagini_translation.call_slot_analyzers import ( + is_call_slot_proof, + is_precondition, + is_postcondition, + is_fold, + is_unfold, + is_assume +) class CallSlotTranslator(CommonTranslator): @@ -24,7 +35,46 @@ class CallSlotTranslator(CommonTranslator): def __init__(self, config: 'TranslatorConfig', jvm: 'JVM', source_file: str, type_info: TypeInfo, viper_ast: 'ViperAST') -> None: super().__init__(config, jvm, source_file, type_info, viper_ast) - self.arg_replacer = _ArgReplacer() + self._var_replacer = _VarReplacer() + + def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Function, Method]: + + old_function = ctx.current_function + ctx.current_function = call_slot + + position = self.to_position(call_slot.node, ctx) + info = self.no_info(ctx) + + call_slot_holds = self.viper.Function( + call_slot.name, + [arg.decl for arg in call_slot.get_args()], + self.viper.Bool, + [], + [], + None, + position, + info + ) + + call_slot_apply = self.create_method_node( + ctx, + call_slot.name + '_apply', + [arg.decl for arg in chain(call_slot.get_args(), call_slot.uq_variables.values())], + [call_slot.locals[ret.id].decl for ret in call_slot.return_variables], + [self._translate_pure_expr(pre[0], ctx, impure=True, target_type=self.viper.Bool) + for pre in call_slot.precondition], + [self._translate_pure_expr(post[0], ctx, impure=True, target_type=self.viper.Bool) + for post in call_slot.postcondition], + [], + [self.viper.Inhale(self.viper.FalseLit(position, info), position, info)], + position, + info, + method=call_slot + ) + + ctx.current_function = old_function + + return call_slot_holds, call_slot_apply def translate_call_slot_check( self, target: CallSlot, args: List[Expr], @@ -61,13 +111,14 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C stmts = self._application_call_slot_justification(justification, ctx) - assert len(call.args) == len(justification.func.args) + assert len(call_slot.get_args()) == len(justification.func.args) arg_map = { py_var.name: arg - for py_var, arg in zip(call_slot.get_args(), call_slot.call.args) + for py_var, arg in zip(call_slot.get_args(), justification.func.args) } - for arg_call, arg_slot in zip(call.args, justification.func.args): + assert len(call.args) == len(call_slot.call.args) + for arg_call, arg_slot in zip(call.args, call_slot.call.args): stmts.extend(self._application_call_slot_arg_match( arg_call, arg_slot, arg_map, ctx)) @@ -75,7 +126,7 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C call.func, call_slot.call.func, arg_map, ctx)) call_stmts, call_expr = self._translate_normal_call( - call_slot, call.args + justification.func.args, ctx + call_slot, justification.func.args + justification.args, ctx ) return stmts + call_stmts, call_expr @@ -85,13 +136,14 @@ def _application_call_slot_justification(self, justification: ast.Call, ctx: Con assert isinstance(justification.func, ast.Call) # uq vars stmts, expr = self.translate_expr(justification.func, ctx, self.viper.Bool) return stmts + [ - self.viper.Assert(expr, self.to_position(justification.func, ctx), self.no_info(ctx)) ] + self.viper.Assert(expr, self.to_position(justification.func, ctx), self.no_info(ctx)) + ] def _application_call_slot_arg_match(self, call_arg: ast.expr, slot_arg: ast.expr, arg_map: Dict[str, ast.expr], ctx: Context) -> List[Stmt]: slot_arg = deepcopy(slot_arg) - slot_arg = self.arg_replacer.replace(slot_arg, arg_map) + slot_arg = self._var_replacer.replace(slot_arg, arg_map) call_arg_stmts, viper_call_arg = self.translate_expr(call_arg, ctx) @@ -113,9 +165,9 @@ def _application_call_slot_arg_match(self, call_arg: ast.expr, def _translate_normal_call(self, target: PythonMethod, args: List[ast.expr], ctx: Context) -> StmtsAndExpr: result_var = None - if target.return_type is not None: + if target.type is not None: result_var = ctx.current_function.create_variable( - target.name + '_res', target.return_type, self.translator).ref() + target.name + '_res', target.type, self.translator).ref() arg_stmts: List[Stmt] = [] arg_exprs: List[Expr] = [] @@ -125,12 +177,12 @@ def _translate_normal_call(self, target: PythonMethod, args: List[ast.expr], ctx arg_stmts.extend(stmts) arg_exprs.append(expr) - call = self.viper.MethodCall( - target.name + '_apply', arg_exprs, [result_var], - self.to_position(target.node, ctx), self.no_info(ctx) + call = self.create_method_call_node( + ctx, target.name + '_apply', arg_exprs, [result_var] if result_var else [], + self.to_position(target.node, ctx), self.no_info(ctx), target_method=target ) - return arg_stmts + [call], result_var + return arg_stmts + call, result_var def _application_static_dispatch(self, call: ast.Call, justification: ast.Name, ctx: Context) -> StmtsAndExpr: # TODO: implement @@ -140,29 +192,84 @@ def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) - assert is_call_slot_proof(proof_node) proof = ctx.current_function.call_slot_proofs[proof_node] - vars_stmts, nv_map = self._proof_extract_vars(proof, ctx) + call_slot = self._get_call_slot(proof, ctx) + cl_map = self._get_cl_map(proof, call_slot) - body_stmts = self._proof_translate_body(proof, ctx) + with ctx.aliases_context(): + vars_stmts = self._proof_extract_vars(proof, ctx) - return stmts + body_stmts = self._proof_translate_body(proof, call_slot, cl_map, ctx) + + while_loop = self._proof_create_non_deterministic_while_loop( + proof, body_stmts, ctx + ) + # TODO: assume call slot holds + + return vars_stmts + [while_loop] - def _proof_extract_vars(self, proof: CallSlotProof, ctx: Context) -> Tuple[List[Stmt], Dict[str, PythonVar]]: + def _proof_extract_vars(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: vars = proof.get_args() values = proof.call_slot_instantiation.args stmts: List[Stmt] = [] - vars_map: Dict[str, PythonVar] = {} assert len(vars) == len(values) for var, value in zip(vars, values): - var_stmts, proof_var = self._proof_extract_var(var, value, ctx) - stmts.extend(var_stmts) - vars_map[var.name] = proof_var + stmts.extend(self._proof_extract_var(var, value, ctx)) - return stmts, vars_map + return stmts + + def _get_call_slot(self, proof: CallSlotProof, ctx: Context) -> CallSlot: + call_slot_name = proof.call_slot_instantiation.func.id + if call_slot_name not in ctx.module.call_slots: + raise InvalidProgramException( + proof.node, + 'call_slots.proof_annotation.invalid_call_slot' + ) + + return ctx.module.call_slots[call_slot_name] + + def _get_cl_map(self, proof: CallSlotProof, call_slot: CallSlot) -> Dict[str, ast.expr]: + + proof_nv = proof.args.values() + cl_nv = call_slot.args.values() + if len(proof_nv) != len(cl_nv): + raise InvalidProgramException( + proof.node, + 'call_slots.proof_annotation.invalid_call_slot' + ) + + proof_uqv = proof.uq_variables.values() + cl_uqv = call_slot.uq_variables.values() + if len(proof_uqv) != len(cl_uqv): + raise InvalidProgramException( + proof.node, + 'call_slots.proof_annotation.invalid_call_slot' + ) - def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Tuple[List[Stmt], PythonVar]: + proof_rv = [proof.locals[rv.id] for rv in proof.return_variables] + cl_rv = [call_slot.locals[rv.id] for rv in call_slot.return_variables] + if len(proof_rv) != len(cl_rv): + raise InvalidProgramException( + proof.node, + 'call_slots.proof_annotation.invalid_call_slot' + ) + + return { + cl_var.name: ast.Name( + proof_var.name, + ast.Load, + lineno=proof_var.node.lineno, + col_offset=proof_var.node.col_offset, + ) + for cl_var, proof_var in zip( + chain(cl_nv, cl_uqv, cl_rv), + chain(proof_nv, proof_uqv, proof_rv) + ) + } + + def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> List[Stmt]: stmts, viper_val = self.translate_expr(val, ctx) proof_var = ctx.current_function.create_variable( @@ -176,17 +283,182 @@ def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Tup self.no_info(ctx) )) - return stmts, proof_var + ctx.set_alias(var.name, proof_var, var) - def _proof_translate_body(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: - pass + return stmts - def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> Dict[str, PythonVar]: - return { - } + def _proof_create_non_deterministic_while_loop( + self, + proof: CallSlotProof, + body: List[Stmt], + ctx: Context + ) -> Stmt: + + non_deterministic_bool = ctx.current_function.create_variable( + '__proof_non_deterministic_choice', ctx.module.global_module.classes['bool'].try_unbox(), self.translator + ) + + position = self.to_position(proof.node, ctx) + info = self.no_info(ctx) + + return self.viper.While( + non_deterministic_bool.ref(), [], [], + self.translate_block(body, position, info), + position, info + ) + + def _proof_translate_body( + self, + proof: CallSlotProof, + call_slot: CallSlot, + cl_map: Dict[str, ast.expr], + ctx: Context + ) -> List[Stmt]: + + self._proof_introduce_uq_ret_vars(proof, ctx) + + stmts: List[Stmt] = [] + + stmts.append(self.viper.Inhale( + self._proof_translate_contract(proof, call_slot.precondition, cl_map, ctx), + self.to_position(proof.node, ctx), + self.no_info(ctx) + )) + + stmts.extend(self._proof_translate_body_only( + proof.body, + proof, + call_slot, + ctx + )) + stmts.append(self.viper.Exhale( + self._proof_translate_contract(proof, call_slot.postcondition, cl_map, ctx), + self.to_position(proof.node, ctx), + self.no_info(ctx) + )) + # TODO: check call counter == 1 + + return stmts + + def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> None: + for var in proof.uq_variables.values(): + proof_var = ctx.current_function.create_variable( + '__proof_' + var.name, var.type, self.config.translator + ) + ctx.set_alias(var.name, proof_var, var) + + if proof.return_variables: + ret_var = proof.locals[proof.return_variables[0].id] + proof_var = ctx.current_function.create_variable( + '__proof_' + ret_var.name, ret_var.type, self.config.translator + ) + ctx.set_alias(ret_var.name, proof_var, ret_var) -class _ArgReplacer(ast.NodeTransformer): + def _proof_translate_contract( + self, + proof: CallSlotProof, + contract: List[Tuple[ast.expr, Dict]], + cl_map: Dict[str, ast.expr], + ctx: Context + ) -> Expr: + + position = self.to_position(proof.node, ctx) + info = self.no_info(ctx) + + contract = [ + self._translate_pure_expr(self._var_replacer.replace(pre[0], cl_map), + ctx, impure=True, target_type=self.viper.Bool) + for pre in contract + ] + + return reduce( + lambda left, right: self.viper.And(left, right, position, info), + contract + ) + + def _proof_translate_body_only( + self, + body: List[ast.stmt], + proof: CallSlotProof, + call_slot: CallSlot, + ctx: Context + ) -> List[Stmt]: + + viper_stmts: List[Stmt] = [] + + for stmt in body: + + # NOTE: for ClosureCall and nested proofs we could add a + # call_slot_proof & call_slot field to the Context class + # then we can add additional checks where they're handled + + if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call): + if is_precondition(stmt.value) or is_postcondition(stmt.value): + continue # ignore + if is_fold(stmt.value) or is_unfold(stmt.value) or is_assume(stmt.value): + viper_stmts.extend(self.translate_stmt(stmt, ctx)) + + # TODO: ClosureCall + # TODO: check purity of closure call arguments + # TODO: check proof call matches call slot call + # TODO: update 'call counter' + elif isinstance(stmt, ast.Assign): + # TODO: ClosureCall + # TODO: check purity of closure call arguments + # TODO: check proof call matches call slot call + # TODO: update 'call counter' + continue + + elif isinstance(stmt, ast.Assert): + viper_stmts.extend(self.translate_stmt(stmt, ctx)) + + elif isinstance(stmt, ast.FunctionDef): + assert is_call_slot_proof(stmt) + # TODO: nested call slot proof + # TODO: check purity of call slot instantiation + + elif isinstance(stmt, ast.If): + position = self.to_position(stmt, ctx) + info = self.no_info(ctx) + + cond = self._translate_pure_expr( + stmt.test, ctx, target_type=self.viper.Bool + ) + + then_body = self._proof_translate_body_only( + stmt.body, proof, call_slot, ctx + ) + then_block = self.translate_block(then_body, position, info) + + else_body = self._proof_translate_body_only( + stmt.orelse, proof, call_slot, ctx + ) + else_block = self.translate_block(else_body, position, info) + + viper_stmts.append(self.viper.If( + cond, then_block, else_block, position, info + )) + + else: + assert False + + return viper_stmts + + def _translate_pure_expr( + self, + node: ast.expr, + ctx: Context, + target_type: object = None, + impure: bool = False + ) -> Expr: + stmts, expr = self.translate_expr(node, ctx, target_type=target_type, impure=impure) + if stmts: + raise InvalidProgramException(node, 'purity.violated') + return expr + + +class _VarReplacer(ast.NodeTransformer): def replace(self, node: ast.expr, arg_map: Dict[str, ast.expr]) -> ast.expr: self.arg_map = arg_map diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 05f43a7e5..76d32887a 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -10,6 +10,7 @@ END_LABEL, FUNCTION_DOMAIN_NAME, INT_TYPE, + CALLABLE_TYPE, LIST_TYPE, OPERATOR_FUNCTIONS, PRIMITIVE_INT_TYPE, @@ -574,7 +575,8 @@ def translate_Name(self, node: ast.Name, ctx: Context) -> StmtsAndExpr: if (isinstance(ctx.actual_function, PythonMethod) and not (ctx.actual_function.pure or ctx.actual_function.predicate) and not isinstance(node.ctx, ast.Store) and - self.is_local_variable(var, ctx)): + self.is_local_variable(var, ctx) and + var.type.name != 'Callable'): result = self.wrap_definedness_check(var, node, ctx) else: result = var.ref(node, ctx) @@ -760,6 +762,8 @@ def _translate_primitive_operation(self, left: Expr, right: Expr, op = self._primitive_operations[type(op)] if op_type.python_class.try_box().name == INT_TYPE: wrap = self.to_int + elif op_type.python_class.try_box().name == CALLABLE_TYPE: + wrap = lambda node, ctx: node else: wrap = self.to_bool result = op(wrap(left, ctx), wrap(right, ctx), pos, self.no_info(ctx)) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 0794a22b9..fe0845d6d 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -655,9 +655,14 @@ def translate_program(self, modules: List[PythonModule], for method in module.methods.values(): self.track_dependencies(selected_names, selected, method, ctx) methods.append(self.translate_method(method, ctx)) + func_constants.append(self.translate_function_constant(method, ctx)) for pred in module.predicates.values(): self.track_dependencies(selected_names, selected, pred, ctx) predicates.append(self.translate_predicate(pred, ctx)) + for call_slot in module.call_slots.values(): + call_slot_holds, call_slot_apply = self.translate_call_slot(call_slot, ctx) + functions.append(call_slot_holds) + methods.append(call_slot_apply) for class_name, cls in module.classes.items(): if class_name in PRIMITIVES or class_name != cls.name: # Skip primitives and type variable entries. From cf8a64e9a085a8b50398cdbdedb640b5521c25d6 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 06:37:48 +0200 Subject: [PATCH 21/31] Finished minimal implementation for static code example --- src/nagini_translation/analyzer.py | 8 +- src/nagini_translation/lib/program_nodes.py | 8 +- src/nagini_translation/lib/resolver.py | 2 +- .../call_slots/call_slot_translator.py | 299 ++++++++++++++---- src/nagini_translation/translators/common.py | 7 + .../verification/examples/static_code.py | 2 +- 6 files changed, 246 insertions(+), 80 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 3ebef3275..c3a3219d4 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -992,10 +992,10 @@ def _convert_normal_type(self, mypy_type) -> PythonType: def _convert_callable_type(self, mypy_type, node) -> PythonType: return GenericType( self.find_or_create_class(CALLABLE_TYPE, module=self.module.global_module), - [ - [self.convert_type(arg_type, node) for arg_type in mypy_type.arg_types], - self.convert_type(mypy_type.ret_type, node) - ] + ( + [self.convert_type(arg_type, node) for arg_type in mypy_type.arg_types] + + [self.convert_type(mypy_type.ret_type, node)] + ) ) def _convert_union_type(self, mypy_type, node) -> PythonType: diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 73bfd691f..406a23ec2 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1648,6 +1648,7 @@ def __init__( ) self.call = None # type: ast.Call + self.sil_application_name: str = None def process(self, sil_name: str, translator: 'Translator') -> None: """ @@ -1656,6 +1657,7 @@ def process(self, sil_name: str, translator: 'Translator') -> None: checks if this method overrides one from a superclass, """ self.sil_name = sil_name + self.sil_application_name = self.get_fresh_name(self.name + '_apply') for name, arg in self.args.items(): arg.process(self.get_fresh_name(name), translator) for name, uq_var in self.uq_variables.items(): @@ -1663,10 +1665,8 @@ def process(self, sil_name: str, translator: 'Translator') -> None: for local in self.locals: self.locals[local].process(self.get_fresh_name(local), translator) self.obligation_info = translator.create_obligation_info(self) - if self.type is not None: - self.result = self.node_factory.create_python_var(RESULT_NAME, None, - self.type) - self.result.process(RESULT_NAME, translator) + if self.type is not None and self.return_variables: + self.result = self.locals[self.return_variables[0].id] def get_contents(self, only_top: bool) -> Dict: """ diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 095603a87..cc8316486 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -358,7 +358,7 @@ def _get_call_type(node: ast.Call, module: PythonModule, list_class = module.global_module.classes[LIST_TYPE] return GenericType(list_class, [arg_type]) elif node.func.id == 'ClosureCall': - return get_type(node.args[0], containers, container).type_args[1] + return get_type(node.args[0], containers, container).type_args[-1] else: raise UnsupportedException(node) elif node.func.id in BUILTINS: diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py index 1b9e31bf0..9e901b6cb 100644 --- a/src/nagini_translation/translators/call_slots/call_slot_translator.py +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -3,6 +3,7 @@ from functools import reduce from itertools import chain from typing import Dict, List, Tuple +from nagini_translation.lib.constants import ERROR_NAME, PRIMITIVES from nagini_translation.lib.context import Context from nagini_translation.lib.program_nodes import ( CallSlot, @@ -22,6 +23,7 @@ from nagini_translation.translators.common import CommonTranslator from nagini_translation.call_slot_analyzers import ( is_call_slot_proof, + is_closure_call, is_precondition, is_postcondition, is_fold, @@ -46,7 +48,7 @@ def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Functi info = self.no_info(ctx) call_slot_holds = self.viper.Function( - call_slot.name, + call_slot.sil_name, [arg.decl for arg in call_slot.get_args()], self.viper.Bool, [], @@ -56,15 +58,14 @@ def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Functi info ) + pres, posts = self.extract_contract(call_slot, ERROR_NAME, False, ctx) call_slot_apply = self.create_method_node( ctx, - call_slot.name + '_apply', + call_slot.sil_application_name, [arg.decl for arg in chain(call_slot.get_args(), call_slot.uq_variables.values())], - [call_slot.locals[ret.id].decl for ret in call_slot.return_variables], - [self._translate_pure_expr(pre[0], ctx, impure=True, target_type=self.viper.Bool) - for pre in call_slot.precondition], - [self._translate_pure_expr(post[0], ctx, impure=True, target_type=self.viper.Bool) - for post in call_slot.postcondition], + [res.decl for res in call_slot.get_results()], + pres, + posts, [], [self.viper.Inhale(self.viper.FalseLit(position, info), position, info)], position, @@ -109,7 +110,7 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C call_slot = ctx.module.call_slots[justification.func.func.id] - stmts = self._application_call_slot_justification(justification, ctx) + stmts = [self._application_call_slot_justification(call_slot, justification, ctx)] assert len(call_slot.get_args()) == len(justification.func.args) arg_map = { @@ -119,50 +120,67 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C assert len(call.args) == len(call_slot.call.args) for arg_call, arg_slot in zip(call.args, call_slot.call.args): - stmts.extend(self._application_call_slot_arg_match( + stmts.append(self._application_call_slot_arg_match( arg_call, arg_slot, arg_map, ctx)) - stmts.extend(self._application_call_slot_arg_match( + stmts.append(self._application_call_slot_arg_match( call.func, call_slot.call.func, arg_map, ctx)) call_stmts, call_expr = self._translate_normal_call( - call_slot, justification.func.args + justification.args, ctx + call_slot.sil_application_name, call_slot, + justification.func.args + justification.args, ctx, pure_args=True ) return stmts + call_stmts, call_expr - def _application_call_slot_justification(self, justification: ast.Call, ctx: Context) -> List[Stmt]: + def _application_call_slot_justification( + self, + call_slot: CallSlot, + justification: ast.Call, + ctx: Context + ) -> Stmt: assert isinstance(justification.func, ast.Call) # uq vars - stmts, expr = self.translate_expr(justification.func, ctx, self.viper.Bool) - return stmts + [ - self.viper.Assert(expr, self.to_position(justification.func, ctx), self.no_info(ctx)) - ] + justification = deepcopy(justification) + + justification.func.id = call_slot.sil_name + expr = self._translate_pure_expr(justification.func, ctx, target_type=self.viper.Bool) + return self.viper.Assert(expr, self.to_position(justification.func, ctx), self.no_info(ctx)) + + def _application_call_slot_arg_match( + self, + call_arg: ast.expr, + slot_arg: ast.expr, + arg_map: Dict[str, ast.expr], + ctx: Context + ) -> Stmt: - def _application_call_slot_arg_match(self, call_arg: ast.expr, - slot_arg: ast.expr, arg_map: Dict[str, ast.expr], - ctx: Context) -> List[Stmt]: slot_arg = deepcopy(slot_arg) slot_arg = self._var_replacer.replace(slot_arg, arg_map) - call_arg_stmts, viper_call_arg = self.translate_expr(call_arg, ctx) + viper_call_arg = self._translate_pure_expr(call_arg, ctx) - slot_arg_stmts, viper_slot_arg = self.translate_expr(slot_arg, ctx) + viper_slot_arg = self._translate_pure_expr(slot_arg, ctx) - return call_arg_stmts + slot_arg_stmts + [ - self.viper.Assert( - self.viper.EqCmp( - viper_call_arg, - viper_slot_arg, - self.to_position(call_arg, ctx), - self.no_info(ctx) - ), + return self.viper.Assert( + self.viper.EqCmp( + viper_call_arg, + viper_slot_arg, self.to_position(call_arg, ctx), self.no_info(ctx) - ) - ] + ), + self.to_position(call_arg, ctx), + self.no_info(ctx) + ) - def _translate_normal_call(self, target: PythonMethod, args: List[ast.expr], ctx: Context) -> StmtsAndExpr: + def _translate_normal_call( + self, + name, + target: PythonMethod, + args: List[ast.expr], + ctx: Context, + pure_args=False + ) -> StmtsAndExpr: result_var = None if target.type is not None: @@ -173,20 +191,51 @@ def _translate_normal_call(self, target: PythonMethod, args: List[ast.expr], ctx arg_exprs: List[Expr] = [] for arg in args: - stmts, expr = self.translate_expr(arg, ctx) - arg_stmts.extend(stmts) - arg_exprs.append(expr) + if pure_args: + expr = self._translate_pure_expr(arg, ctx) + arg_exprs.append(expr) + else: + stmts, expr = self.translate_expr(arg, ctx) + arg_stmts.extend(stmts) + arg_exprs.append(expr) + call = self.create_method_call_node( - ctx, target.name + '_apply', arg_exprs, [result_var] if result_var else [], + ctx, name, arg_exprs, [result_var] if result_var else [], self.to_position(target.node, ctx), self.no_info(ctx), target_method=target ) return arg_stmts + call, result_var def _application_static_dispatch(self, call: ast.Call, justification: ast.Name, ctx: Context) -> StmtsAndExpr: - # TODO: implement - assert False + + stmts: List[Stmt] = [] + position = self.to_position(call, ctx) + info = self.no_info(ctx) + + closure_expr = self._translate_pure_expr(call.func, ctx) + + justification_expr = self._translate_pure_expr(justification, ctx) + + stmts.append(self.viper.Assert( + self.viper.EqCmp( + closure_expr, + justification_expr, + position, + info + ), + position, + info + )) + + method = ctx.module.get_func_or_method(justification.id) + + call_stmts, call_expr = self._translate_normal_call( + method.sil_name, method, call.args, ctx, pure_args=True + ) + stmts.extend(call_stmts) + + return stmts, call_expr def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) -> List[Stmt]: assert is_call_slot_proof(proof_node) @@ -203,9 +252,18 @@ def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) - while_loop = self._proof_create_non_deterministic_while_loop( proof, body_stmts, ctx ) - # TODO: assume call slot holds - return vars_stmts + [while_loop] + instantiation = deepcopy(proof.call_slot_instantiation) + instantiation.func.id = call_slot.sil_name + + instantiation_expr = self._translate_pure_expr( + instantiation, ctx, target_type=self.viper.Bool) + + instantiation_stmt = self.viper.Inhale( + instantiation_expr, self.to_position(instantiation, ctx), self.no_info(ctx) + ) + + return vars_stmts + [while_loop] + [instantiation_stmt] def _proof_extract_vars(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: @@ -269,13 +327,30 @@ def _get_cl_map(self, proof: CallSlotProof, call_slot: CallSlot) -> Dict[str, as ) } - def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> List[Stmt]: - stmts, viper_val = self.translate_expr(val, ctx) + def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Stmt: + viper_val = self._translate_pure_expr(val, ctx) proof_var = ctx.current_function.create_variable( '__proof_' + var.name, var.type, self.config.translator ) + stmts: List[Stmt] = [] + position = self.to_position(val, ctx) + info = self.no_info(ctx) + stmts.append(self.set_var_defined( + proof_var, position, info)) + + if proof_var.type.name not in PRIMITIVES: + stmts.append(self.viper.Inhale( + self.var_type_check( + proof_var.name, proof_var.type, position, ctx + ), + position, + info + )) + + ctx.set_alias(var.name, proof_var, var) + stmts.append(self.viper.LocalVarAssign( proof_var.ref(proof_var.node, ctx), viper_val, @@ -283,8 +358,6 @@ def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Lis self.no_info(ctx) )) - ctx.set_alias(var.name, proof_var, var) - return stmts def _proof_create_non_deterministic_while_loop( @@ -315,46 +388,90 @@ def _proof_translate_body( ctx: Context ) -> List[Stmt]: - self._proof_introduce_uq_ret_vars(proof, ctx) - stmts: List[Stmt] = [] + position = self.to_position(proof.node, ctx) + info = self.no_info(ctx) + + stmts.extend(self._proof_introduce_uq_ret_vars(proof, ctx)) stmts.append(self.viper.Inhale( self._proof_translate_contract(proof, call_slot.precondition, cl_map, ctx), - self.to_position(proof.node, ctx), - self.no_info(ctx) + position, + info + )) + + call_counter = ctx.current_function.create_variable( + '__proof_call_counter', ctx.module.global_module.classes['int'].try_unbox(), self.translator + ) + + stmts.append(self.viper.LocalVarAssign( + call_counter.ref(), + self.viper.IntLit(0, position, info), + position, + info )) stmts.extend(self._proof_translate_body_only( proof.body, proof, call_slot, + call_counter, + cl_map, ctx )) stmts.append(self.viper.Exhale( self._proof_translate_contract(proof, call_slot.postcondition, cl_map, ctx), - self.to_position(proof.node, ctx), - self.no_info(ctx) + position, + info + )) + + stmts.append(self.viper.Assert( + self.viper.EqCmp( + call_counter.ref(), + self.viper.IntLit(1, position, info), + position, + info + ), + position, + info )) - # TODO: check call counter == 1 return stmts - def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> None: + def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: + stmts: List[Stmt] = [] + for var in proof.uq_variables.values(): proof_var = ctx.current_function.create_variable( - '__proof_' + var.name, var.type, self.config.translator + '__proof_' + var.name, var.type, self.translator ) + + position = self.to_position(proof_var.node, ctx) + info = self.no_info(ctx) + + stmts.append(self.set_var_defined(proof_var, position, info)) + + if proof_var.type.name not in PRIMITIVES: + stmts.append(self.viper.Inhale( + self.var_type_check( + proof_var.name, proof_var.type, position, ctx + ), + position, + info + )) + ctx.set_alias(var.name, proof_var, var) if proof.return_variables: ret_var = proof.locals[proof.return_variables[0].id] proof_var = ctx.current_function.create_variable( - '__proof_' + ret_var.name, ret_var.type, self.config.translator + '__proof_' + ret_var.name, ret_var.type, self.translator ) ctx.set_alias(ret_var.name, proof_var, ret_var) + return stmts + def _proof_translate_contract( self, proof: CallSlotProof, @@ -382,6 +499,8 @@ def _proof_translate_body_only( body: List[ast.stmt], proof: CallSlotProof, call_slot: CallSlot, + call_counter: PythonVar, + cl_map: Dict[str, ast.expr], ctx: Context ) -> List[Stmt]: @@ -389,25 +508,66 @@ def _proof_translate_body_only( for stmt in body: - # NOTE: for ClosureCall and nested proofs we could add a - # call_slot_proof & call_slot field to the Context class - # then we can add additional checks where they're handled - if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call): if is_precondition(stmt.value) or is_postcondition(stmt.value): continue # ignore if is_fold(stmt.value) or is_unfold(stmt.value) or is_assume(stmt.value): viper_stmts.extend(self.translate_stmt(stmt, ctx)) - # TODO: ClosureCall - # TODO: check purity of closure call arguments - # TODO: check proof call matches call slot call - # TODO: update 'call counter' + assert is_closure_call(stmt.value) + + for arg_call, arg_slot in zip(stmt.value.args[0].args, call_slot.call.args): + viper_stmts.append(self._application_call_slot_arg_match( + arg_call, arg_slot, cl_map, ctx + )) + viper_stmts.append(self._application_call_slot_arg_match( + stmt.value.args[0].func, call_slot.call.func, cl_map, ctx + )) + + viper_stmts.extend(self.translate_stmt(stmt, ctx)) + + position = self.to_position(stmt, ctx) + info = self.no_info(ctx) + viper_stmts.append(self.viper.LocalVarAssign( + call_counter.ref(), + self.viper.Add( + call_counter.ref(), + self.viper.IntLit(1, position, info), + position, + info + ), + position, + info + )) + elif isinstance(stmt, ast.Assign): - # TODO: ClosureCall - # TODO: check purity of closure call arguments - # TODO: check proof call matches call slot call - # TODO: update 'call counter' + + assert is_closure_call(stmt.value) + + for arg_call, arg_slot in zip(stmt.value.args[0].args, call_slot.call.args): + viper_stmts.append(self._application_call_slot_arg_match( + arg_call, arg_slot, cl_map, ctx + )) + viper_stmts.append(self._application_call_slot_arg_match( + stmt.value.args[0].func, call_slot.call.func, cl_map, ctx + )) + + viper_stmts.extend(self.translate_stmt(stmt, ctx)) + + position = self.to_position(stmt, ctx) + info = self.no_info(ctx) + viper_stmts.append(self.viper.LocalVarAssign( + call_counter.ref(), + self.viper.Add( + call_counter.ref(), + self.viper.IntLit(1, position, info), + position, + info + ), + position, + info + )) + continue elif isinstance(stmt, ast.Assert): @@ -415,8 +575,7 @@ def _proof_translate_body_only( elif isinstance(stmt, ast.FunctionDef): assert is_call_slot_proof(stmt) - # TODO: nested call slot proof - # TODO: check purity of call slot instantiation + viper_stmts.extend(self.translate_stmt(stmt, ctx)) elif isinstance(stmt, ast.If): position = self.to_position(stmt, ctx) @@ -427,12 +586,12 @@ def _proof_translate_body_only( ) then_body = self._proof_translate_body_only( - stmt.body, proof, call_slot, ctx + stmt.body, proof, call_slot, call_counter, cl_map, ctx ) then_block = self.translate_block(then_body, position, info) else_body = self._proof_translate_body_only( - stmt.orelse, proof, call_slot, ctx + stmt.orelse, proof, call_slot, call_counter, cl_map, ctx ) else_block = self.translate_block(else_body, position, info) diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 9a7bf1192..3dcd59eab 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -20,6 +20,7 @@ PythonModule, PythonType, PythonVar, + CallSlot, ) from nagini_translation.lib.resolver import get_target as do_get_target from nagini_translation.lib.typedefs import ( @@ -225,6 +226,12 @@ def is_local_variable(self, var: PythonVar, ctx: Context) -> bool: """ if var.name in ctx.actual_function.args: return False + if ( + isinstance(ctx.actual_function, CallSlot) and + ctx.actual_function.result and + var.name == ctx.actual_function.result.name + ): + return False return var in ctx.actual_function.locals.values() def get_may_set_predicate(self, rec: Expr, field: PythonField, ctx: Context, diff --git a/tests/closures/verification/examples/static_code.py b/tests/closures/verification/examples/static_code.py index 5585a533c..4f712cf52 100644 --- a/tests/closures/verification/examples/static_code.py +++ b/tests/closures/verification/examples/static_code.py @@ -31,7 +31,7 @@ def log(x: int) -> int: def choice() -> bool: - pass + return True class Argument: From 11afaebfe49f08e10e4d67880672f56534867382 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 08:37:40 +0200 Subject: [PATCH 22/31] Implemented Old expressions --- src/nagini_translation/call_slot_analyzers.py | 8 ++++++-- src/nagini_translation/lib/context.py | 2 ++ src/nagini_translation/lib/program_nodes.py | 10 +++++++--- src/nagini_translation/lib/viper_ast.py | 3 +++ .../translators/call_slots/call_slot_translator.py | 9 ++++++++- src/nagini_translation/translators/contract.py | 11 +++++++++-- 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 8527b3ad7..eac347ffd 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -59,6 +59,8 @@ def analyze(self, node: ast.FunctionDef) -> None: self.call_slot.body = body_node.body for child in body_node.body: self.analyzer.visit(child, body_node) + for decorator in node.decorator_list: + self.analyzer.visit(decorator, node) self._check_body(body_node.body) @@ -71,7 +73,8 @@ def analyze(self, node: ast.FunctionDef) -> None: self.call_slot.postcondition = mock_call_slot.postcondition self.analyzer.outer_functions.pop() - self.call_slot.type = self.call_slot.locals[self.call_slot.return_variables[0].id].type + if self.call_slot.return_variables: + self.call_slot.type = self.call_slot.locals[self.call_slot.return_variables[0].id].type self.analyzer.current_function = old_current_function if old_current_function is not None: self.analyzer.outer_functions.pop() @@ -360,7 +363,8 @@ def _pre_process(self, node: ast.FunctionDef) -> None: node, self.analyzer.current_function, self.analyzer.node_factory, - call_slot_instantiation + call_slot_instantiation, + self.analyzer.current_function.get_fresh_name('__proof_old_label') ) self.analyzer.current_function.call_slot_proofs[node] = self.call_slot diff --git a/src/nagini_translation/lib/context.py b/src/nagini_translation/lib/context.py index ddd636354..cef3db4cf 100644 --- a/src/nagini_translation/lib/context.py +++ b/src/nagini_translation/lib/context.py @@ -6,6 +6,7 @@ PythonMethod, PythonVar, PythonVarBase, + CallSlotProof, ) from nagini_translation.lib.typedefs import Expr from typing import Dict, List @@ -36,6 +37,7 @@ def __init__(self) -> None: self._current_alias_context = [] self.bound_type_vars = {} self._global_counter = 0 + self.current_call_slot_proof: CallSlotProof = None def get_fresh_int(self) -> int: """ diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 406a23ec2..9cfd2243e 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1572,14 +1572,16 @@ def create_call_slot_proof( node: ast.FunctionDef, superscope: PythonScope, container_factory: 'ProgramNodeFactory', - call_slot_instantiation: ast.Call + call_slot_instantiation: ast.Call, + old_label: str ) -> 'CallSlotProof': return CallSlotProof( name, node, superscope, container_factory, - call_slot_instantiation + call_slot_instantiation, + old_label ) def create_python_io_operation(self, name: str, node: ast.AST, @@ -1709,7 +1711,8 @@ def __init__( node: ast.FunctionDef, superscope: PythonScope, node_factory: 'ProgramNodeFactory', - call_slot_instantiation: ast.Call + call_slot_instantiation: ast.Call, + old_label: str ) -> None: CallSlotBase.__init__( @@ -1721,3 +1724,4 @@ def __init__( ) self.call_slot_instantiation = call_slot_instantiation + self.old_label: str = old_label diff --git a/src/nagini_translation/lib/viper_ast.py b/src/nagini_translation/lib/viper_ast.py index 78c1ea765..125a2a452 100644 --- a/src/nagini_translation/lib/viper_ast.py +++ b/src/nagini_translation/lib/viper_ast.py @@ -245,6 +245,9 @@ def FieldAccessPredicate(self, fieldacc, perm, position, info): def Old(self, expr, position, info): return self.ast.Old(expr, position, info, self.NoTrafos) + def LabelledOld(self, expr, label, position, info): + return self.ast.LabelledOld(expr, label, position, info, self.NoTrafos) + def Inhale(self, expr, position, info): return self.ast.Inhale(expr, position, info, self.NoTrafos) diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py index 9e901b6cb..e4d9b520d 100644 --- a/src/nagini_translation/translators/call_slots/call_slot_translator.py +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -241,6 +241,9 @@ def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) - assert is_call_slot_proof(proof_node) proof = ctx.current_function.call_slot_proofs[proof_node] + old_proof = ctx.current_call_slot_proof + ctx.current_call_slot_proof = proof + call_slot = self._get_call_slot(proof, ctx) cl_map = self._get_cl_map(proof, call_slot) @@ -263,6 +266,8 @@ def translate_call_slot_proof(self, proof_node: ast.FunctionDef, ctx: Context) - instantiation_expr, self.to_position(instantiation, ctx), self.no_info(ctx) ) + ctx.current_call_slot_proof = old_proof + return vars_stmts + [while_loop] + [instantiation_stmt] def _proof_extract_vars(self, proof: CallSlotProof, ctx: Context) -> List[Stmt]: @@ -400,6 +405,8 @@ def _proof_translate_body( info )) + stmts.append(self.viper.Label(proof.old_label, position, info)) + call_counter = ctx.current_function.create_variable( '__proof_call_counter', ctx.module.global_module.classes['int'].try_unbox(), self.translator ) @@ -491,7 +498,7 @@ def _proof_translate_contract( return reduce( lambda left, right: self.viper.And(left, right, position, info), - contract + contract, self.viper.TrueLit(position, info) ) def _proof_translate_body_only( diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 696785a72..7aa8ba735 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -280,8 +280,15 @@ def translate_old(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: if len(node.args) != 1: raise InvalidProgramException(node, 'invalid.contract.call') stmt, exp = self.translate_expr(node.args[0], ctx) - res = self.viper.Old(exp, self.to_position(node, ctx), - self.no_info(ctx)) + + if not ctx.current_call_slot_proof: + res = self.viper.Old(exp, self.to_position(node, ctx), + self.no_info(ctx)) + else: + res = self.viper.LabelledOld( + exp, ctx.current_call_slot_proof.old_label, + self.to_position(node, ctx), self.no_info(ctx) + ) return stmt, res def translate_fold(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: From a0ce593091f7fcebf805d1f79b56d39b951b480e Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 08:39:12 +0200 Subject: [PATCH 23/31] Activated all tests and added basic old expressions test --- .../test_method_in_contract.py | 2 - .../call_slot_proof/test_double.py | 8 +-- .../call_slot_proof/test_simple.py | 4 -- .../call_slot_proof/test_simple_no_uq.py | 5 -- .../translation/closure_call/test_simple.py | 4 -- .../verification/examples/static_code.py | 2 - tests/closures/verification/test_old.py | 71 +++++++++++++++++++ 7 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 tests/closures/verification/test_old.py diff --git a/tests/closures/translation/call_slot_declaration/test_method_in_contract.py b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py index 7238c8a96..349f0a71d 100644 --- a/tests/closures/translation/call_slot_declaration/test_method_in_contract.py +++ b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py @@ -14,8 +14,6 @@ def call_slot(f: Callable[[int, int], int], arg: 'Arg') -> None: @UniversallyQuantified def uq(y: int) -> None: - #:: IgnoreFile(42) - # will be supported during translation #:: ExpectedOutput(invalid.program:purity.violated) Requires(is_arg(arg) and y > 0) z = f(arg.val, y) diff --git a/tests/closures/translation/call_slot_proof/test_double.py b/tests/closures/translation/call_slot_proof/test_double.py index a63e58fad..9ee2e7bb4 100644 --- a/tests/closures/translation/call_slot_proof/test_double.py +++ b/tests/closures/translation/call_slot_proof/test_double.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Optional from nagini_contracts.contracts import ( CallSlot, CallSlotProof, @@ -13,11 +13,11 @@ def add(x: int, y: int) -> int: return x + y -def mul(x: int, y: int) -> int: +def mul(x: int, y: int) -> Optional[int]: return x * y -F_Type = Callable[[int, int], None] +F_Type = Callable[[int, int], Optional[int]] @CallSlot @@ -34,8 +34,6 @@ def method() -> None: x = 1 f = add - #:: IgnoreFile(42) - # Implementation not far enough yet @CallSlotProof(call_slot(f, x)) def call_slot_proof1(f: F_Type, x: int) -> None: diff --git a/tests/closures/translation/call_slot_proof/test_simple.py b/tests/closures/translation/call_slot_proof/test_simple.py index 61c2932eb..ecf5197a2 100644 --- a/tests/closures/translation/call_slot_proof/test_simple.py +++ b/tests/closures/translation/call_slot_proof/test_simple.py @@ -20,10 +20,6 @@ def mul(x: int, y: int) -> int: F_Type = Callable[[int, int], int] -#:: IgnoreFile(42) -# Call slot proof support isn't implemented enough at the moment. - - @CallSlot def call_slot(f: F_Type, x: int) -> None: diff --git a/tests/closures/translation/call_slot_proof/test_simple_no_uq.py b/tests/closures/translation/call_slot_proof/test_simple_no_uq.py index 55100d78c..86b01d8c3 100644 --- a/tests/closures/translation/call_slot_proof/test_simple_no_uq.py +++ b/tests/closures/translation/call_slot_proof/test_simple_no_uq.py @@ -2,7 +2,6 @@ from nagini_contracts.contracts import ( CallSlot, CallSlotProof, - UniversallyQuantified, ClosureCall, Requires, Ensures @@ -20,10 +19,6 @@ def twice(x: int) -> int: F_Type = Callable[[int], int] -#:: IgnoreFile(42) -# Call slot proof support isn't implemented enough at the moment. - - @CallSlot def call_slot(f: F_Type, x: int) -> None: diff --git a/tests/closures/translation/closure_call/test_simple.py b/tests/closures/translation/closure_call/test_simple.py index e63a939d4..cbb3bc5a2 100644 --- a/tests/closures/translation/closure_call/test_simple.py +++ b/tests/closures/translation/closure_call/test_simple.py @@ -20,10 +20,6 @@ def mul(x: int, y: int) -> int: F_Type = Callable[[int, int], int] -#:: IgnoreFile(42) -# Call slot proof support isn't implemented enough at the moment. - - @CallSlot def call_slot(f: F_Type, x: int) -> None: diff --git a/tests/closures/verification/examples/static_code.py b/tests/closures/verification/examples/static_code.py index 4f712cf52..58a092ed6 100644 --- a/tests/closures/verification/examples/static_code.py +++ b/tests/closures/verification/examples/static_code.py @@ -12,8 +12,6 @@ ClosureCall, ) -#:: IgnoreFile(42) -# Call slot support isn't implemented enough at the moment. @Pure def idiv(x: int, y: int) -> int: diff --git a/tests/closures/verification/test_old.py b/tests/closures/verification/test_old.py new file mode 100644 index 000000000..4e98d2c03 --- /dev/null +++ b/tests/closures/verification/test_old.py @@ -0,0 +1,71 @@ +from typing import Callable, Optional +from nagini_contracts.contracts import ( + Requires, + Ensures, + Predicate, + Acc, + Old, + CallSlot, + CallSlotProof, + ClosureCall, +) + + +class Argument: + + def __init__(self, parameter: int, result: int) -> None: + self.parameter = parameter # type: int + self.result = result # type: int + + Ensures(Acc(self.parameter) and Acc(self.result)) + Ensures(self.parameter == parameter and self.result == result) + + +inc_type = Callable[[Argument], Optional[object]] + + +def inc(arg: Argument) -> Optional[object]: + Requires(Acc(arg.parameter) and Acc(arg.result)) + + Ensures(Acc(arg.parameter) and Acc(arg.result)) + Ensures(arg.result == Old(arg.result) + arg.parameter) + Ensures(arg.parameter == Old(arg.parameter)) + + arg.result = arg.result + arg.parameter + + return None + + +@CallSlot +def inc_call_slot(f: inc_type, arg: Argument) -> None: + Requires(Acc(arg.parameter) and Acc(arg.result)) + + f(arg) + + Ensures(Acc(arg.parameter) and Acc(arg.result)) + Ensures(arg.result >= Old(arg.result) + arg.parameter) + Ensures(arg.parameter == Old(arg.parameter)) + + +def test() -> None: + + arg = Argument(1, 2) + + arg.result = 20 + arg.parameter = 50 + + f = inc + + @CallSlotProof(inc_call_slot(inc, arg)) + def inc_proof(f: inc_type, arg: Argument) -> None: + Requires(Acc(arg.parameter) and Acc(arg.result)) + + ClosureCall(f(arg), inc) + + Ensures(Acc(arg.parameter) and Acc(arg.result)) + Ensures(arg.result >= Old(arg.result) + arg.parameter) + Ensures(arg.parameter == Old(arg.parameter)) + + ClosureCall(f(arg), inc_call_slot(f, arg)()) + + assert arg.result >= 70 and arg.parameter == 50 From 5427ebefbcc6ff79caa4caebd8b3a088b11f48e3 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 16:24:35 +0200 Subject: [PATCH 24/31] First version of pure call slots --- src/nagini_translation/analyzer.py | 5 +- src/nagini_translation/call_slot_analyzers.py | 33 +++- .../call_slots/call_slot_translator.py | 161 +++++++++++++----- src/nagini_translation/translators/program.py | 5 +- 4 files changed, 162 insertions(+), 42 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index c3a3219d4..972c39736 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1199,7 +1199,10 @@ def _incompatible_decorators(self, decorators) -> bool: return ((('Predicate' in decorators) and ('Pure' in decorators)) or (('IOOperation' in decorators) and (len(decorators) != 1)) or (('property' in decorators) and (len(decorators) != 1)) or - (('CallSlot' in decorators) and (len(decorators) != 1)) or + ( + (('CallSlot' in decorators) and (len(decorators) != 1)) and + (('Pure' in decorators) and ('CallSlot' in decorators) and (len(decorators) != 2)) + ) or (('UniversallyQuantified' in decorators) and (len(decorators) != 1)) or (('CallSlotProof' in decorators) and (len(decorators) != 1))) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index eac347ffd..4414a25a3 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -231,6 +231,10 @@ def _pre_process(self, node: ast.FunctionDef) -> None: self.analyzer.node_factory ) + + if _has_double_decorator_name(node, 'Pure', 'CallSlot'): + self.call_slot.pure = True + scope.call_slots[node.name] = self.call_slot def _check_body(self, body: List[ast.stmt]) -> None: @@ -544,7 +548,10 @@ def is_call_slot(node: ast.FunctionDef) -> bool: """ Whether node is a call slot declaration. """ - return _has_single_decorator_name(node, 'CallSlot') + return ( + _has_single_decorator_name(node, 'CallSlot') or + _has_double_decorator_name(node, 'Pure', 'CallSlot') + ) def is_universally_quantified(node: ast.FunctionDef) -> bool: @@ -575,6 +582,30 @@ def _has_single_decorator_name(node: ast.FunctionDef, decorator_name: str) -> bo return decorator.id == decorator_name +def _has_double_decorator_name( + node: ast.FunctionDef, + decorator_name1: str, + decorator_name2: str +) -> bool: + """ + Whether `node' has only one decorator that equals to `decorator' + """ + if len(node.decorator_list) != 2: + return False + + decorator1, decorator2 = node.decorator_list + if not isinstance(decorator1, ast.Name): + return False + + if not isinstance(decorator2, ast.Name): + return False + + if decorator1 == decorator_name2: + decorator1, decorator2 = decorator2, decorator1 + + return decorator1.id == decorator_name1 and decorator2.id == decorator_name2 + + def _has_single_decorator_call(node: ast.FunctionDef, decorator_name: str) -> bool: """ Whether `node' has only one decorator that equals to `decorator' diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py index e4d9b520d..839fdfd1f 100644 --- a/src/nagini_translation/translators/call_slots/call_slot_translator.py +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import reduce from itertools import chain -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union from nagini_translation.lib.constants import ERROR_NAME, PRIMITIVES from nagini_translation.lib.context import Context from nagini_translation.lib.program_nodes import ( @@ -17,7 +17,8 @@ Stmt, StmtsAndExpr, Function, - Method + Method, + Position ) from nagini_translation.lib.util import InvalidProgramException from nagini_translation.translators.common import CommonTranslator @@ -39,7 +40,7 @@ def __init__(self, config: 'TranslatorConfig', jvm: 'JVM', source_file: str, super().__init__(config, jvm, source_file, type_info, viper_ast) self._var_replacer = _VarReplacer() - def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Function, Method]: + def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Function, Union[Method, Function]]: old_function = ctx.current_function ctx.current_function = call_slot @@ -58,20 +59,68 @@ def translate_call_slot(self, call_slot: CallSlot, ctx: Context) -> Tuple[Functi info ) - pres, posts = self.extract_contract(call_slot, ERROR_NAME, False, ctx) - call_slot_apply = self.create_method_node( - ctx, - call_slot.sil_application_name, - [arg.decl for arg in chain(call_slot.get_args(), call_slot.uq_variables.values())], - [res.decl for res in call_slot.get_results()], - pres, - posts, - [], - [self.viper.Inhale(self.viper.FalseLit(position, info), position, info)], - position, - info, - method=call_slot - ) + args = [arg.decl for arg in chain(call_slot.get_args(), call_slot.uq_variables.values())] + + if call_slot.pure: + + pres = [] + for arg in chain(call_slot.get_args(), call_slot.uq_variables.values()): + if arg.type.name in PRIMITIVES: + continue + pres.append(self.type_check(arg.ref(), arg.type, position, ctx)) + for pre, _ in call_slot.precondition: + pres.append(self._translate_pure_expr( + pre, ctx, target_type=self.viper.Bool, impure=True)) + + if call_slot.pure and call_slot.type is not None: + old_posts = call_slot.postcondition + call_slot.postcondition = [ + ( + self._var_replacer.replace(deepcopy(post), { + call_slot.return_variables[0].id: + ast.Call(ast.Name('Result', ast.Load), [], []) + }), + aliases + ) + for post, aliases in old_posts + ] + posts = [] + + if call_slot.type is not None and call_slot.type.name not in PRIMITIVES: + viper_type = self.translate_type(call_slot.type, ctx) + posts.append(self.type_check( + self.viper.Result(viper_type, position, info), + call_slot.type, position, ctx + )) + + for post, _ in call_slot.postcondition: + posts.append(self._translate_pure_expr( + post, ctx, target_type=self.viper.Bool, impure=True)) + + if call_slot.pure and call_slot.type is not None: + call_slot.postcondition = old_posts + + _type = self.translate_type(call_slot.type, ctx) + call_slot_apply = self.viper.Function( + call_slot.sil_application_name, args, _type, pres, posts, + None, position, info + ) + + else: + pres, posts = self.extract_contract(call_slot, ERROR_NAME, False, ctx) + call_slot_apply = self.create_method_node( + ctx, + call_slot.sil_application_name, + args, + [res.decl for res in call_slot.get_results()], + pres, + posts, + [], + [self.viper.Inhale(self.viper.FalseLit(position, info), position, info)], + position, + info, + method=call_slot + ) ctx.current_function = old_function @@ -115,7 +164,10 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C assert len(call_slot.get_args()) == len(justification.func.args) arg_map = { py_var.name: arg - for py_var, arg in zip(call_slot.get_args(), justification.func.args) + for py_var, arg in zip( + chain(call_slot.get_args(), call_slot.uq_variables.values()), + chain(justification.func.args, justification.args) + ) } assert len(call.args) == len(call_slot.call.args) @@ -126,9 +178,16 @@ def _application_call_slot(self, call: ast.Call, justification: ast.Call, ctx: C stmts.append(self._application_call_slot_arg_match( call.func, call_slot.call.func, arg_map, ctx)) + formal_args = [] + if call_slot.pure: + for arg in call_slot.get_args(): + formal_args.append(arg.decl) + for arg in call_slot.uq_variables.values(): + formal_args.append(arg.decl) + call_stmts, call_expr = self._translate_normal_call( - call_slot.sil_application_name, call_slot, - justification.func.args + justification.args, ctx, pure_args=True + call_slot.sil_application_name, call_slot, justification.func.args + justification.args, + self.to_position(call, ctx), ctx, pure_call=call_slot.pure, formal_args=formal_args ) return stmts + call_stmts, call_expr @@ -175,43 +234,48 @@ def _application_call_slot_arg_match( def _translate_normal_call( self, - name, + name: str, target: PythonMethod, args: List[ast.expr], + position: Position, ctx: Context, - pure_args=False + pure_call: bool = False, + formal_args: List[Expr] = [] ) -> StmtsAndExpr: result_var = None - if target.type is not None: + if target.type is not None and not pure_call: result_var = ctx.current_function.create_variable( target.name + '_res', target.type, self.translator).ref() - arg_stmts: List[Stmt] = [] + stmts: List[Stmt] = [] arg_exprs: List[Expr] = [] for arg in args: - if pure_args: - expr = self._translate_pure_expr(arg, ctx) - arg_exprs.append(expr) - else: - stmts, expr = self.translate_expr(arg, ctx) - arg_stmts.extend(stmts) - arg_exprs.append(expr) - + expr = self._translate_pure_expr(arg, ctx) + arg_exprs.append(expr) - call = self.create_method_call_node( - ctx, name, arg_exprs, [result_var] if result_var else [], - self.to_position(target.node, ctx), self.no_info(ctx), target_method=target - ) + if pure_call: + _type = self.translate_type(target.type, ctx) + expr = self.viper.FuncApp( + name, arg_exprs, position, self.no_info(ctx), _type, formal_args + ) + else: + stmts = self.create_method_call_node( + ctx, name, arg_exprs, [result_var] if result_var else [], + position, self.no_info(ctx), target_method=target + ) + expr = result_var - return arg_stmts + call, result_var + return stmts, expr def _application_static_dispatch(self, call: ast.Call, justification: ast.Name, ctx: Context) -> StmtsAndExpr: stmts: List[Stmt] = [] position = self.to_position(call, ctx) info = self.no_info(ctx) + target = self.get_target(justification, ctx) + assert isinstance(target, PythonMethod) closure_expr = self._translate_pure_expr(call.func, ctx) @@ -230,8 +294,14 @@ def _application_static_dispatch(self, call: ast.Call, justification: ast.Name, method = ctx.module.get_func_or_method(justification.id) + formal_args = [] + if target.pure: + for arg in target.get_args(): + formal_args.append(arg.decl) + call_stmts, call_expr = self._translate_normal_call( - method.sil_name, method, call.args, ctx, pure_args=True + method.sil_name, method, call.args, self.to_position(call, ctx), + ctx, pure_call=target.pure, formal_args=formal_args ) stmts.extend(call_stmts) @@ -491,7 +561,7 @@ def _proof_translate_contract( info = self.no_info(ctx) contract = [ - self._translate_pure_expr(self._var_replacer.replace(pre[0], cl_map), + self._translate_pure_expr(self._var_replacer.replace(deepcopy(pre[0]), cl_map), ctx, impure=True, target_type=self.viper.Bool) for pre in contract ] @@ -520,6 +590,7 @@ def _proof_translate_body_only( continue # ignore if is_fold(stmt.value) or is_unfold(stmt.value) or is_assume(stmt.value): viper_stmts.extend(self.translate_stmt(stmt, ctx)) + continue assert is_closure_call(stmt.value) @@ -531,6 +602,18 @@ def _proof_translate_body_only( stmt.value.args[0].func, call_slot.call.func, cl_map, ctx )) + if call_slot.pure: + if isinstance(stmt.value.args[1], ast.Name): + target = self.get_target(stmt.value.args[1].id, ctx) + else: + target = self.get_target(stmt.value.args[1].func.func.id) + + if not target.pure: + raise InvalidProgramException( + stmt.value, + 'call_slots.impure_closure_call.inside_pure_proof' + ) + viper_stmts.extend(self.translate_stmt(stmt, ctx)) position = self.to_position(stmt, ctx) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index fe0845d6d..0039b9f81 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -662,7 +662,10 @@ def translate_program(self, modules: List[PythonModule], for call_slot in module.call_slots.values(): call_slot_holds, call_slot_apply = self.translate_call_slot(call_slot, ctx) functions.append(call_slot_holds) - methods.append(call_slot_apply) + if call_slot.pure: + functions.append(call_slot_apply) + else: + methods.append(call_slot_apply) for class_name, cls in module.classes.items(): if class_name in PRIMITIVES or class_name != cls.name: # Skip primitives and type variable entries. From 6ba95f4af118280dc8f126062fcdcb66e895b136 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 17:30:19 +0200 Subject: [PATCH 25/31] Minor bugfix in translate_Return --- src/nagini_translation/translators/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 76d32887a..ce9598b66 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -128,7 +128,7 @@ def _translate_only(self, node: ast.AST, ctx: Context, impure=False): def translate_Return(self, node: ast.Return, ctx: Context, impure=False) -> StmtsAndExpr: - return self.translate_expr(node.value, ctx, impure=impure) + return self.translate_expr(node.value, ctx, impure=impure, target_type=self._target_type) def translate_ListComp(self, node: ast.ListComp, ctx: Context) -> StmtsAndExpr: if len(node.generators) != 1: From 979f66c29475d2094de0f59c2b0e77e9c0ca4b52 Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sat, 21 Oct 2017 17:30:52 +0200 Subject: [PATCH 26/31] Added pure test and example b from the project description --- .../verification/examples/setup_compute.py | 155 ++++++++++++++++++ tests/closures/verification/test_pure.py | 97 +++++++++++ 2 files changed, 252 insertions(+) create mode 100644 tests/closures/verification/examples/setup_compute.py create mode 100644 tests/closures/verification/test_pure.py diff --git a/tests/closures/verification/examples/setup_compute.py b/tests/closures/verification/examples/setup_compute.py new file mode 100644 index 000000000..aa7b5c084 --- /dev/null +++ b/tests/closures/verification/examples/setup_compute.py @@ -0,0 +1,155 @@ +from typing import Callable, Optional +from nagini_contracts.contracts import ( + Requires, + Ensures, + Acc, + Predicate, + Fold, + Unfold, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, +) + + +class Argument: + + def __init__(self, a: int, b: int) -> None: + self.a = a # type: int + self.b = b # type: int + + Ensures(Acc(self.a) and Acc(self.b)) + Ensures(self.a == a and self.b == b) + + +F_Type = Callable[[Argument, int], Optional[object]] + + +@CallSlot +def f_setup(setup: F_Type, c: int, d: int, before_token: int, between_token: int) -> None: + + @UniversallyQuantified + def uq(arg: Argument) -> None: + Requires(before(arg, c, d, before_token)) + + setup(arg, c) + + Ensures(between(arg, c, d, between_token)) + + +@CallSlot +def f_compute(compute: F_Type, c: int, d: int, between_token: int, after_token: int) -> None: + + @UniversallyQuantified + def uq(arg: Argument) -> None: + Requires(between(arg, c, d, between_token)) + + compute(arg, d) + + Ensures(after(arg, c, d, after_token)) + + +def f( + setup: F_Type, + compute: F_Type, + arg: Argument, + c: int, + d: int, + before_token: int, + between_token: int, + after_token: int +) -> None: + + Requires(f_setup(setup, c, d, before_token, between_token)) + Requires(f_compute(compute, c, d, between_token, after_token)) + Requires(before(arg, c, d, before_token)) + + ClosureCall(setup(arg, c), f_setup(setup, c, d, before_token, between_token)(arg)) + + # assert between(arg, c, d, between_token) + + ClosureCall(compute(arg, d), f_compute(compute, c, d, between_token, after_token)(arg)) + + Ensures(after(arg, c, d, after_token)) + + +def setup(arg: Argument, c: int) -> Optional[object]: + Requires(Acc(arg.a)) + Requires(c > 1) + Ensures(Acc(arg.a)) + Ensures(arg.a == 3 * c) + + arg.a = 3 * c + + +def compute(arg: Argument, d: int) -> Optional[object]: + Requires(Acc(arg.a, 1 / 2) and Acc(arg.b)) + Ensures(Acc(arg.a, 1 / 2) and Acc(arg.b)) + Ensures(arg.b == arg.a + d) + + arg.b = arg.a + d + + +@Predicate +def before(arg: Argument, c: int, d: int, token: int) -> bool: + return ( + Acc(arg.a) and Acc(arg.b) and c > 1 if token == 1 else + True + ) + + +@Predicate +def between(arg: Argument, c: int, d: int, token: int) -> bool: + return ( + Acc(arg.b) and Acc(arg.a) and arg.a == 3 * c if token == 1 else + True + ) + + +@Predicate +def after(arg: Argument, c: int, d: int, token: int) -> bool: + return ( + Acc(arg.a) and Acc(arg.b) and arg.a == 3 * c and arg.b == arg.a + d if token == 1 else + True + ) + + +def client() -> None: + + arg = Argument(2, 2) + c, d = 2, 3 + before_token = between_token = after_token = 1 + + @CallSlotProof(f_setup(setup, c, d, before_token, between_token)) + def f_setup_proof(f: F_Type, c: int, d: int, A: int, B: int) -> None: + + @UniversallyQuantified + def uq(arg: Argument) -> None: + Requires(before(arg, c, d, before_token)) + Unfold(before(arg, c, d, between_token)) + + ClosureCall(f(arg, c), setup) + + Fold(between(arg, c, d, between_token)) + Ensures(between(arg, c, d, between_token)) + + @CallSlotProof(f_compute(compute, c, d, between_token, after_token)) + def f_compute_proof(f: F_Type, c: int, d: int, B: int, C: int) -> None: + + @UniversallyQuantified + def uq(arg: Argument) -> None: + Requires(between(arg, c, d, between_token)) + Unfold(between(arg, c, d, between_token)) + + ClosureCall(f(arg, d), compute) + + Fold(after(arg, c, d, after_token)) + Ensures(after(arg, c, d, after_token)) + + Fold(before(arg, c, d, between_token)) + f(setup, compute, arg, c, d, before_token, between_token, after_token) + Unfold(after(arg, c, d, after_token)) + + assert arg.a == 6 + assert arg.b == 9 diff --git a/tests/closures/verification/test_pure.py b/tests/closures/verification/test_pure.py new file mode 100644 index 000000000..fa4af9a90 --- /dev/null +++ b/tests/closures/verification/test_pure.py @@ -0,0 +1,97 @@ +from typing import Callable +from nagini_contracts.contracts import ( + Requires, + Ensures, + Acc, + Pure, + Result, + Old, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall +) + + +def choice() -> bool: + return True + + +class Argument: + + def __init__(self, parameter: int, result: int) -> None: + self.parameter = parameter # type: int + self.result = result # type: int + + Ensures(Acc(self.parameter) and Acc(self.result)) + Ensures(self.parameter == parameter and self.result == result) + + +F_Type = Callable[[Argument, int], int] + + +@Pure +def add(arg: Argument, x: int) -> int: + Requires(Acc(arg.parameter)) + Ensures(Result() == x + arg.parameter) + return x + arg.parameter + + +@Pure +def mul(arg: Argument, x: int) -> int: + Requires(Acc(arg.parameter)) + Ensures(Result() == x * arg.parameter) + return x * arg.parameter + + +@Pure +@CallSlot +def pure_call_slot(f: F_Type, arg: Argument) -> None: + + @UniversallyQuantified + def uq(x: int) -> None: + Requires(Acc(arg.parameter) and arg.parameter > 0 and x > 1) + + y = f(arg, x) + + Ensures(y > arg.parameter) + + +def client(f: F_Type, arg: Argument) -> None: + Requires(Acc(arg.parameter) and Acc(arg.result)) + Requires(arg.parameter > 0) + Requires(pure_call_slot(f, arg)) + Ensures(Acc(arg.parameter) and Acc(arg.result)) + Ensures(arg.parameter == Old(arg.parameter)) + Ensures(arg.result > arg.parameter) + + arg.result = ClosureCall(f(arg, 20), pure_call_slot(f, arg)(20)) + + +def method() -> None: + + if choice(): + f = add + else: + f = mul + + arg = Argument(10, 5) + + @CallSlotProof(pure_call_slot(f, arg)) + def pure_call_slot(f: F_Type, arg: Argument) -> None: + + @UniversallyQuantified + def uq(x: int) -> None: + Requires(Acc(arg.parameter) and arg.parameter > 0 and x > 1) + + if f == add: + y = ClosureCall(f(arg, x), add) # type: int + else: + y = ClosureCall(f(arg, x), mul) + + Ensures(y > arg.parameter) + + client(f, arg) + + assert arg.parameter == 10 + assert arg.result > arg.parameter From e8bf2a565a7e89aa6d2d22eeff5a294b7ce8719c Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sun, 22 Oct 2017 23:47:14 +0200 Subject: [PATCH 27/31] Some bugfixes --- src/nagini_translation/call_slot_analyzers.py | 6 +- .../call_slots/call_slot_translator.py | 123 +++++++++--------- src/nagini_translation/translators/method.py | 7 +- 3 files changed, 72 insertions(+), 64 deletions(-) diff --git a/src/nagini_translation/call_slot_analyzers.py b/src/nagini_translation/call_slot_analyzers.py index 4414a25a3..f6ca50a0a 100644 --- a/src/nagini_translation/call_slot_analyzers.py +++ b/src/nagini_translation/call_slot_analyzers.py @@ -388,7 +388,7 @@ def _check_body(self, body: List[ast.stmt]) -> None: continue if is_fold(child.value) or is_unfold(child.value): continue - if is_assume(child.value): + if is_assume(child.value) or is_assert(child.value): continue self._check_call_declaration(child.value) @@ -644,5 +644,9 @@ def is_assume(call: ast.Call) -> bool: return is_named_call(call, 'Assume') +def is_assert(call: ast.Call) -> bool: + return is_named_call(call, 'Assert') + + def is_named_call(call: ast.Call, name: str) -> bool: return isinstance(call.func, ast.Name) and call.func.id == name diff --git a/src/nagini_translation/translators/call_slots/call_slot_translator.py b/src/nagini_translation/translators/call_slots/call_slot_translator.py index 839fdfd1f..35d6f1e21 100644 --- a/src/nagini_translation/translators/call_slots/call_slot_translator.py +++ b/src/nagini_translation/translators/call_slots/call_slot_translator.py @@ -29,7 +29,8 @@ is_postcondition, is_fold, is_unfold, - is_assume + is_assume, + is_assert, ) @@ -418,7 +419,7 @@ def _proof_extract_var(self, var: PythonVar, val: ast.expr, ctx: Context) -> Stm if proof_var.type.name not in PRIMITIVES: stmts.append(self.viper.Inhale( self.var_type_check( - proof_var.name, proof_var.type, position, ctx + proof_var.sil_name, proof_var.type, position, ctx ), position, info @@ -532,7 +533,7 @@ def _proof_introduce_uq_ret_vars(self, proof: CallSlotProof, ctx: Context) -> Li if proof_var.type.name not in PRIMITIVES: stmts.append(self.viper.Inhale( self.var_type_check( - proof_var.name, proof_var.type, position, ctx + proof_var.sil_name, proof_var.type, position, ctx ), position, info @@ -588,78 +589,27 @@ def _proof_translate_body_only( if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call): if is_precondition(stmt.value) or is_postcondition(stmt.value): continue # ignore - if is_fold(stmt.value) or is_unfold(stmt.value) or is_assume(stmt.value): + if is_fold(stmt.value) or is_unfold(stmt.value): + viper_stmts.extend(self.translate_stmt(stmt, ctx)) + continue + if is_assume(stmt.value) or is_assert(stmt.value): viper_stmts.extend(self.translate_stmt(stmt, ctx)) continue assert is_closure_call(stmt.value) - for arg_call, arg_slot in zip(stmt.value.args[0].args, call_slot.call.args): - viper_stmts.append(self._application_call_slot_arg_match( - arg_call, arg_slot, cl_map, ctx - )) - viper_stmts.append(self._application_call_slot_arg_match( - stmt.value.args[0].func, call_slot.call.func, cl_map, ctx - )) - - if call_slot.pure: - if isinstance(stmt.value.args[1], ast.Name): - target = self.get_target(stmt.value.args[1].id, ctx) - else: - target = self.get_target(stmt.value.args[1].func.func.id) - - if not target.pure: - raise InvalidProgramException( - stmt.value, - 'call_slots.impure_closure_call.inside_pure_proof' - ) - - viper_stmts.extend(self.translate_stmt(stmt, ctx)) - - position = self.to_position(stmt, ctx) - info = self.no_info(ctx) - viper_stmts.append(self.viper.LocalVarAssign( - call_counter.ref(), - self.viper.Add( - call_counter.ref(), - self.viper.IntLit(1, position, info), - position, - info - ), - position, - info + viper_stmts.extend(self._proof_translate_application( + stmt, call_slot, call_counter, cl_map, ctx )) elif isinstance(stmt, ast.Assign): assert is_closure_call(stmt.value) - for arg_call, arg_slot in zip(stmt.value.args[0].args, call_slot.call.args): - viper_stmts.append(self._application_call_slot_arg_match( - arg_call, arg_slot, cl_map, ctx - )) - viper_stmts.append(self._application_call_slot_arg_match( - stmt.value.args[0].func, call_slot.call.func, cl_map, ctx + viper_stmts.extend(self._proof_translate_application( + stmt, call_slot, call_counter, cl_map, ctx )) - viper_stmts.extend(self.translate_stmt(stmt, ctx)) - - position = self.to_position(stmt, ctx) - info = self.no_info(ctx) - viper_stmts.append(self.viper.LocalVarAssign( - call_counter.ref(), - self.viper.Add( - call_counter.ref(), - self.viper.IntLit(1, position, info), - position, - info - ), - position, - info - )) - - continue - elif isinstance(stmt, ast.Assert): viper_stmts.extend(self.translate_stmt(stmt, ctx)) @@ -694,6 +644,55 @@ def _proof_translate_body_only( return viper_stmts + def _proof_translate_application( + self, + stmt: Union[ast.Assign, ast.Expr], + call_slot: CallSlot, + call_counter: PythonVar, + cl_map: Dict[str, ast.expr], + ctx: Context + ) -> List[Stmt]: + + stmts: List[Stmt] = [] + + for arg_call, arg_slot in zip(stmt.value.args[0].args, call_slot.call.args): + stmts.append(self._application_call_slot_arg_match( + arg_call, arg_slot, cl_map, ctx + )) + stmts.append(self._application_call_slot_arg_match( + stmt.value.args[0].func, call_slot.call.func, cl_map, ctx + )) + + if call_slot.pure: + if isinstance(stmt.value.args[1], ast.Name): + target = self.get_target(stmt.value.args[1], ctx) + else: + target = self.get_target(stmt.value.args[1].func.func, ctx) + + if not target.pure: + raise InvalidProgramException( + stmt.value, + 'call_slots.impure_closure_call.inside_pure_proof' + ) + + stmts.extend(self.translate_stmt(stmt, ctx)) + + position = self.to_position(stmt, ctx) + info = self.no_info(ctx) + stmts.append(self.viper.LocalVarAssign( + call_counter.ref(), + self.viper.Add( + call_counter.ref(), + self.viper.IntLit(1, position, info), + position, + info + ), + position, + info + )) + + return stmts + def _translate_pure_expr( self, node: ast.expr, diff --git a/src/nagini_translation/translators/method.py b/src/nagini_translation/translators/method.py index ccdeeef61..40615c9b8 100644 --- a/src/nagini_translation/translators/method.py +++ b/src/nagini_translation/translators/method.py @@ -7,6 +7,7 @@ PRIMITIVES, ) from nagini_translation.lib.program_nodes import ( + CallSlot, GenericType, MethodType, PythonExceptionHandler, @@ -160,7 +161,11 @@ def _create_typeof_pres(self, func: PythonMethod, is_constructor: bool, """ Creates 'typeof' preconditions for function arguments. """ - args = func.get_args() + if isinstance(func, CallSlot): + args = func.get_args() + args.extend(func.uq_variables.values()) + else: + args = func.get_args() pres = [] for i, arg in enumerate(args): if not (arg.type.name in PRIMITIVES): From 67539a1c97bd9038aa1c0a853102b2cef0368dce Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Sun, 22 Oct 2017 23:48:11 +0200 Subject: [PATCH 28/31] Added pure, while loop and n times examples --- .../closures/verification/examples/n_times.py | 132 ++++++++++++ tests/closures/verification/examples/pure.py | 141 +++++++++++++ .../verification/examples/while_loop.py | 193 ++++++++++++++++++ 3 files changed, 466 insertions(+) create mode 100644 tests/closures/verification/examples/n_times.py create mode 100644 tests/closures/verification/examples/pure.py create mode 100644 tests/closures/verification/examples/while_loop.py diff --git a/tests/closures/verification/examples/n_times.py b/tests/closures/verification/examples/n_times.py new file mode 100644 index 000000000..7cbb820c2 --- /dev/null +++ b/tests/closures/verification/examples/n_times.py @@ -0,0 +1,132 @@ +from typing import Callable, Optional +from nagini_contracts.contracts import ( + Implies, + Requires, + Ensures, + Result, + Old, + Invariant, + Predicate, + Fold, + Unfold, + Unfolding, + Pure, + Acc, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Assert, +) + + +class State: + + def __init__( + self, + counter: int, + value: int, + offset: int, + next: Optional['State'] + ) -> None: + + self.counter = counter + self.value = value + self.offset = offset + self.next = next + Ensures(Acc(self.counter) and Acc(self.value)) + Ensures(Acc(self.offset) and Acc(self.next)) + Ensures(self.counter == counter and self.value == value) + Ensures(self.offset == offset and self.next == next) + + +f_type = Callable[[State], Optional[object]] + + +@Predicate +def n_inv(s: State, i: int, n_inv_token: int) -> bool: + return ( + Acc(s.counter) and Acc(s.value) and + s.counter == i and s.value == i * (i + 1) // 2 + if n_inv_token == 1 else + True + ) + + +@CallSlot +def n_times_slot(f: f_type, n_inv_token: int) -> None: + + @UniversallyQuantified + def uq(s: State, i: int) -> None: + Requires(n_inv(s, i, n_inv_token)) + + f(s) + + Ensures(n_inv(s, i + 1, n_inv_token)) + + +def n_times(f: f_type, n: int, s: State, n_inv_token: int) -> None: + Requires(0 <= n) + Requires(n_inv(s, 0, n_inv_token)) + Requires(n_times_slot(f, n_inv_token)) + Ensures(n_inv(s, n, n_inv_token)) + + i = 0 + while i < n: + Invariant(i <= n) + Invariant(n_inv(s, i, n_inv_token)) + + ClosureCall(f(s), n_times_slot(f, n_inv_token)(s, i)) + i += 1 + + # FIXME: this shouldn't be necessary + # (would be impossible with proper 'parametric assertions') + # However while i == n, theyr values as Ref types seem to be uneqal + Unfold(n_inv(s, i, n_inv_token)) + Fold(n_inv(s, n, n_inv_token)) + + +def sum_range(s: State) -> Optional[object]: + Requires(Acc(s.counter) and Acc(s.value)) + Ensures(Acc(s.counter) and Acc(s.value)) + Ensures(s.counter == Old(s.counter) + 1) + Ensures(s.value == Old(s.value + s.counter + 1)) + + s.counter += 1 + s.value += s.counter + + return None + + +def n_times_client() -> None: + + s = State(0, 0, 0, None) + + f = sum_range + + @CallSlotProof(n_times_slot(f, 1)) + def n_times_slot(f: f_type, n_inv_token: int) -> None: + + @UniversallyQuantified + def uq(s: State, i: int) -> None: + Requires(n_inv(s, i, n_inv_token)) + Unfold(n_inv(s, i, n_inv_token)) + + assert i == s.counter + assert s.value == i * (i + 1) // 2 + ClosureCall(f(s), sum_range) + + assert s.counter == i + 1 + assert s.value == i * (i + 1) // 2 + i + 1 + assert s.value == i * (i + 1) // 2 + i + 1 + assert s.value == i * (i + 1) // 2 + 2 * (i + 1) // 2 + assert s.value == (i * (i + 1) + 2 * (i + 1)) // 2 + + Fold(n_inv(s, i + 1, n_inv_token)) + Ensures(n_inv(s, i + 1, n_inv_token)) + + Fold(n_inv(s, 0, 1)) + n_times(f, 23, s, 1) + Unfold(n_inv(s, 23, 1)) + assert s.value == 276 + assert s.counter == 23 diff --git a/tests/closures/verification/examples/pure.py b/tests/closures/verification/examples/pure.py new file mode 100644 index 000000000..582088124 --- /dev/null +++ b/tests/closures/verification/examples/pure.py @@ -0,0 +1,141 @@ +from typing import Callable +from nagini_contracts.contracts import ( + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Pure, + Result, + Requires, + Ensures, + Acc +) + + +def choice() -> bool: + return True + + +class Argument: + + def __init__(self, value_a: int, value_b: int) -> None: + self.value_a = value_a + self.value_b = value_b + Ensures(Acc(self.value_a) and Acc(self.value_b)) + Ensures(self.value_a == value_a and self.value_b == value_b) + + +f_type = Callable[[Argument, int], int] + + +@Pure +def add(arg: Argument, x: int) -> int: + Requires(Acc(arg.value_a)) + Ensures(Result() == arg.value_a + x) + return arg.value_a + x + + +@Pure +def mul(arg: Argument, x: int) -> int: + Requires(Acc(arg.value_b)) + Ensures(Result() == arg.value_b * x) + return arg.value_b * x + + +@Pure +@CallSlot +def add_or_mul(f: f_type) -> None: + + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) + + y = f(arg, x) + + Ensures(y == arg.value_a + x or y == arg.value_b * x) + + +@CallSlot +def hof_slot(f: f_type) -> None: + + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + + y = f(arg, x) + + Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + + +def hof(f: f_type, arg: Argument) -> int: + Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + Requires(hof_slot(f)) + + Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + Ensures(Result() <= arg.value_a + 5 or Result() >= arg.value_b * 5) + + return ClosureCall(f(arg, 5), hof_slot(f)(arg, 5)) + + +def client() -> None: + + arg = Argument(1, 2) + assert arg.value_a == 1 + assert arg.value_b == 2 + + f = add + y = ClosureCall(f(arg, 3), add) # type: int + assert y == 4 + assert arg.value_a == 1 + assert arg.value_b == 2 + + f = mul + y = ClosureCall(f(arg, 3), mul) + assert y == 6 + assert arg.value_a == 1 + assert arg.value_b == 2 + + if choice(): + f = add + else: + f = mul + + @CallSlotProof(add_or_mul(f)) + def add_or_mul_proof(f: f_type) -> None: + + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) + + if f == add: + y = ClosureCall(f(arg, x), add) # type: int + else: + y = ClosureCall(f(arg, x), mul) + + Ensures(y == arg.value_a + x or y == arg.value_b * x) + + y = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) + assert y == 4 or y == 6 + assert arg.value_a == 1 + assert arg.value_b == 2 + + @CallSlotProof(hof_slot(f)) + def hof_slot_proof(f: f_type) -> None: + + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) + + if f == add: + y = ClosureCall(f(arg, x), add) # type: int + else: + y = ClosureCall(f(arg, x), mul) + + Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + + h = hof + y = ClosureCall(h(f, arg), hof) + assert y <= 6 or y >= 10 + assert arg.value_a == 1 + assert arg.value_b == 2 diff --git a/tests/closures/verification/examples/while_loop.py b/tests/closures/verification/examples/while_loop.py new file mode 100644 index 000000000..ceaadaca5 --- /dev/null +++ b/tests/closures/verification/examples/while_loop.py @@ -0,0 +1,193 @@ +from typing import Callable, Optional +from nagini_contracts.contracts import ( + Implies, + Requires, + Ensures, + Result, + Old, + Invariant, + Predicate, + Fold, + Unfold, + Unfolding, + Pure, + Acc, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, + Assert, +) + + +class State: + + def __init__( + self, + counter: int, + value: int, + offset: int, + next: Optional['State'] + ) -> None: + + self.counter = counter + self.value = value + self.offset = offset + self.next = next + Ensures(Acc(self.counter) and Acc(self.value)) + Ensures(Acc(self.offset) and Acc(self.next)) + Ensures(self.counter == counter and self.value == value) + Ensures(self.offset == offset and self.next == next) + + +cond_t = Callable[[State], bool] +body_t = Callable[[State], Optional[object]] + + +@Predicate +def inv(s: State, inv_token: int) -> bool: + return ( + ( + Acc(s.counter) and Acc(s.value, 1 / 2) and + s.counter <= s.value + ) if inv_token == 1 else + True + ) + + +@Pure +def cond_expr(s: State, inv_token: int) -> bool: + Requires(inv(s, inv_token)) + return Unfolding( + inv(s, inv_token), + s.counter < s.value if inv_token == 1 else + True + ) + + +@Pure +@CallSlot +def cond_slot( + cond: cond_t, + inv_token: int +) -> None: + + @UniversallyQuantified + def uq(s: State) -> None: + Requires(inv(s, inv_token)) + + b = cond(s) + + Ensures(b == cond_expr(s, inv_token)) + + +@CallSlot +def body_slot(body: body_t, inv_token: int) -> None: + + @UniversallyQuantified + def uq(s: State) -> None: + Requires(inv(s, inv_token)) + Requires(cond_expr(s, inv_token)) + + body(s) + + Ensures(inv(s, inv_token)) + + +def while_loop( + cond: cond_t, + body: body_t, + s: State, + inv_token: int +) -> None: + + Requires(inv(s, inv_token)) + Requires(cond_slot(cond, inv_token)) + Requires(body_slot(body, inv_token)) + + Ensures(inv(s, inv_token)) + Ensures(not cond_expr(s, inv_token)) + + b = ClosureCall( + cond(s), + cond_slot(cond, inv_token)(s) + ) # type: bool + + assert b == cond_expr(s, inv_token) + + while b: + Invariant(inv(s, inv_token)) + Invariant(b == cond_expr(s, inv_token)) + + ClosureCall( + body(s), + body_slot(body, inv_token)(s) + ) + + b = ClosureCall( + cond(s), + cond_slot(cond, inv_token)(s) + ) + + +@Pure +def count_to_cond(s: State) -> bool: + Requires(Acc(s.counter) and Acc(s.value)) + Ensures(Result() == (s.counter < s.value)) + return s.counter < s.value + + +def count_to_body(s: State) -> Optional[object]: + Requires(Acc(s.counter) and Acc(s.value, 1 / 2)) + Requires(s.counter < s.value) + + Ensures(Acc(s.counter) and Acc(s.value, 1 / 2)) + Ensures(s.counter <= s.value) + Ensures(s.counter == Old(s.counter) + 1) + + s.counter += 1 + return None + + +def while_loop_client() -> None: + + cond_f = count_to_cond + body_f = count_to_body + + s = State(0, 20, 0, None) + + @CallSlotProof(cond_slot(cond_f, 1)) + def cond_slot( + cond: cond_t, + inv_token: int + ) -> None: + + @UniversallyQuantified + def uq(s: State) -> None: + Requires(inv(s, inv_token)) + + Unfold(inv(s, inv_token)) + b = ClosureCall(cond(s), count_to_cond) # type: bool + Fold(inv(s, inv_token)) + + Ensures(b == cond_expr(s, inv_token)) + + @CallSlotProof(body_slot(body_f, 1)) + def body_slot(body: body_t, inv_token: int) -> None: + + @UniversallyQuantified + def uq(s: State) -> None: + Requires(inv(s, inv_token)) + Requires(Unfolding(inv(s, inv_token), cond_expr(s, inv_token))) + + Unfold(inv(s, inv_token)) + ClosureCall(body(s), count_to_body) + Fold(inv(s, inv_token)) + + Ensures(inv(s, inv_token)) + + Fold(inv(s, 1)) + while_loop(cond_f, body_f, s, 1) + Unfold(inv(s, 1)) + + assert s.counter == 20 and s.value == 20 From 1d468904dc95c6248a615b7446d7813385f049fc Mon Sep 17 00:00:00 2001 From: Ben Weber Date: Thu, 16 Nov 2017 10:01:05 +0100 Subject: [PATCH 29/31] Minor changes to the tests --- .../verification/examples/hof_forwarding.py | 172 ++++++++++++++++++ .../closures/verification/examples/n_times.py | 2 +- tests/closures/verification/examples/pure.py | 61 ++++--- 3 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 tests/closures/verification/examples/hof_forwarding.py diff --git a/tests/closures/verification/examples/hof_forwarding.py b/tests/closures/verification/examples/hof_forwarding.py new file mode 100644 index 000000000..3a41024aa --- /dev/null +++ b/tests/closures/verification/examples/hof_forwarding.py @@ -0,0 +1,172 @@ +from typing import Callable +from nagini_contracts.contracts import ( + Implies, + Requires, + Ensures, + Result, + Old, + Invariant, + Predicate, + Fold, + Unfold, + Unfolding, + Acc, + CallSlot, + CallSlotProof, + UniversallyQuantified, + ClosureCall, +) + + +def inc(x: int) -> int: + Ensures(Result() == x + 1) + return x + 1 + + +def mul(x: int) -> int: + Requires(x > 0) + Ensures(Result() == x * 2) + return x * 2 + + +@Predicate +def pre(x: int, pre_token: int) -> bool: + return ( + True if pre_token == 1 else + x > 0 if pre_token == 2 else + True + ) + + +@Predicate +def post(x: int, ret: int, post_token: int) -> bool: + return ( + ret == x + 1 if post_token == 1 else + ret == x * 2 if post_token == 2 else + True + ) + + +f0_type = Callable[[int], int] + + +@CallSlot +def f1_slot(f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + ret = f0(x) + Ensures(post(x, ret, post_token)) + + +def f1(f0: f0_type, x: int, pre_token: int, post_token: int) -> int: + Requires(f1_slot(f0, x, pre_token, post_token)) + Requires(pre(x, pre_token)) + Ensures(post(x, Result(), post_token)) + return ClosureCall(f0(x), f1_slot(f0, x, pre_token, post_token)()) + + +f1_type = Callable[[f0_type, int, int, int], int] + + +@CallSlot +def f2_slot(f1: f1_type, f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + ret = f1(f0, x, pre_token, post_token) + Ensures(post(x, ret, post_token)) + + +def f2(f1: f1_type, f0: f0_type, x: int, pre_token: int, post_token: int) -> int: + Requires(f2_slot(f1, f0, x, pre_token, post_token)) + Requires(pre(x, pre_token)) + Ensures(post(x, Result(), post_token)) + return ClosureCall( + f1(f0, x, pre_token, post_token), + f2_slot(f1, f0, x, pre_token, post_token)() + ) + + +f2_type = Callable[[f1_type, f0_type, int, int, int], int] + + +@CallSlot +def f3_slot(f2: f2_type, f1: f1_type, f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + ret = f2(f1, f0, x, pre_token, post_token) + Ensures(post(x, ret, post_token)) + + +def f3(f2: f2_type, f1: f1_type, f0: f0_type, x: int, pre_token: int, post_token: int) -> int: + Requires(f3_slot(f2, f1, f0, x, pre_token, post_token)) + Requires(pre(x, pre_token)) + Ensures(post(x, Result(), post_token)) + return ClosureCall( + f2(f1, f0, x, pre_token, post_token), + f3_slot(f2, f1, f0, x, pre_token, post_token)() + ) + + +def client() -> None: + + _inc = inc + _mul = mul + + _f1 = f1 + _f2 = f2 + _f3 = f3 + + @CallSlotProof(f1_slot(_inc, 5, 1, 1)) + def f1_slot_inc(f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + Unfold(pre(x, pre_token)) + ret = ClosureCall(f0(x), inc) # type: int + Fold(post(x, ret, post_token)) + Ensures(post(x, ret, post_token)) + + @CallSlotProof(f2_slot(_f1, _inc, 5, 1, 1)) + def f2_slot_inc(_f1: f1_type, _f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + + ret = ClosureCall(_f1(_f0, x, pre_token, post_token), f1) # type: int + + Ensures(post(x, ret, post_token)) + + @CallSlotProof(f3_slot(_f2, _f1, _inc, 5, 1, 1)) + def f3_slot_inc(_f2: f2_type, _f1: f1_type, _f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + + ret = ClosureCall(_f2(_f1, _f0, x, pre_token, post_token), f2) # type: int + + Ensures(post(x, ret, post_token)) + + Fold(pre(5, 1)) + y1 = ClosureCall(_f3(_f2, _f1, _inc, 5, 1, 1), f3) # type: int + Unfold(post(5, y1, 1)) + assert y1 == 6 + + @CallSlotProof(f1_slot(_mul, 5, 2, 2)) + def f1_slot_mul(f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + Unfold(pre(x, pre_token)) + ret = ClosureCall(f0(x), mul) # type: int + Fold(post(x, ret, post_token)) + Ensures(post(x, ret, post_token)) + + @CallSlotProof(f2_slot(_f1, _mul, 5, 2, 2)) + def f2_slot_mul(_f1: f1_type, _f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + + ret = ClosureCall(_f1(_f0, x, pre_token, post_token), f1) # type: int + + Ensures(post(x, ret, post_token)) + + @CallSlotProof(f3_slot(_f2, _f1, _mul, 5, 2, 2)) + def f3_slot_mul(_f2: f2_type, _f1: f1_type, _f0: f0_type, x: int, pre_token: int, post_token: int) -> None: + Requires(pre(x, pre_token)) + + ret = ClosureCall(_f2(_f1, _f0, x, pre_token, post_token), f2) # type: int + + Ensures(post(x, ret, post_token)) + + Fold(pre(5, 2)) + y2 = ClosureCall(_f3(_f2, _f1, _mul, 5, 2, 2), f3) # type: int + Unfold(post(5, y2, 2)) + assert y2 == 10 diff --git a/tests/closures/verification/examples/n_times.py b/tests/closures/verification/examples/n_times.py index 7cbb820c2..5d17f4eb8 100644 --- a/tests/closures/verification/examples/n_times.py +++ b/tests/closures/verification/examples/n_times.py @@ -73,7 +73,7 @@ def n_times(f: f_type, n: int, s: State, n_inv_token: int) -> None: i = 0 while i < n: - Invariant(i <= n) + Invariant(0 <= i and i <= n) Invariant(n_inv(s, i, n_inv_token)) ClosureCall(f(s), n_times_slot(f, n_inv_token)(s, i)) diff --git a/tests/closures/verification/examples/pure.py b/tests/closures/verification/examples/pure.py index 582088124..043fdb244 100644 --- a/tests/closures/verification/examples/pure.py +++ b/tests/closures/verification/examples/pure.py @@ -46,26 +46,26 @@ def mul(arg: Argument, x: int) -> int: @CallSlot def add_or_mul(f: f_type) -> None: - @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) - y = f(arg, x) + y = f(arg, x) - Ensures(y == arg.value_a + x or y == arg.value_b * x) + Ensures(y == arg.value_a + x or y == arg.value_b * x) @CallSlot def hof_slot(f: f_type) -> None: - @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) - y = f(arg, x) + y = f(arg, x) - Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) - Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + Ensures(y <= arg.value_a + x or y >= arg.value_b * x) def hof(f: f_type, arg: Argument) -> int: @@ -104,35 +104,38 @@ def client() -> None: @CallSlotProof(add_or_mul(f)) def add_or_mul_proof(f: f_type) -> None: - @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) - if f == add: - y = ClosureCall(f(arg, x), add) # type: int - else: - y = ClosureCall(f(arg, x), mul) + if f == add: + y = ClosureCall(f(arg, x), add) # type: int + else: + y = ClosureCall(f(arg, x), mul) - Ensures(y == arg.value_a + x or y == arg.value_b * x) + Ensures(y == arg.value_a + x or y == arg.value_b * x) - y = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) - assert y == 4 or y == 6 + y1 = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) # type: int + y2 = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) # type: int + assert y1 == 4 or y2 == 6 + assert y1 == y2 assert arg.value_a == 1 assert arg.value_b == 2 + assert y1 == ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) @CallSlotProof(hof_slot(f)) def hof_slot_proof(f: f_type) -> None: - @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + @UniversallyQuantified + def uq(arg: Argument, x: int) -> None: + Requires(Acc(arg.value_a) and Acc(arg.value_b)) - if f == add: - y = ClosureCall(f(arg, x), add) # type: int - else: - y = ClosureCall(f(arg, x), mul) + if f == add: + y = ClosureCall(f(arg, x), add) # type: int + else: + y = ClosureCall(f(arg, x), mul) - Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + Ensures(y <= arg.value_a + x or y >= arg.value_b * x) h = hof y = ClosureCall(h(f, arg), hof) From 55053ce6314659d7b6cecbf4075b66b94f5c2e0c Mon Sep 17 00:00:00 2001 From: Marco Eilers Date: Tue, 21 Nov 2017 00:03:17 +0100 Subject: [PATCH 30/31] Adapted to Viper changes --- src/nagini_contracts/contracts.py | 2 +- src/nagini_translation/lib/viper_ast.py | 14 ++++++++++++-- .../translators/obligation/method_node.py | 7 ++++--- src/nagini_translation/translators/program.py | 3 ++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 5ba8db18d..61fef8a71 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -302,7 +302,7 @@ def CallSlotProof(call_slot: Callable[..., Any]) -> Callable[[Callable[..., None pass -def ClosureCall(call: Any, justification: Any) -> Any: +def ClosureCall(call: T, justification: Any) -> T: """ Justifies a closure call through either * a CallSlot (justification == the callslot instance) diff --git a/src/nagini_translation/lib/viper_ast.py b/src/nagini_translation/lib/viper_ast.py index 125a2a452..979be52e0 100644 --- a/src/nagini_translation/lib/viper_ast.py +++ b/src/nagini_translation/lib/viper_ast.py @@ -134,10 +134,20 @@ def Function(self, name, args, type, pres, posts, body, position, info): def Method(self, name, args, returns, pres, posts, locals, body, position, info): - body_with_locals = self.Seqn([body], position, info, locals) + if body is None: + body_with_locals = self.none + else: + body_with_locals = self.scala.Some(self.Seqn([body], position, info, locals)) return self.ast.Method(name, self.to_seq(args), self.to_seq(returns), self.to_seq(pres), self.to_seq(posts), - body_with_locals, position, info, self.NoTrafos) + body_with_locals, position, info, + self.NoTrafos) + + def from_option(self, option): + if option == self.none: + return None + else: + return option.get() def Field(self, name, type, position, info): return self.ast.Field(name, type, position, info, self.NoTrafos) diff --git a/src/nagini_translation/translators/obligation/method_node.py b/src/nagini_translation/translators/obligation/method_node.py index 3b00709e2..e333fe091 100644 --- a/src/nagini_translation/translators/obligation/method_node.py +++ b/src/nagini_translation/translators/obligation/method_node.py @@ -58,7 +58,8 @@ def prepend_return(self, arg: VarDecl) -> None: def prepend_body(self, statements: List[Stmt]) -> None: """Prepend ``statements`` to body.""" - self.body[0:0] = statements + if self.body is not None: + self.body[0:0] = statements def prepend_precondition(self, preconditions: List[Expr]) -> None: """Prepend ``preconditions`` to precondition list.""" @@ -132,9 +133,9 @@ def add_obligations(self) -> None: def _is_body_native_silver(self) -> bool: """Check if body is already in Silver.""" - return isinstance( + return (self._obligation_method.body is None or isinstance( self._obligation_method.body, - self._translator.jvm.viper.silver.ast.Seqn) + self._translator.jvm.viper.silver.ast.Seqn)) def _need_skip_body(self) -> bool: """Check if altering body should not be done.""" diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 0039b9f81..cbb3102bf 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -463,6 +463,7 @@ def _convert_silver_elements( for sil_prog in sil_progs: for method in self.viper.to_list(sil_prog.methods()): if method.name() in used_names: + body = self.viper.from_option(method.body()) converted_method = self.create_method_node( ctx=ctx, name=method.name(), @@ -471,7 +472,7 @@ def _convert_silver_elements( pres=self.viper.to_list(method.pres()), posts=self.viper.to_list(method.posts()), locals=[], - body=method.body(), + body=body, position=method.pos(), info=method.info(), ) From 29ee8938dff75522108e9b938f45ca3350444c68 Mon Sep 17 00:00:00 2001 From: Marco Eilers Date: Fri, 16 Mar 2018 19:02:08 +0100 Subject: [PATCH 31/31] Fixed some things --- src/nagini_translation/analyzer.py | 2 + src/nagini_translation/lib/typeinfo.py | 3 + .../translators/expression.py | 11 ++- .../test_method_in_contract.py | 6 +- .../test_predicate_in_contract.py | 10 +- tests/closures/verification/examples/pure.py | 98 +++++++++---------- .../verification/examples/setup_compute.py | 94 +++++++++--------- tests/closures/verification/test_old.py | 48 ++++----- tests/closures/verification/test_pure.py | 60 ++++++------ 9 files changed, 173 insertions(+), 159 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 438bf95b5..9dba7b045 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1063,6 +1063,8 @@ def convert_type(self, mypy_type, node=None) -> PythonType: return self._convert_type_type(mypy_type, node) elif self.types.is_callable_type(mypy_type): return self._convert_callable_type(mypy_type, node) + elif self.types.is_any_type(mypy_type): + raise UnsupportedException(node, 'Found Any type. Type annotation missing?') else: raise UnsupportedException(mypy_type) return result diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 09a3ccfb6..e80a38707 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -337,6 +337,9 @@ def is_union_type(self, type: mypy.types.Type) -> bool: def is_callable_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.CallableType) + def is_any_type(self, type: mypy.types.Type) -> bool: + return isinstance(type, mypy.types.AnyType) + def is_type_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.TypeType) diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 080597ab3..7f06018a0 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -832,7 +832,7 @@ def _is_primitive_operation(self, op: ast.operator, left_type: PythonType, translated as a native silver binary operation. True iff both types are identical and primitives. """ - if op not in self._primitive_operations: + if type(op) not in self._primitive_operations: return False left_type_boxed = left_type.python_class.try_box() right_type_boxed = right_type.python_class.try_box() @@ -892,6 +892,13 @@ def is_thread_method_definition(self, node: ast.Compare, ctx: Context) -> bool: return True return False + def is_callable_equality(self, node: ast.Compare, ctx: Context) -> bool: + if len(node.ops) != 1 or len(node.comparators) != 1: + return False + if not isinstance(node.ops[0], (ast.Eq, ast.Is, ast.NotEq, ast.IsNot)): + return False + # TODO + def is_type_equality(self, node: ast.Compare, ctx: Context) -> bool: """ Checks if a comparison checks the equality of the type of an object with @@ -970,6 +977,8 @@ def translate_Compare(self, node: ast.Compare, return self.translate_thread_method_definition(node, ctx) if self.is_type_equality(node, ctx): return self.translate_type_equality(node, ctx) + # if self.is_callable_equality(node, ctx): + # return self.translate_callable_equality(node, ctx) if len(node.ops) != 1 or len(node.comparators) != 1: raise UnsupportedException(node) left_stmt, left = self.translate_expr(node.left, ctx) diff --git a/tests/closures/translation/call_slot_declaration/test_method_in_contract.py b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py index 349f0a71d..d618fcc8f 100644 --- a/tests/closures/translation/call_slot_declaration/test_method_in_contract.py +++ b/tests/closures/translation/call_slot_declaration/test_method_in_contract.py @@ -10,13 +10,13 @@ @CallSlot -def call_slot(f: Callable[[int, int], int], arg: 'Arg') -> None: +def call_slot(f: Callable[[int, int], int], argm: 'Arg') -> None: @UniversallyQuantified def uq(y: int) -> None: #:: ExpectedOutput(invalid.program:purity.violated) - Requires(is_arg(arg) and y > 0) - z = f(arg.val, y) + Requires(is_arg(argm) and y > 0) + z = f(argm.val, y) Ensures(z >= y) diff --git a/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py b/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py index b6f16935f..190c63a5d 100644 --- a/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py +++ b/tests/closures/translation/call_slot_declaration/test_predicate_in_contract.py @@ -11,12 +11,12 @@ @CallSlot -def call_slot(f: Callable[[int, int], int], arg: 'Arg') -> None: +def call_slot(f: Callable[[int, int], int], argm: 'Arg') -> None: @UniversallyQuantified def uq(y: int) -> None: - Requires(is_arg(arg) and y > 0) - z = f(arg.val, y) + Requires(is_arg(argm) and y > 0) + z = f(argm.val, y) Ensures(z >= y) @@ -29,5 +29,5 @@ def __init__(self) -> None: @Predicate -def is_arg(arg: Arg) -> bool: - return Acc(arg.val) +def is_arg(argm: Arg) -> bool: + return Acc(argm.val) diff --git a/tests/closures/verification/examples/pure.py b/tests/closures/verification/examples/pure.py index 043fdb244..e631086b8 100644 --- a/tests/closures/verification/examples/pure.py +++ b/tests/closures/verification/examples/pure.py @@ -29,17 +29,17 @@ def __init__(self, value_a: int, value_b: int) -> None: @Pure -def add(arg: Argument, x: int) -> int: - Requires(Acc(arg.value_a)) - Ensures(Result() == arg.value_a + x) - return arg.value_a + x +def add(argm: Argument, x: int) -> int: + Requires(Acc(argm.value_a)) + Ensures(Result() == argm.value_a + x) + return argm.value_a + x @Pure -def mul(arg: Argument, x: int) -> int: - Requires(Acc(arg.value_b)) - Ensures(Result() == arg.value_b * x) - return arg.value_b * x +def mul(argm: Argument, x: int) -> int: + Requires(Acc(argm.value_b)) + Ensures(Result() == argm.value_b * x) + return argm.value_b * x @Pure @@ -47,54 +47,54 @@ def mul(arg: Argument, x: int) -> int: def add_or_mul(f: f_type) -> None: @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + def uq(argm: Argument, x: int) -> None: + Requires(Acc(argm.value_a) and Acc(argm.value_b)) - y = f(arg, x) + y = f(argm, x) - Ensures(y == arg.value_a + x or y == arg.value_b * x) + Ensures(y == argm.value_a + x or y == argm.value_b * x) @CallSlot def hof_slot(f: f_type) -> None: @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) + def uq(argm: Argument, x: int) -> None: + Requires(Acc(argm.value_a, 1 / 2) and Acc(argm.value_b, 1 / 3)) - y = f(arg, x) + y = f(argm, x) - Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) - Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + Ensures(Acc(argm.value_a, 1 / 2) and Acc(argm.value_b, 1 / 3)) + Ensures(y <= argm.value_a + x or y >= argm.value_b * x) -def hof(f: f_type, arg: Argument) -> int: - Requires(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) +def hof(f: f_type, argm: Argument) -> int: + Requires(Acc(argm.value_a, 1 / 2) and Acc(argm.value_b, 1 / 3)) Requires(hof_slot(f)) - Ensures(Acc(arg.value_a, 1 / 2) and Acc(arg.value_b, 1 / 3)) - Ensures(Result() <= arg.value_a + 5 or Result() >= arg.value_b * 5) + Ensures(Acc(argm.value_a, 1 / 2) and Acc(argm.value_b, 1 / 3)) + Ensures(Result() <= argm.value_a + 5 or Result() >= argm.value_b * 5) - return ClosureCall(f(arg, 5), hof_slot(f)(arg, 5)) + return ClosureCall(f(argm, 5), hof_slot(f)(argm, 5)) def client() -> None: - arg = Argument(1, 2) - assert arg.value_a == 1 - assert arg.value_b == 2 + argm = Argument(1, 2) + assert argm.value_a == 1 + assert argm.value_b == 2 f = add - y = ClosureCall(f(arg, 3), add) # type: int + y = ClosureCall(f(argm, 3), add) # type: int assert y == 4 - assert arg.value_a == 1 - assert arg.value_b == 2 + assert argm.value_a == 1 + assert argm.value_b == 2 f = mul - y = ClosureCall(f(arg, 3), mul) + y = ClosureCall(f(argm, 3), mul) assert y == 6 - assert arg.value_a == 1 - assert arg.value_b == 2 + assert argm.value_a == 1 + assert argm.value_b == 2 if choice(): f = add @@ -105,40 +105,40 @@ def client() -> None: def add_or_mul_proof(f: f_type) -> None: @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + def uq(argm: Argument, x: int) -> None: + Requires(Acc(argm.value_a) and Acc(argm.value_b)) if f == add: - y = ClosureCall(f(arg, x), add) # type: int + y = ClosureCall(f(argm, x), add) # type: int else: - y = ClosureCall(f(arg, x), mul) + y = ClosureCall(f(argm, x), mul) - Ensures(y == arg.value_a + x or y == arg.value_b * x) + Ensures(y == argm.value_a + x or y == argm.value_b * x) - y1 = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) # type: int - y2 = ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) # type: int + y1 = ClosureCall(f(argm, 3), add_or_mul(f)(argm, 3)) # type: int + y2 = ClosureCall(f(argm, 3), add_or_mul(f)(argm, 3)) # type: int assert y1 == 4 or y2 == 6 assert y1 == y2 - assert arg.value_a == 1 - assert arg.value_b == 2 - assert y1 == ClosureCall(f(arg, 3), add_or_mul(f)(arg, 3)) + assert argm.value_a == 1 + assert argm.value_b == 2 + assert y1 == ClosureCall(f(argm, 3), add_or_mul(f)(argm, 3)) @CallSlotProof(hof_slot(f)) def hof_slot_proof(f: f_type) -> None: @UniversallyQuantified - def uq(arg: Argument, x: int) -> None: - Requires(Acc(arg.value_a) and Acc(arg.value_b)) + def uq(argm: Argument, x: int) -> None: + Requires(Acc(argm.value_a) and Acc(argm.value_b)) if f == add: - y = ClosureCall(f(arg, x), add) # type: int + y = ClosureCall(f(argm, x), add) # type: int else: - y = ClosureCall(f(arg, x), mul) + y = ClosureCall(f(argm, x), mul) - Ensures(y <= arg.value_a + x or y >= arg.value_b * x) + Ensures(y <= argm.value_a + x or y >= argm.value_b * x) h = hof - y = ClosureCall(h(f, arg), hof) + y = ClosureCall(h(f, argm), hof) assert y <= 6 or y >= 10 - assert arg.value_a == 1 - assert arg.value_b == 2 + assert argm.value_a == 1 + assert argm.value_b == 2 diff --git a/tests/closures/verification/examples/setup_compute.py b/tests/closures/verification/examples/setup_compute.py index aa7b5c084..4dcc8dc40 100644 --- a/tests/closures/verification/examples/setup_compute.py +++ b/tests/closures/verification/examples/setup_compute.py @@ -30,30 +30,30 @@ def __init__(self, a: int, b: int) -> None: def f_setup(setup: F_Type, c: int, d: int, before_token: int, between_token: int) -> None: @UniversallyQuantified - def uq(arg: Argument) -> None: - Requires(before(arg, c, d, before_token)) + def uq(argm: Argument) -> None: + Requires(before(argm, c, d, before_token)) - setup(arg, c) + setup(argm, c) - Ensures(between(arg, c, d, between_token)) + Ensures(between(argm, c, d, between_token)) @CallSlot def f_compute(compute: F_Type, c: int, d: int, between_token: int, after_token: int) -> None: @UniversallyQuantified - def uq(arg: Argument) -> None: - Requires(between(arg, c, d, between_token)) + def uq(argm: Argument) -> None: + Requires(between(argm, c, d, between_token)) - compute(arg, d) + compute(argm, d) - Ensures(after(arg, c, d, after_token)) + Ensures(after(argm, c, d, after_token)) def f( setup: F_Type, compute: F_Type, - arg: Argument, + argm: Argument, c: int, d: int, before_token: int, @@ -63,61 +63,61 @@ def f( Requires(f_setup(setup, c, d, before_token, between_token)) Requires(f_compute(compute, c, d, between_token, after_token)) - Requires(before(arg, c, d, before_token)) + Requires(before(argm, c, d, before_token)) - ClosureCall(setup(arg, c), f_setup(setup, c, d, before_token, between_token)(arg)) + ClosureCall(setup(argm, c), f_setup(setup, c, d, before_token, between_token)(argm)) # assert between(arg, c, d, between_token) - ClosureCall(compute(arg, d), f_compute(compute, c, d, between_token, after_token)(arg)) + ClosureCall(compute(argm, d), f_compute(compute, c, d, between_token, after_token)(argm)) - Ensures(after(arg, c, d, after_token)) + Ensures(after(argm, c, d, after_token)) -def setup(arg: Argument, c: int) -> Optional[object]: - Requires(Acc(arg.a)) +def setup(argm: Argument, c: int) -> Optional[object]: + Requires(Acc(argm.a)) Requires(c > 1) - Ensures(Acc(arg.a)) - Ensures(arg.a == 3 * c) + Ensures(Acc(argm.a)) + Ensures(argm.a == 3 * c) - arg.a = 3 * c + argm.a = 3 * c -def compute(arg: Argument, d: int) -> Optional[object]: - Requires(Acc(arg.a, 1 / 2) and Acc(arg.b)) - Ensures(Acc(arg.a, 1 / 2) and Acc(arg.b)) - Ensures(arg.b == arg.a + d) +def compute(argm: Argument, d: int) -> Optional[object]: + Requires(Acc(argm.a, 1 / 2) and Acc(argm.b)) + Ensures(Acc(argm.a, 1 / 2) and Acc(argm.b)) + Ensures(argm.b == argm.a + d) - arg.b = arg.a + d + argm.b = argm.a + d @Predicate -def before(arg: Argument, c: int, d: int, token: int) -> bool: +def before(argm: Argument, c: int, d: int, token: int) -> bool: return ( - Acc(arg.a) and Acc(arg.b) and c > 1 if token == 1 else + Acc(argm.a) and Acc(argm.b) and c > 1 if token == 1 else True ) @Predicate -def between(arg: Argument, c: int, d: int, token: int) -> bool: +def between(argm: Argument, c: int, d: int, token: int) -> bool: return ( - Acc(arg.b) and Acc(arg.a) and arg.a == 3 * c if token == 1 else + Acc(argm.b) and Acc(argm.a) and argm.a == 3 * c if token == 1 else True ) @Predicate -def after(arg: Argument, c: int, d: int, token: int) -> bool: +def after(argm: Argument, c: int, d: int, token: int) -> bool: return ( - Acc(arg.a) and Acc(arg.b) and arg.a == 3 * c and arg.b == arg.a + d if token == 1 else + Acc(argm.a) and Acc(argm.b) and argm.a == 3 * c and argm.b == argm.a + d if token == 1 else True ) def client() -> None: - arg = Argument(2, 2) + argm = Argument(2, 2) c, d = 2, 3 before_token = between_token = after_token = 1 @@ -125,31 +125,31 @@ def client() -> None: def f_setup_proof(f: F_Type, c: int, d: int, A: int, B: int) -> None: @UniversallyQuantified - def uq(arg: Argument) -> None: - Requires(before(arg, c, d, before_token)) - Unfold(before(arg, c, d, between_token)) + def uq(argm: Argument) -> None: + Requires(before(argm, c, d, before_token)) + Unfold(before(argm, c, d, between_token)) - ClosureCall(f(arg, c), setup) + ClosureCall(f(argm, c), setup) - Fold(between(arg, c, d, between_token)) - Ensures(between(arg, c, d, between_token)) + Fold(between(argm, c, d, between_token)) + Ensures(between(argm, c, d, between_token)) @CallSlotProof(f_compute(compute, c, d, between_token, after_token)) def f_compute_proof(f: F_Type, c: int, d: int, B: int, C: int) -> None: @UniversallyQuantified - def uq(arg: Argument) -> None: - Requires(between(arg, c, d, between_token)) - Unfold(between(arg, c, d, between_token)) + def uq(argm: Argument) -> None: + Requires(between(argm, c, d, between_token)) + Unfold(between(argm, c, d, between_token)) - ClosureCall(f(arg, d), compute) + ClosureCall(f(argm, d), compute) - Fold(after(arg, c, d, after_token)) - Ensures(after(arg, c, d, after_token)) + Fold(after(argm, c, d, after_token)) + Ensures(after(argm, c, d, after_token)) - Fold(before(arg, c, d, between_token)) - f(setup, compute, arg, c, d, before_token, between_token, after_token) - Unfold(after(arg, c, d, after_token)) + Fold(before(argm, c, d, between_token)) + f(setup, compute, argm, c, d, before_token, between_token, after_token) + Unfold(after(argm, c, d, after_token)) - assert arg.a == 6 - assert arg.b == 9 + assert argm.a == 6 + assert argm.b == 9 diff --git a/tests/closures/verification/test_old.py b/tests/closures/verification/test_old.py index 4e98d2c03..41a702eb1 100644 --- a/tests/closures/verification/test_old.py +++ b/tests/closures/verification/test_old.py @@ -24,48 +24,48 @@ def __init__(self, parameter: int, result: int) -> None: inc_type = Callable[[Argument], Optional[object]] -def inc(arg: Argument) -> Optional[object]: - Requires(Acc(arg.parameter) and Acc(arg.result)) +def inc(argm: Argument) -> Optional[object]: + Requires(Acc(argm.parameter) and Acc(argm.result)) - Ensures(Acc(arg.parameter) and Acc(arg.result)) - Ensures(arg.result == Old(arg.result) + arg.parameter) - Ensures(arg.parameter == Old(arg.parameter)) + Ensures(Acc(argm.parameter) and Acc(argm.result)) + Ensures(argm.result == Old(argm.result) + argm.parameter) + Ensures(argm.parameter == Old(argm.parameter)) - arg.result = arg.result + arg.parameter + argm.result = argm.result + argm.parameter return None @CallSlot -def inc_call_slot(f: inc_type, arg: Argument) -> None: - Requires(Acc(arg.parameter) and Acc(arg.result)) +def inc_call_slot(f: inc_type, argm: Argument) -> None: + Requires(Acc(argm.parameter) and Acc(argm.result)) - f(arg) + f(argm) - Ensures(Acc(arg.parameter) and Acc(arg.result)) - Ensures(arg.result >= Old(arg.result) + arg.parameter) - Ensures(arg.parameter == Old(arg.parameter)) + Ensures(Acc(argm.parameter) and Acc(argm.result)) + Ensures(argm.result >= Old(argm.result) + argm.parameter) + Ensures(argm.parameter == Old(argm.parameter)) def test() -> None: - arg = Argument(1, 2) + argm = Argument(1, 2) - arg.result = 20 - arg.parameter = 50 + argm.result = 20 + argm.parameter = 50 f = inc - @CallSlotProof(inc_call_slot(inc, arg)) - def inc_proof(f: inc_type, arg: Argument) -> None: - Requires(Acc(arg.parameter) and Acc(arg.result)) + @CallSlotProof(inc_call_slot(inc, argm)) + def inc_proof(f: inc_type, argm: Argument) -> None: + Requires(Acc(argm.parameter) and Acc(argm.result)) - ClosureCall(f(arg), inc) + ClosureCall(f(argm), inc) - Ensures(Acc(arg.parameter) and Acc(arg.result)) - Ensures(arg.result >= Old(arg.result) + arg.parameter) - Ensures(arg.parameter == Old(arg.parameter)) + Ensures(Acc(argm.parameter) and Acc(argm.result)) + Ensures(argm.result >= Old(argm.result) + argm.parameter) + Ensures(argm.parameter == Old(argm.parameter)) - ClosureCall(f(arg), inc_call_slot(f, arg)()) + ClosureCall(f(argm), inc_call_slot(f, argm)()) - assert arg.result >= 70 and arg.parameter == 50 + assert argm.result >= 70 and argm.parameter == 50 diff --git a/tests/closures/verification/test_pure.py b/tests/closures/verification/test_pure.py index fa4af9a90..b4719c4fa 100644 --- a/tests/closures/verification/test_pure.py +++ b/tests/closures/verification/test_pure.py @@ -31,41 +31,41 @@ def __init__(self, parameter: int, result: int) -> None: @Pure -def add(arg: Argument, x: int) -> int: - Requires(Acc(arg.parameter)) - Ensures(Result() == x + arg.parameter) - return x + arg.parameter +def add(argm: Argument, x: int) -> int: + Requires(Acc(argm.parameter)) + Ensures(Result() == x + argm.parameter) + return x + argm.parameter @Pure -def mul(arg: Argument, x: int) -> int: - Requires(Acc(arg.parameter)) - Ensures(Result() == x * arg.parameter) - return x * arg.parameter +def mul(argm: Argument, x: int) -> int: + Requires(Acc(argm.parameter)) + Ensures(Result() == x * argm.parameter) + return x * argm.parameter @Pure @CallSlot -def pure_call_slot(f: F_Type, arg: Argument) -> None: +def pure_call_slot(f: F_Type, argm: Argument) -> None: @UniversallyQuantified def uq(x: int) -> None: - Requires(Acc(arg.parameter) and arg.parameter > 0 and x > 1) + Requires(Acc(argm.parameter) and argm.parameter > 0 and x > 1) - y = f(arg, x) + y = f(argm, x) - Ensures(y > arg.parameter) + Ensures(y > argm.parameter) -def client(f: F_Type, arg: Argument) -> None: - Requires(Acc(arg.parameter) and Acc(arg.result)) - Requires(arg.parameter > 0) - Requires(pure_call_slot(f, arg)) - Ensures(Acc(arg.parameter) and Acc(arg.result)) - Ensures(arg.parameter == Old(arg.parameter)) - Ensures(arg.result > arg.parameter) +def client(f: F_Type, argm: Argument) -> None: + Requires(Acc(argm.parameter) and Acc(argm.result)) + Requires(argm.parameter > 0) + Requires(pure_call_slot(f, argm)) + Ensures(Acc(argm.parameter) and Acc(argm.result)) + Ensures(argm.parameter == Old(argm.parameter)) + Ensures(argm.result > argm.parameter) - arg.result = ClosureCall(f(arg, 20), pure_call_slot(f, arg)(20)) + argm.result = ClosureCall(f(argm, 20), pure_call_slot(f, argm)(20)) def method() -> None: @@ -75,23 +75,23 @@ def method() -> None: else: f = mul - arg = Argument(10, 5) + argm = Argument(10, 5) - @CallSlotProof(pure_call_slot(f, arg)) - def pure_call_slot(f: F_Type, arg: Argument) -> None: + @CallSlotProof(pure_call_slot(f, argm)) + def pure_call_slot(f: F_Type, argm: Argument) -> None: @UniversallyQuantified def uq(x: int) -> None: - Requires(Acc(arg.parameter) and arg.parameter > 0 and x > 1) + Requires(Acc(argm.parameter) and argm.parameter > 0 and x > 1) if f == add: - y = ClosureCall(f(arg, x), add) # type: int + y = ClosureCall(f(argm, x), add) # type: int else: - y = ClosureCall(f(arg, x), mul) + y = ClosureCall(f(argm, x), mul) - Ensures(y > arg.parameter) + Ensures(y > argm.parameter) - client(f, arg) + client(f, argm) - assert arg.parameter == 10 - assert arg.result > arg.parameter + assert argm.parameter == 10 + assert argm.result > argm.parameter