-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Avoid free variable conflicts within descendant scopes instead of global #5398
base: main
Are you sure you want to change the base?
Conversation
Instead of a global `referencedVars` option that is computed at the lexer level (via all IDENTIFIER tokens), recurse through the AST to mark each scope with all variables used within it and descendant scopes. Likely also more efficient via `Set`s instead of `Array`s. Also fix some missing `children` in `For` nodes, which otherwise caused this rewrite to fail. Fixes jashkenas#4865 by causing parameter renaming in fewer (more predictable) scenarios.
This seems fine. Any idea why the prior implementation was written the way that it was? Or put another way, are there any disadvantages of this refactor? Also wouldn’t |
@lydell wrote the original, and in #4865 (comment) says:
So the main reason was simplicity. I imagine the original initialization is also somewhat faster (array scan vs tree scan), though lookups should be faster here; and the original approach was more robust to mistakes in I agree that the new approach doesn't help renaming if the name is in the same scope (or descendant scope), but in that scenario the renaming is not surprising, whereas the current nonlocal behavior is very surprising (and especially annoying with jsdoc). @lydell, do you agree that this is a good approach?
Yes, I had the same thought. I imagine |
I ran I want to leave this open for a few days in case @lydell or anyone else wants to review or discuss it, but it looks good to me.
I guess the issue with potentially outputting a declaration/first assignment “in place,” as opposed to moving it up to where the |
I won’t be reviewing, but thanks for asking! |
I'm excited for this! Is there anything I can do to help? |
This one was pretty much ready to merge, but I was hoping it could get another reviewer. If you don’t mind reviewing it, and updating it per latest While you review, my main concern with this PR and with #5395, though more so with #5395, was that I found the code related to scope-tracking quite dense and hard to follow. It could benefit from a lot more commentary, from you or @edemaine if you have time to better explain what’s going on. See the review of #5395 to get a sense of the parts I found confusing. |
Sorry for the long hiatus. In some ways, coming back to this code after a long delay is good for seeing how confusing it is. 🙂
I added a bunch of comments to document how this specific PR works. There's still a lot to explain how the I also merged with |
d7e6609
to
2ab383d
Compare
@@ -106,6 +100,7 @@ exports.compile = compile = withPrettyErrors (code, options = {}) -> | |||
ast.tokens = tokens | |||
return ast | |||
|
|||
nodes.findReferencedVars() |
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.
What is this doing? Populating the referencedVars
property of the root and all children? Can we add a comment explaining this?
# `findReferencedVars` recurses through the source parse tree to mark every | ||
# "scope" parse node (`Root` and `Code`s) with all referenced | ||
# (accessed or assigned) variables in that source scope, so that every scope | ||
# knows what to avoid when generating new variable names. | ||
# Specifically, it takes in an array of `scopeNodes`, | ||
# each of which already has a `@referencedVars` attribute that is a `Set`. | ||
# For each found reference to an identifier (see `IdentifierLiteral`'s | ||
# override for `findReferencedVars`), it adds the name (as a `String`) | ||
# to each scope node's `referencedVars` Set. |
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.
Great comment, just updating typography to match (most, if not all) other comments.
# `findReferencedVars` recurses through the source parse tree to mark every | |
# "scope" parse node (`Root` and `Code`s) with all referenced | |
# (accessed or assigned) variables in that source scope, so that every scope | |
# knows what to avoid when generating new variable names. | |
# Specifically, it takes in an array of `scopeNodes`, | |
# each of which already has a `@referencedVars` attribute that is a `Set`. | |
# For each found reference to an identifier (see `IdentifierLiteral`'s | |
# override for `findReferencedVars`), it adds the name (as a `String`) | |
# to each scope node's `referencedVars` Set. | |
# `findReferencedVars` recurses through the source parse tree to mark every | |
# “scope” parse node (`Root` and `Code`s) with all referenced | |
# (accessed or assigned) variables in that source scope, so that every scope | |
# knows what to avoid when generating new variable names. | |
# Specifically, it takes in an array of `scopeNodes`, | |
# each of which already has a `@referencedVars` attribute that is a `Set`. | |
# For each found reference to an identifier (see `IdentifierLiteral`’s | |
# override for `findReferencedVars`), it adds the name (as a `String`) | |
# to each scope node’s `referencedVars` Set. |
@@ -515,9 +528,16 @@ exports.Root = class Root extends Base | |||
super() | |||
|
|||
@isAsync = (new Code [], @body).isAsync | |||
@referencedVars = new Set # all referenced variable names in this scope |
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.
@referencedVars = new Set # all referenced variable names in this scope | |
# This set contains all the referenced variable names in this scope. | |
@referencedVars = new Set |
|
||
children: ['body'] | ||
|
||
findReferencedVars: (scopeNodes = []) -> | ||
# This is called at the top level to start the recursion, |
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 there any way to make this “automatic,” in the sense that we don’t need to remember to call findReferencedVars
on the root, outside of nodes.coffee
, to trigger this recursion? I assume we probably can’t call it in Root
‘s constructor, as that would be too early? Could it happen within compile
or compileNode
, as maybe those get called after all the child nodes are added?
@@ -3904,6 +3929,7 @@ exports.Code = class Code extends Base | |||
@isGenerator = no | |||
@isAsync = no | |||
@isMethod = no | |||
@referencedVars = new Set # all referenced variable names in this scope |
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.
@referencedVars = new Set # all referenced variable names in this scope | |
# This set contains all the referenced variable names in this scope. | |
@referencedVars = new Set |
@@ -5342,7 +5373,7 @@ exports.For = class For extends While | |||
@addBody body | |||
@addSource source | |||
|
|||
children: ['body', 'source', 'guard', 'step'] | |||
children: ['body', 'name', 'index', 'source', 'guard', 'step'] |
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 are these added? I don’t see the connection to the “referenced vars” change. Not opposed, but I’m wondering if this should be here (or is part of a separate goal).
@@ -49,7 +47,8 @@ replDefaults = | |||
isAsync = ast.isAsync | |||
# Invoke the wrapping closure. | |||
ast = new Root new Block [new Call ast] | |||
js = ast.compile {bare: yes, locals: Object.keys(context), referencedVars, sharedScope: yes} | |||
ast.findReferencedVars() |
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.
Per my comment above, I’m wondering if we could do without this call, that it could happen automatically as part of assembling the nodes tree.
Instead of a global
referencedVars
option that is computed at the lexer level (via all IDENTIFIER tokens), recurse through the AST to mark each scope with all variables used within it and descendant scopes.Likely also more efficient via
Set
s instead ofArray
s.Also fix some missing
children
inFor
nodes, which otherwise caused this rewrite to fail.Fixes #4865 by causing parameter renaming in fewer (more predictable) scenarios.
I've separated into separate commits for:
cake build
a second time), so you can see the reduced renaming.