-
Notifications
You must be signed in to change notification settings - Fork 5
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
Decorators called multiple times #46
Comments
Actually this requirement might slightly annoying for peak. So I think we just need to be okay with inner decorators being called multiple times. |
Okay it turns out that this really hard to get right in the face of multiple rewrite groups Suppose I use the strip all decorators before a rewrite group as I suggested (I will use an apply_rewrites style instead of begin/end for the sake of brevity) @wrapper1
@apply_rewrites([...])
@wrapper2
@apply_rewrites([...])
@wrapper3
def foo(...): ... Now the first @wrapper3
def rewriten_foo(...): ... Now after @wrapper3
def rewritten_rewriten_foo(...): ... So in the end we have: @wrapper1
@wrapper3
def rewritten_rewriten_foo(...): ... or if we think about @wrapper1
@wrapper3
@apply_rewrites([...])
@wrapper2
@wrapper3
@apply_rewrites([...])
@wrapper3
def foo(...): ... Note how |
Now one possible solution is to lie. By that I mean serialized a file with only the rewriter stripped but execute the version with all decorators before the rewrite group stripped. So given: @wrapper1
@apply_rewrites([...])
@wrapper2
@apply_rewrites([...])
@wrapper3
def foo(...): ... the first apply_rewrites would execute: @wrapper3
def rewriten_foo(...): ... but serializes: @wrapper1
@apply_rewrites([...])
@wrapper2
@wrapper3
def rewriten_foo(...): ... Now when the second apply rewrites it would get the preceding as the ast and would execute: @wrapper2
@wrapper3
def rewritten_rewriten_foo(...): ... and serialize: @wrapper1
@wrapper2
@wrapper3
def rewritten_rewriten_foo(...): ... So in the end we would get: @wrapper1
@wrapper2
@wrapper3
def rewritten_rewriten_foo(...): ... or @wrapper1
@wrapper2
@wrapper3
@apply_rewrites([...])
@wrapper2
@wrapper3
@apply_rewrites([...])
@wrapper3
def foo(...): ... Which is about as close to correct as we can hope to get without forbidding multiple rewrite groups / requiring rewrites be the innermost decorators. |
I think the idea of effectively applying all the non-rewrite decorators to the final rewritten tree seems to make sense. Assuming those decorators don't do rewrites to the tree (that you would want a subsequent rewrite to see), but it seems difficult/impossible to support this case. Is there a potential use case for a decorator being applied before a rewrite that would cause the result to change if it was applied after? Perhaps one simple conceptual model that we could also try is differentiating between compile/run times (ala macros), so AST rewrites happen before regular decorators which happen at runtime. I think your last solution effectively does this (but allows the decorators to be arbitrarily ordered). One potential issue is side-effects in decorators (e.g. state or globals that cause multiple invocations to have different behavior). What is a peak code example that uses a decorator before applying a rewrite? |
I am not sure if there is a real use case but I could certainly construct an example: def bar(...): ...
def stupid_wrapper(fn):
return bar
@stupid_wrapper
def foo(...): ... Applying
Agreed I think we just have to live with this though.
This is more of code organization thing rather than an actual requirement but However we also decorate |
Actually the destruction of type annotations is an example of why apply a decorator before and after rewrites might have different effects. Now moving to cst will fix that as they preserve type annotations but for now it does matter. |
If possible, I would vote for having rewrites be applied first in the execution order of decorators, so having some convention where passes are run before "normal" python decorators are run. This seems like the simplest organization and allows for some easier assumptions in our design (treat it more like a "compile" time macro or something). I think having these rewrites play nicely with more conventional runtime behavior modifications used in Python will likely be really difficult to have work in the general case. |
I don't really follow. There is no way to get rewrites to be applied before other decorators (unless of course they are innermost). Now a lot of the complexity disappears if we forbid multiple rewrite groups but I think fundamentally inner decorators must be called multiple times and if our users don't like that they will have to not make them inner decorators. |
Yea, I was advocating for them to be the innermost |
(rewrites) |
Hmmm... I think in principle I agree but I also want to be able to apply rewrites automatically and, not simultaneously prevent people from using decorators. I think the just saying that any decorator inside of a rewrite may be called multiple times seems fine. Also the destruction of type annotations by rewrites is pretty problematic. |
I was assuming we could preserve the type annotations, but I guess that involves a big effort to migrate to CST (but we're doing this anyway?). Allowing decorators inside as long as they are "idempotent" does seem okay as a relaxation |
I think I am going to go with my approach of serializing and executing different things. If the rewrites are the innermost than there is no strange side-effects and performs "reasonably" when they are not. |
Ive discovered a bug (maybe undocumented requirement is a better term) that is hard to resolve.
Currently given:
end
will exec:so in effect we get
So
wrapper2
is called twice. Now this is pretty easy to fix, instead of stripping decorators betweenbegin
/end
I just need to strip decorators before begin (in terms of line numbers).end
will exec:so in effect we get
However, everything I just said has a slight error. Not only is
wrapper2
called twice so iswrapper1
.wrapper1
is first called onfoo
and then its return value is feed intobegin
and so forth. It is then called a second time onrewritten_foo
.I don't really see anyway to avoid this as I can't undo the call and I can't assume the changes will be somehow propagated. For example if
wrapper1
sets an attr then it must be called a second a time asrewritten_foo
will not have that attribute set. However ifwrapper1
does something stateful or has differing behavior when called onfoo
andrewritten_foo
then perhaps calling a second time is bad.So I propose that we require rewrites be the innermost decorators.
The text was updated successfully, but these errors were encountered: