Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closures call slots: First analysis implementation #92

Open
wants to merge 32 commits into
base: master
Choose a base branch
from

Conversation

BenWeber42
Copy link
Collaborator

@BenWeber42 BenWeber42 commented Sep 26, 2017

This change is Reviewable

Copy link
Owner

@marcoeilers marcoeilers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good overall

Decorator to mark a method as a call slot declaration.
"""

def call_slot_handler(*args, **kwargs) -> Any:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this code here? Is it supposed to be an example usage or... what?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is needed for normal runs (without the verifier). It makes sure no code is run when Python comes accross a call slot application.
The way it works is that whenever there is a '@CallSlot' decorator on a function, that function will be replaced with 'call_slot_handler'. 'call_slot_handler' does nothing, but has to support both calls call_slot_handler(...) and call_slot_handler(...)(<<uq vars>>) (the latter is supported through the 'uq_handler').

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay. It's fine to assume that there'll be some postprocessing to take the annotations out though; if it's any effort at all, you shouldn't have to worry about this, as long as you make sure that there's enough information in the annotated code to enable the postprocessing.


def ClosureCall(call: Any, justification: Any) -> Any:
"""
Justifies a closure call by proving static dispatch (in contrast to using
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure I agree with the description here. What does "proving static dispatch" mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

@@ -453,6 +457,12 @@ 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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that you create a new CallSlotAnalyzer for every call slot, right? Is that necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to reusing the same CallSlotAnalyzer in the Analyzer class.

@@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have said from nagini_translation.call_slot_analyzers import CallSlotAnalyzer, CallSlotProofAnalyzer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

@@ -0,0 +1,280 @@
import ast
from typing import Union
from nagini_translation import analyzer as analyzer_pkg
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only need this for type annotations, I wouldn't bother, I would just write 'Analyzer' in the annotation. We're not using a type checker anyway, so it's only for documentation purposes, and it's clear what it refers to.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

analyzer.outer_functions.pop()
analyzer.current_function = None

def _check_body(self, body):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotations would be nice

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

self._check_call_declaration(child.value)

elif isinstance(child, ast.Assign):
self._check_call_declaration(child)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work for methods that don't return anything? I guess it's possible that there are no target variables, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled by the if body above. If there are no target variables it shouldn't be an assignment, but an ast.Expr.

https://greentreesnakes.readthedocs.io/en/latest/nodes.html#Expr

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, I overlooked that somehow.


assert len(node.targets) == 1

if isinstance(node.targets[0], ast.Name):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be fine, at the very least initially, to make your life a bit easier and require that you have at most one, normal target. Then you can replace that directly with Result in the generated method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll try to support 'simple tuples' (without nesting) for now. It seems simple enough but should also cover the majority of use cases.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me, but if you drop anything for time reasons, this kind of stuff (syntactic niceties) would be the first thing I'd drop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed as discussed in our last meeting (change not pushed yet).

("Method '%s' contains illegal keyword parameters"
% method.node.name)
)
# TODO: what about defaults?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd forbid using defaults

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

@@ -20,6 +20,7 @@
_SIF_TESTS_DIR = 'tests/sif/'
_IO_TESTS_DIR = 'tests/io/'
_OBLIGATIONS_TESTS_DIR = 'tests/obligations/'
_CLOSURES_TESTS_DIR = 'tests/closures/'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, this is all really neat. Nice.

@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably return after this?

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen?

@@ -1,95 +1,292 @@
import ast
from typing import Union, List
from typing import Union, List, Set
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally we sort imports alphabetically. First all import statements, then all from ... import ... statements, sorted by the name of the module, and then the imported names sorted by name.

"Illegal reference to non-local name '%s'" % illegal_variable_uses[0].id
)

class _AllowedVariablesChecker(ast.NodeVisitor):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to be a nested class?

set correctly.

* No magic name ('__...__')
* Return type = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we need to enforce this return type?

'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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like you need two different cases for this.


self.call_slot = self.analyzer.node_factory.create_call_slot_proof(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird that you store a proof object in a field called call_slot

elif (
len(self.call_slot.return_variables) != 1 or
self.call_slot.return_variables[0].id != node.targets[0].id
):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks.... weird. I'm not sure how to do it instead, but to me, this looks weird.

"ClosureCall's justification has to instatiate uq variables if it's a call slot"
)

if not isinstance(justification.func.func, ast.Name):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that would already work at this point (maybe you're still building up the necessary data structures), but in principle, you should be able to say get_target(justification) and make sure that it gives you e.g. a PythonMethod or a CallSlot object. Then you don't have to bother with checking that it uses an ast.Name etc.

F_Type = Callable[[int, int], int]


#:: IgnoreFile(42)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the number here should refer to an issue in the issue tracker because of which this file should be ignored. If this one isn't ready yet and depends on not-yet-implemented features, IMO the test just shouldn't be in the PR yet.

Also, IgnoreFile should generally be at the top of the file, and this seems like it should eventually be a verification test, not a translation test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants