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

Fixes for closure in comprehensions and tracing of lambda functions #245

Merged
merged 4 commits into from
Jan 24, 2018

Conversation

MatthieuDartiailh
Copy link
Member

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

@MatthieuDartiailh
Copy link
Member Author

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

codecov-io commented Nov 30, 2017

Codecov Report

Merging #245 into master will increase coverage by 0.02%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
enaml/core/code_tracing.py 82.35% <ø> (ø) ⬆️
enaml/core/enaml_compiler.py 97.29% <100%> (ø) ⬆️
enaml/core/parser/base_parser.py 72.06% <100%> (+0.1%) ⬆️
enaml/core/compiler_common.py 93.55% <100%> (+0.03%) ⬆️
enaml/core/compiler_helpers.py 84.84% <100%> (+0.35%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8ed8a29...6c78bf0. Read the comment docs.

@frmdstryr
Copy link
Contributor

For callbacks to async functions it would be nice.

@MatthieuDartiailh
Copy link
Member Author

MatthieuDartiailh commented Nov 30, 2017

@frmdstryr could you test this code ?

@frmdstryr
Copy link
Contributor

Tests are passing in 2.7 and 3.5 for me.

@frmdstryr
Copy link
Contributor

Also undid my previous workarounds for the listcomp and local var issue and both are working 👍

@MatthieuDartiailh
Copy link
Member Author

@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,
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

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

Copy link
Member Author

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)

Copy link
Member

@sccolbert sccolbert Dec 4, 2017

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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"?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

@MatthieuDartiailh MatthieuDartiailh Dec 9, 2017

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:

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.

@MatthieuDartiailh
Copy link
Member Author

MatthieuDartiailh commented Dec 4, 2017

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 inspected the bytecode both on master (where the test case using the 'test' value) and on this branch. The bytecode on master uses STORE_NAME and LOAD_NAME but fails and to me this is a mystery. On my branch we use a closure and LOAD_DEREF and it works.
Adding locals() just before the comprehension (after test=1) fix the situation on master. Again this does not make sense to me. If you have any suggestions on how to investigate further I am interested.

The last inline post explains the observed behavior.

@MatthieuDartiailh
Copy link
Member Author

I believe I have answered all your questions in my last two posts @sccolbert, could you see if that answers your concerns.

@MatthieuDartiailh
Copy link
Member Author

ping @sccolbert
I guess you are fairly busy but do you think you could review this so that we get the bugfix release out before Christmas.

@MatthieuDartiailh
Copy link
Member Author

ping @sccolbert
I would like to make the next release at the beginning of February and this PR is really needed.

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)
Copy link
Member

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?

Copy link
Member Author

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.

@MatthieuDartiailh MatthieuDartiailh dismissed sccolbert’s stale review January 24, 2018 20:45

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
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.

4 participants