-
Notifications
You must be signed in to change notification settings - Fork 8
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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').
There was a problem hiding this comment.
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.
src/nagini_contracts/contracts.py
Outdated
|
||
def ClosureCall(call: Any, justification: Any) -> Any: | ||
""" | ||
Justifies a closure call by proving static dispatch (in contrast to using |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.
src/nagini_translation/analyzer.py
Outdated
@@ -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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/nagini_translation/analyzer.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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/' |
There was a problem hiding this comment.
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.
f1406f0
to
3222cc0
Compare
First attempt through an external 'CallSlotAnalyzer'. Might not work so well, might need to modify the Analyzer directly.
3222cc0
to
f03106f
Compare
f03106f
to
2f55236
Compare
Progressed with CallSlotAnalyzer
@@ -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) |
There was a problem hiding this comment.
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?
src/nagini_translation/analyzer.py
Outdated
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 | ||
): |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
This change is