Fix binding freevars in comprehension scope#1073
Fix binding freevars in comprehension scope#1073slozier merged 3 commits intoIronLanguages:masterfrom
Conversation
|
Thanks for the PR! Adding some test cases would be useful: def test_ipy3_gh817(self):
"""https://github.com/IronLanguages/ironpython3/issues/817"""
for x in [lambda: i for i in [1,2]]:
self.assertEqual(x(), 2)
self.assertEqual([tuple(i for j in [1]) for i in [1]], [(1,)])
self.assertEqual({i: tuple(j for j in t if i != j) for t in ((1,2),) for i in t}, {1: (2,), 2: (1,)}) |
|
@slozier nice catch, I actually didn't know there is an issue reported.
|
|
Regarding regression tests, this is already covered by the comprehension tests, e.g. https://github.com/IronLanguages/ironpython3/blob/master/Src/StdLib/Lib/test/test_listcomps.py#L84 In case you want to a specific test for the PR, do you have a convention to do this? Couldn't find other specs related to PRs. |
|
I tried the tests cases I posted above with var engine = Python.CreateEngine();
var scope = engine.CreateScope();
var source = engine.CreateScriptSourceFromString("assert [lambda: i for i in [1,2]][0]() == 2");
var compiledCode = source.Compile();
compiledCode.Execute(scope);If ironpython3/Src/IronPythonTest/Cases/CPythonCasesManifest.ini Lines 573 to 574 in cbfbac2 As for the regression tests I would probably still add them at least to Tests\test_regressions.py since there are also non-listcomp cases (although that one is running with IsolationLevel=PROCESS so it wouldn't have caught the failure mentioned by @BCSharp).
|
|
@isaiah Thanks for the contribution! It's more fun when more minds are engaged with the project. About the tests, To test rewriting, I suggest to add
@slozier, I believe the C# example you have given fails exactly because of the failure in rewriting. So this is another way how it can be tested. As for tackling rewriting issue itself, my suggestion would be to look into lookup rewriting section in When all this works, some cleanup in |
|
@BCSharp thanks for the information, it's very helpful. I managed to reproduce the issue, but i had trouble debug the problematic code, as VisualStudio test explorer failed to discover the NUnit tests, how do you do that normally? |
|
I don't know why your VisualStudio test explorer cannot discover tests, it shows me all 3489 tests (I'm using VS version 16.8.2). But I don't use the test explorer (unless I want to debug tests written in C#) and use the command line instead. For instance, to debug Just place |
|
@BCSharp Could you shed me more light on how the expression rewrite works? Cannot really tell where it splits based on the isolation level. |
|
With the PROCESS isolation level, tests are run by spawning With the SCOPE isolation level, the test case executer creates one engine for all SCOPE-level tests, and only creates a new scope for each test module. The engine is reused. This means that when the test code is compiled by the engine, the references to the global variables are not yet bound to the right scope, which will be provided separately. When the scope is given the code has to be updated, but since expressions are immutable, updating of the global references happens through rewriting of the AST tree. For details how the test isolation levels are handled, look into |
|
@BCSharp @slozier Because of the difficulty to treat a comprehension as a new scope, I used the same technic as the |
|
While I like the idea of using the same approach for comprehensions and generators, I have a few concerns:
|
Yes, there are some performance penalty unfortunately, that it has to use dynamic method binding for the accumulation, instead of the native lock free method.
Currently the parser generates "lowered" AST, which is the form to be reduced. There are two forms of AST at the moment, one for Python, which are defined in At the moment all specs including class Foo():
a = [1,2]
b = [i for i in a]
=> NameError: name "a" is not defined |
|
@isaiah Thank you for putting effort into this challenge. This is by no means an easy one. To have it properly resolved, I believe some parts of the compiler machinery would have to be redesigned. This is obviously not the path you chose here, and that's OK, if we can get things working at reasonable cost. This means that some hacks and kinks in the code are unavoidable, but the performance impact of the current proposed solution in my opinion is too high. Using This is probably where the performance hit comes from. Given that in most of the cases in Python programs comprehensions do not contain embedded generators or lambdas, this is a bad trade-off. If we continue on this path, perhaps a solution could be to only use On the other hand, did you investigate the option of hooking the old comprehension scope up to the scope hierarchy, for the benefit of proper rewriting?
I didn't know it was possible to convert an expression tree back to a Python AST. This is interesting, can you point to me where it is done/used? |
Is this what's causing the failures in |
|
Thanks for the review! @BCSharp What you said makes sense, it could be possible to only generate
there is indeed only one function per comprehension, not for each
this would be the cleanest solution, I imagine if we move the scope handling out of the AST, and make
There are multiple It needs this because the Parser produces only expression tree, if we need Python AST, we have to convert back. re: @slozier
the late two for sure, |
|
Interesting that using a comprehension creates a closure on an unrelated function. The def test_function_closure():
def f(): pass
assert f.__closure__ is None
f = []
[x for x in f]
test_function_closure() |
What I meant is one function for nested comprehensions like [{ y: (z**2 for z in range(x)) for y in range(3)} for x in range(3)]But if we can keep ordinary cases inlined, I suppose this becomes less important.
Oh yes, I knew about them. Sorry, I got confused about the terminology. I thought that "lowered" AST meant reduced AST. So am I assuming correctly that a reduced AST (expression tree) never gets converted back to a Python AST? What about a rewritten AST? If so, we can keep the expression tree produced by the parser aligned with Python, and only create hidden lambdas when really needed. |
|
@BCSharp for cases like: def <dictcomp>():
__ret__ = {}
for x in range(3):
for y in range(3):
__ret__.__setitem__(y, (z**2 for z in range(x)))
return __ret__You might have seen two scopes, because the value itself is an generator expression, if you were talking about merging both scope, probably not easy and will cause other issues. |
[lambda: i for i in [1,2]] should bind the `i` correctly now.
|
Rolled back the AST changes and only kept the outer binding in |
|
As you say, it's an improvement over the current state so if @BCSharp is good with it, so am I. |
|
Yes, I am OK with that, it is a step in the right direction (functionality-wise) and we can always come back to it later. Good to merge. |
|
The specs are green, please approve. |
BCSharp
left a comment
There was a problem hiding this comment.
Looks good to me.
The delay in review had nothing to do with the quality of the work, I was simply not at my workstation.
For future submissions, I'd recommend using a dedicated (per pull request) git branch rather than master. Otherwise the history of your pull request commits is erased the moment you reset your master to the upstream master and push it to GitHub again.
|
Thanks for all the work! |
|
And my thanks! |
|
Thank you, looking forward to contributing more.
Good point, will keep that in mind. |
[lambda: i for i in [1,2]]should bind theicorrectly now.