-
-
Notifications
You must be signed in to change notification settings - Fork 130
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
Fixes for closure in comprehensions and tracing of lambda functions #245
Fixes for closure in comprehensions and tracing of lambda functions #245
Conversation
@sccolbert in theory this could allow to define function in declarative functions and event handler but I do not see a compelling use case to remove this restriction. |
Codecov Report
@@ Coverage Diff @@
## master #245 +/- ##
==========================================
+ Coverage 63.93% 63.95% +0.02%
==========================================
Files 287 287
Lines 23356 23372 +16
==========================================
+ Hits 14932 14948 +16
Misses 8424 8424
Continue to review full report at Codecov.
|
For callbacks to async functions it would be nice. |
@frmdstryr could you test this code ? |
Tests are passing in 2.7 and 3.5 for me. |
Also undid my previous workarounds for the listcomp and local var issue and both are working 👍 |
@sccolbert this is the last pending PR that needs to be in before the next bugfix release that I plan mid-december. It would be terrific if you could give it a look. |
@@ -755,5 +778,5 @@ def add_decl_function(node, func, is_override): | |||
'validate_spec': validate_spec, | |||
'validate_template': validate_template, | |||
'validate_unpack_size': validate_unpack_size, | |||
'call_func': call_func, |
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.
there's no need to remove this, just add the new helper.
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 removed it because I was the one to add it in the first place when adding first support for comprehensions.
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.
👍
def wrapper(*args, **kwargs): | ||
return call_func(func, args, kwargs, scope) | ||
|
||
update_wrapper(wrapper, func) |
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.
why this over @functools.wraps
?
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.
No reason I will use functools.wraps
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.
Actually it is because wraps expects the decorator to take as single argument the function.
|
||
mod = ast.Module(body=[func_node]) | ||
ast.fix_missing_locations(mod) | ||
|
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 don't think this is correct. Can you give an example of the closure variables you're hoping to pick up with this change?
Also, I don't think this is going to work as you expect. When the ::
operator is run to generate the expression handler, it wraps the generated bytecode in it's own function, which is then invoked by the handler: https://github.com/nucleic/enaml/blob/master/enaml/core/operators.py#L70 So each time the notification is triggered, this new internal function is going to be defined, instead of executed. I'm not entirely sure how this is passing tests.
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 look at the compilation of the ast for the operator you will notice that I extract the body of the function that I then pass onward. I simply chose to keep the ast manipulation located in the parser, but I could do the wrapping in the compiler.
As far as closure are concerned the point is that if one compile the following:
a = 1
[a for i in range (2)]
In a top level module a is accessed using LOAD_GLOBAL which fails. By wrappring the expression in a function, a closure is used. The point is that we need either to directly compile the right function or simply extract its code which is what we currently do.
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.
But your example already works without this change, because all LOAD_GLOBALS
are re-written to LOAD_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.
Can you give me an example of something that doesn't work currently, that this change fixes? Also, can you link me to where you "extract the body of the function and pass it forward"?
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, you mean that doesn't work on Python 3. Sorry.
We just need to recursively rewrite LOAD_GLOBAL to LOAD_NAME for nested code objects.
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 is not so simple because LOAD_NAME fails to access fast locals of the outer scope. I will answer in more details later. I am attending a conference this week.
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 don't think there's anything in fast locals that the inner code will need to access.
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.
Still looking at this kind of comprehension:
a = 1
[a for i in range (2)]
Digging deeper I found that:
- when Python compile the code as if it was a top-level module (which it does without this fix), it uses STORE_NAME and LOAD_GLOBAL to handle
a
- to unify the handling of comprehensions between operators and declarative functions I refactored the handling of global variables such that as LOAD_GLOBAL and STORE_GLOBAL are replaced by LOAD_NAME/STORE_NAME save if the variable is marked using the
global
keyword, both in declarative functions and operators. Currently this is not done if there is no comprehension in an operator which is inconsistent. cf https://github.com/nucleic/enaml/blob/master/enaml/core/compiler_common.py#L685 - finally when creating the operator (https://github.com/nucleic/enaml/blob/master/enaml/core/operators.py#L156), we optimize the code for locals (https://github.com/nucleic/enaml/blob/master/enaml/core/operators.py#L49) which replaces LOAD_NAME/STORE_NAME for local variables by LOAD_FAST/STORE_FAST.
As a consequence a is not stored in the namespace, which explains the failure.
From my point of view the proposed fixed is the more correct since the body of a notification operator is truly a function body (even without return and yield). Alternatively we could suppress the global variable analysis in operators. Alternatively we can suppress the locals optimization in gen_simple.
I will update the code to always analyse global variables in operators for the time being, so that the current behavior is consistent.
I will try to answer all your concerns point by point. First concerning the extraction of the code in the compiler the relevant change is https://github.com/MatthieuDartiailh/enaml/blob/b779e5ae62a0cda8e90e85874c1a33bff23ed337/enaml/core/compiler_common.py#L683. As we know that the code has been wrapped in a function we find its code object and pass it along. As mentioned, I can move this part back into the compiler_common if you want. Concerning your other concerns, I created a small gist testing just the problematic cases https://gist.github.com/MatthieuDartiailh/578b6c7c13b65f966a7cab8c41e7f300. You can look at the bytecode by adding the following at https://github.com/MatthieuDartiailh/enaml/blob/b779e5ae62a0cda8e90e85874c1a33bff23ed337/enaml/core/compiler_common.py#L697. for op, op_arg in bp.Code.from_code(code).code:
print(op, op_arg)
if isinstance(op_arg, bp.Code):
for op2, op_arg2 in op_arg.code:
print(' ', op2, op_arg2)
|
I believe I have answered all your questions in my last two posts @sccolbert, could you see if that answers your concerns. |
25a2a59
to
d77804b
Compare
ping @sccolbert |
ping @sccolbert |
enaml/core/compiler_common.py
Outdated
code = compile(node.value.ast, cg.filename, mode=mode) | ||
for op, op_arg in bp.Code.from_code(code).code: | ||
if isinstance(op_arg, bp.Code): | ||
rewrite_globals_access(op_arg, global_vars) |
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 this duplicating logic from line 697 below?
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 like it is indeed. I will fix it.
All point addressed and agreement on hangout
Wrap functions defined inside operators or declarative function to call them with their scope of definition. This allows to handle properly comprehensions and lambdas (whose body is now properly traced). Also ensure that we compile the body of the :: operator as a function to properly handle closure. Bump the compiler version to 26
3550ad8
to
6c78bf0
Compare
Wrap functions defined inside operators or declarative function to call them with their scope of definition. This allows to handle properly comprehensions and lambdas (whose body is now properly traced). Also ensure that we compile the body of the :: operator as a function to properly handle closure.
Bump the compiler version to 26
I added tests for all the new behaviors