-
-
Notifications
You must be signed in to change notification settings - Fork 648
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
Call-by-name syntax migration goal #20714
Call-by-name syntax migration goal #20714
Conversation
…base, looking for statements of interest
…ll comments and whitespace - Could use the AST representation to replace lines, or jump out to tokenize to perform the replacement
… try to YOLO it on the repo
…r form to the migration
- Easier to compose later, or pull out exactly what's needed - rather than trying to do that after
- Those should be manually updated
…main migration cases
- Made added imports deterministic
@benjyw Two items here I'd like some direction on, if you get a chance:
Haven't touched anything related to Is My naive assumption is that it's still just a co-routine collector, same as
I'm not entirely sure where to even start here. There are a bunch of calls that just don't even register in the Rust rule graph call-by-syntax mapper. I think an example I gave was: In the associated issue, Stu mentioned:
Do you have any idea what is going on with this one? |
Re names - yes, all rules should have names now. Context: #19755 Is something not working vis-a-vis the intrinsics names? See src/rust/engine/src/intrinsics.rs specifically for those names |
Re MultiGet: IIUC it should be renamed |
Yeah, so the generated rule graph isn't showing a handful of called-by-name functions. So, the example of But based on your comment, this set of intrinsics is where I should be looking? If so, perfect - having not dug through the rust code yet, I was at a bit of a loss.
|
Yeah that is mapped to a rust function, not a python one. |
- Put a direct call to the constructor as an arg, instead of burying it in implicitly
- Replaced re-export of MultiGet due to autoflake breaking - Added more docs
Added you all to the PR. I've got some lame tax paperwork to do for the couple days, but I wanted to start getting some eyeballs on this. When I'm back, I'm going to work on some cleaning up and re-factoring of the code for a few things that bother me, and to see if I can find a cleaner way to satisfy the type checker other than the barrage of I've pulled off intrinsics and partials as separate tickets, as I think those could be rabbit hole investigations, and I didn't want to delay getting the 90% tool into a PR.
It's worth noting there are LOTS of places that aren't migrated, as we're not doing a find/replace, but navigating through the AST. For example, code like |
pex = await Get( | ||
VenvPex, | ||
PexRequest, | ||
# Some comment that the AST parse wipes out, so we can't migrate this call safely |
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.
Embedded comments are wiped out by the AST. There are ways we can try to move this around at replacement time, but effort to value might not be there - especially as we ensure that the comment loses some context when we summarily move it.
I'd rather let the user know to either manually move/remove/replace the comment, or to extract the code with an embedded comment out. In the Pants repo, we have these cases specifically around Process calls with embedded 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.
Yea, that seems fine with me as long as the warning is rendered to remind people to look for the stripped comments in code review.
That looks awesome: kudos! Will review this weekend. |
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 great: thanks so much @sureshjoshi!
The migration plan is a JSON representation of the rule graph, which is generated by the | ||
engine based on the active backends/rules in the project. | ||
|
||
Each item in the migration plan is a rule that contains the old `Get` syntax, the associated | ||
input/output types, and the new function to directly call. The migration plan can be dumped as | ||
JSON using the `--json` flag, which can be useful for debugging. For example: |
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.
The migration plan is a JSON representation of the rule graph, which is generated by the | |
engine based on the active backends/rules in the project. | |
Each item in the migration plan is a rule that contains the old `Get` syntax, the associated | |
input/output types, and the new function to directly call. The migration plan can be dumped as | |
JSON using the `--json` flag, which can be useful for debugging. For example: | |
A JSON migration plan can also be emitted, which contains the old `Get` syntax, the associated | |
input/output types, and the new function to directly call. The migration plan can be dumped as | |
JSON using the `--json` flag, which can be useful for debugging. For example: |
def map_no_args_get_to_new_syntax( | ||
self, get: ast.Call, deps: list[RuleGraphGetDep] | ||
) -> tuple[ast.Call, list[ast.ImportFrom]]: | ||
"""Get(<OutputType>) -> the_rule_to_call(**implicitly())""" |
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.
FWIW: If the called rule doesn't take any arguments, you can skip the **implicitly()
, and just call it as the_rule_to_call()
.
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, I didn't know if there were any ambient arguments that could still be passed without an explicit Get arg.
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.
...oh. Hm. Good question. 😬
|
||
logger.debug(ast.dump(get, indent=2)) | ||
assert len(get.args) == 2, f"Expected 2 arg, got {len(get.args)}" | ||
assert isinstance(output_type := get.args[0], ast.Name), f"Expected Name, got {get.args[0]}" |
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 is a neat pattern!
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.
walrus FTW
Though, the lack of AST inference is brutal - I need to extract some utility AST stuff to support our use cases - too much dupe code to make the type checker happy (but, the value/need in making the type checker happy with AST code is really high)
def map_long_form_get_to_new_syntax( | ||
self, get: ast.Call, deps: list[RuleGraphGetDep] | ||
) -> tuple[ast.Call, list[ast.ImportFrom]]: | ||
"""Get(<OutputType>, <InputType>, input) -> the_rule_to_call(**implicitly(input))""" |
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 seeing why there is a difference between map_long_form_get_to_new_syntax
and map_short_form_get_to_new_syntax
in terms of the output syntax: it's clear why the input is different, but the "if the called function only takes a single argument with a type that matches the argument" case applies to both of them I think.
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.
Yeah, I updated the short-form syntax before pushing this PR, but didn't give much thought to the long-form at the time.
From the examples I've seen in the code, the "short-form" syntax calls the XYZRequest constructor, so replacing that is pretty simple.
But the long-form calls seem to have a wider variety of call patterns, and one of the original calls I tried to replace similar to the short-form ended up failing the type checker due to some missing context (but works when called using implicitly(dict)
I'll take a look back at this, to see if it was a runtime or just type check problem.
def _maybe_replaceable_call(self, statement: ast.stmt) -> ast.Call | None: | ||
"""Looks for the following forms of Get that we want to replace: | ||
|
||
- bar_get = Get(...) | ||
- bar = await Get(...) | ||
This function is pretty gross, but a lot of it is to satisfy the type checker and | ||
to not need to try/catch everywhere. | ||
""" | ||
if ( | ||
isinstance(statement, ast.Assign) | ||
and ( | ||
isinstance((call_node := statement.value), ast.Call) | ||
or ( | ||
isinstance((await_node := statement.value), ast.Await) | ||
and isinstance((call_node := await_node.value), ast.Call) | ||
) | ||
) | ||
and isinstance(call_node.func, ast.Name) | ||
and call_node.func.id == "Get" | ||
): | ||
return call_node | ||
return 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.
AFAIK, any call to Get(..)
should be replaceable to function_call(..)
: the only difference should be that before replacement you get out a Get[T]
, and after replacement you get out an Awaitable[T]
... so it might break some type checking I suppose. But maybe it would leave you happier with this code?
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.
Yeah, this is more about satisfying the type checker, as if the call is awaitable, we access a different part of the AST vs if it's just straight callable. This will be pulled out to a utility to at least make the primary code more linearly readable.
pex = await Get( | ||
VenvPex, | ||
PexRequest, | ||
# Some comment that the AST parse wipes out, so we can't migrate this call safely |
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.
Yea, that seems fine with me as long as the warning is rendered to remind people to look for the stripped comments in code review.
- Playing some type gymnastics - Removed assertions in lieu of proper throwable errors (in case asserts are optimized away)
This PR creates a native Python built-in goal which performs the call-by-name migration for passed in files/targets (covers items 1 and 4 in #20572).
Note: This is not a completed feature, as per the "split off tickets" below.
Outstanding items in progress:
concurrently
(review what else is required here)Out of scope of this PR, but
migrate-call-by-name
isn't a great name. This could be a good jump off point for a genericmigrate
goal with options/sub-goals (ala Swift's or Svelte's native migration tools).See #20572