-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Let/const #431
Let/const #431
Conversation
In order to support let/const, we need to know what kind of scope each block is. Currently, the only blocks we have are what I am calling "Hoist" blocks. Whenever an entry which should be hoisted is added to the env, it should be added to the closest "Hoist" block. Let/const identifiers should be added to the nearest "Block" block. Currently we only create blocks corresponding to hoist points. Subsequent commits will need to create a great many more blocks to support block-level scope. NB: We should probably rename "block" to "scope."
The big change here is that I changed `init_env` to take an extra boolean parameter, which when true indicates that the nearest "Hoist" block should be extended. Obviously, there is a lot of logic missing here. I suspect I'll need to add block scope information to the block entries themselves, but so far the only roadblock I hit I don't quite understand, so I'm reaching out.
Also restore "basic" test for let and duplicate let tests for const.
Also reorganize a bit: common.js has features that are relevant to both let and const.
Freebie from the parser!
I extracted variable_declaration from outside of statement_decl. I changed the name to variable_decl to mirror statement_decl, but would be happy to revert that. I don't totally understand the change I made to widen_env, so please take a close look there. This is very similar to a change I made in clear_env, where the modification happens in all scopes up to the closest VarScope. Once I understand this better, and if it is clear that is the correct thing to do, that pattern should be extracted out into a function.
type scope = scope_entry SMap.t ref | ||
|
||
type scope_kind = | ||
| VarScope (* var, class, functions hoisted up to this point *) |
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.
FYI: Classes are actually let-scoped
To handle some TDZ cases and const reassignment, I'll need to track more information that I do now about the scope entries. I have an idea for how I can proceed with that, but if anyone has a preference for how I should proceed, I appreciate all input. |
Regarding TDZ, I'm not sure it's possible to (in general) prevent errors like this: function bar() {
console.log(foo);
}
bar();
let foo = "foo"; because the following is valid: function bar() {
console.log(foo);
}
let foo = "foo";
bar(); Thoughts? |
Right, some TDZ errors for For now, same-env TDZ errors seems like a good start |
1. function scopes (VarScope) are lexical, too 2. vars hoist outside of non-var scopes
@@ -870,7 +865,7 @@ and statement_decl cx = Ast.Statement.( | |||
let _, { Ast.Identifier.name; _ } = id in | |||
let r = mk_reason (spf "type %s" name) loc in | |||
let tvar = Flow_js.mk_tvar cx r in | |||
Env_js.init_env cx name (create_env_entry ~for_type:true tvar tvar (Some loc)) | |||
Env_js.init_env cx name (create_env_entry ~for_type:true tvar tvar (Some loc)) VarScope (* TODO: hoist behavior *) |
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.
@jeffmo What do you think the scoping behavior for type aliases should be?
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.
Definitely var-scoped -- type aliases need recursion
This is important because type aliases can be mutually recursive. I added a test for this case, even though it currently passes with a non-var lexical scope, as we haven't implemented any TDZ yet.
@@ -327,7 +401,7 @@ let string_of_env cx ctx = | |||
let install_refinement cx x xtypes = | |||
if exists_in_scope x (peek_env ()) = None then | |||
let t = SMap.find_unsafe x xtypes in | |||
init_env cx x (create_env_entry t t None) | |||
init_env cx x (create_env_entry t t None) VarScope (* TODO: verify correct hoist behavior *) |
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 pretty sure that the scope of the refinement depends on the type of entry corresponding to x. Furthermore, code that relies on exists_in_scope
/peek_env
instead of exists_in_env
/scope_kind
is generally suspect.
Unfortunately, we don't generally know the scope kind of the entry here, and I'm not even sure we can thread it through. The refinement logic is still a bit opaque to me, but I'll try to come up with some test cases to stress this code. Help here would be appreciated.
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.
OK, so I think I have two tests that show the refinement scope needs to depend on the scope of the thing being refined.
Here is why just VarScope
doesn't work:
function scope() {
{
let x: {p:?string} = {p:"xxx"};
if (x.p == null) {
return;
}
}
x.p // should fail, but $REFI x.p is hoisted to here, so there is no warning
}
Here is why just LexicalScope
doesn't work:
function scope() {
var x: {p:?string} = {p:"xxx"};
{
if (x.p == null) {
return;
}
}
var y: string = x.p // should pass, but refinement doesn't live long enough
}
@gabelevi, you helped me understand this refinement stuff in the first place (thanks!). Do you see any problem with my reasoning here? It seems like the scope_kind of the refinement needs to depend on the scope_kind of the thing being refined (x
in this case).
If that's true, we currently don't store that information beyond init_env
, so I believe I'd need to add scope_kind to the scope_entry struct. (If true, this isn't so bad, because I think we need to add this kind of information to support TDZ use cases anyway.)
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've implemented this in 4659a41. Mostly sure it's correct :)
Only `declare var` syntax is implemented. There is no `declare let` or `declare const`.
Modules and imports can only ever be at the top level, and imports should be hoisted.
While work on Let/Const continues, is there a way to tell flow to treat Let and Const as simple vars and continue to work. Many of us have been using Babel for our code, and use Let and Const instead of var everywhere. The fact that const isn't re-assigned is checked by Babel. But flow completely fails on such code. Having it treat Let and Const the same as var for now would be a great temporary fix. I tried to make a change in the source itself, but I don't know enough OCaml. :( |
Voting for @nmn's suggestion. I'm on an es6 codebase, want to get Flow going. Choices are to change lets/const -> var (and incur the political capital of selling that to team), or not using Flow. Would much rather use Flow sooner than later. An interim flag to treat let/const as "var" would be ideal. |
@nmn @winkler1 Treating function bad1() {
if (true) {
let foo = "local var";
}
return foo; // out of scope, but flow doesn't know
} So, unfortunately, you'll have to sit tight a bit longer while we get this right. Sorry for the delay! |
Flow might not catch it but eslint (with babel-eslint) sure does. Flow is primarily a type checker, so I'd think most people interested in reporting like that also use a linter, if only as a backup. +1 for doing the wrong thing before doing the right thing. |
Doing the wrong thing in the mean time was only a suggestion. I'm not sure how much sense it makes from a time commitment point of view. But while I understand the fact that it would not catch errors, it's still good to be able to at least flow. As of now, it's more of a all or nothing approach. The Let/Const pull request has been around for a while, and I've been watching it patiently for a while. If it's close to being merged than this whole discussion is pointless anyway! |
If you are truly intent on treating let/const as var, it's not hard to make that happen in a fork. Just change the Another possibility: You might be able to run babel with a limited number of transforms using the "whitelist" option. If you just whitelisted the "es6.blockScoping" transform, flow could process the post-transformed source, and you wouldn't run into the pitfalls of using flow on fully transformed source files. Lastly, this feature branch is actually feature complete—but it's pending some internal refactoring. I haven't bothered to keep this up-to-date with master, so it's a bit behind on other features. If you want to go ahead and use this branch, please do let me know if you run into any issues. |
@samwgoldman thanks for pointing me to the right place. I am finally able to use flow on my code! |
+1 Thank for working on supporting let/const and hope it gets merged soon |
👍 |
@bsr203 @fredericksilva If you are using babel to transpile your code, you may be happy enough with my fork of flow: github.com/nmn/flow |
thanks @nmn your fork let me use |
Quick interim update - unf. delay will last a little longer, it's due to some refactoring I need to land in front of Sam's awesome work. I'm back on the grid end of this week, will update status here. |
Thanks @nmn, let's see how your work flow. 👊 |
@bhosmer Any updates on this? |
Is const + destructing in scope of this PR? At the moment, code like |
@matklad Thanks for the report! Looks like I mishandled destructuring, so I'm throwing an unnecessary error there. I will add a test case for that once the refactoring lands and I pick this up again. |
i'm not keeping up here, so apologies if this is already handled, but this is seriously wtf so hopefully we can detect it properly: http://es-discourse.com/t/why-typeof-is-no-longer-safe/15 |
@mroch We do handle a number of TDZ errors, although it's worth adding an explicit test case for |
All right. Looks like the refactoring landed (@bhosmer, let me know if there's more in the pipeline), so now I'm the one blocking this. |
@samwgoldman nothing imminent - the one nontrivial remaining loose end is far less important than your stuff and can wait. Definitely happy to walk through the new stuff to whatever extent you want BTW. |
+1 |
superseded by #784 |
Rebased #404 on top of the
block
→scope
rename.i++
)typeof
expressionsLeft for future work