Skip to content
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 #784

Closed
wants to merge 14 commits into from
Closed

let/const #784

wants to merge 14 commits into from

Conversation

samwgoldman
Copy link
Member

This commit allows for the creation of let/const bindings and ensures
all block-scoped bindings are installed into the appropriate scope. This
involves creating new LexScopes, as outlined in test/bindings/scope.js.

Before this change, the environment stack, i.e., frames, changesets, and
scopes, would change in lockstep. Now, lex scopes change separately from
frames and changesets via push_lex and pop_lex.

When adding heap refinements, we may traverse over lex scopes to find
the scope where the base object was bound.

In unusual cases, like try/catch, envs may have an uneven number of
scopes when merging. The uneven scopes will always be lex. I have
updated merge_env to accept this imbalance.

Note that this is not a complete implementation of let/const:

  • switch statements should forbid rebinding lexicals between cases
  • functions, amazingly, are block scoped and hoisted in ES2015
  • numerous TDZ cases, surely, which I have not yet investigated
  • can't update const bindings (assignment already handled)
  • var bindings can not shadow a lexical binding, ever
  • more tests

This commit allows for the creation of let/const bindings and ensures
all block-scoped bindings are installed into the appropriate scope. This
involves creating new LexScopes, as outlined in `test/bindings/scope.js`.

Before this change, the environment stack, i.e., frames, changesets, and
scopes, would change in lockstep. Now, lex scopes change separately from
frames and changesets via `push_lex` and `pop_lex`.

When adding heap refinements, we may traverse over lex scopes to find
the scope where the base object was bound.

In unusual cases, like try/catch, envs may have an uneven number of
scopes when merging. The uneven scopes will always be lex. I have
updated `merge_env` to accept this imbalance.

Note that this is not a complete implementation of let/const:
* switch statements should forbid rebinding lexicals between cases
* functions, amazingly, are block scoped *and* hoisted in ES2015
* numerous TDZ cases, surely, which I have not yet investigated
* var bindings can not shadow a lexical binding, ever
* more tests
@samwgoldman samwgoldman mentioned this pull request Sep 6, 2015
14 tasks
Also fix comment typos / reword
Wrote the comment first, thinking what I was going to write. Then I
wrote it differently, which turned out to be the correct way. Updating
comment to match.
I'm referring to the `statement_decl` phase as phase 1 and the `toplevels` phase
as phase 2 in this description.

Before this change, rebinding errors were ignored in `Env_js.bind__entry`, but
caught in `Env_js.init_value_entry`. This made it difficult to catch rebinding
errors in switch statements, because phase 2 does some sophisticated env and
changeset management, which makes it difficult to know when one case rebinds a
name from a previous case.

After this change, a simple, straightforward modification to `statement_decl`
exposes rebinding errors in switch statements.

There was a comment that indicated that the error messages were better if we
deferred rebinding error handling. However, after this change, I saw no change
in the error messages.

Also, now that binding errors are captured in phase 1, it became necessary to
add a suite of tests for inference behavior in phase 2, to ensure scopes are
managed properly in both phases. I added tests around two inference behaviors
that are affected by scoping: refinments and type widening.

The added test cases also forced some real errors in `Env_js.merge_env` and
`Env_js.copy_env`. I took this opportunity to tighten up the logic there.
For compatibility reasons, functions can shadow other vars/functions at
the top level (global scope or function scope) only.
Before, we only forbade var/lex shadowing when they were both found in
the same scope. However, a lexical may be in a nested scope.
Certain update expressions need to be forbidden, but are semantically
different from assignment.
@samwgoldman
Copy link
Member Author

All right. This is about as far as I can get right now. Feedback on what's here is appreciated. I may have missed something, but TDZ should be the only part outstanding here.

@bhosmer, did you have an approach in mind for handling that? I noticed you want to kill two birds with one stone here, and allow declaring non-maybe vars without an initializer.

@kastermester
Copy link

I have only briefly tried this out, but so far it seems to work as advertised! Great work! :) Will let you know if I run into any issues :)

which AST traversal takes place, these lookups may legitimately violate
rule #2, hence the need for a special mode.
*)
module LookupMode = struct
type t = ForValue | ForType | ForTypeof
type t = ForValue | ForUpdate | ForType | ForTypeof
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per IRC, if you take the modes path, I'd suggest

type t = ForValue of value_op | ForType | ForTypeof 
and value_op = Read | Write

The important thing is to keep the modes nonoverlapping, and pushing the read/write distinction down into ForValue will also spare you the occasional mode = ForValueRead || mode = ForValueWrite down below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I went down this path and things got pretty ugly. Happy to go over it in more depth, but I think the alternative approach of defining a set_var-like entrypoint for Update/PlusAssign. As far as a name for that entrypoint... any ideas? To be honest, I don't really see the problem with ForUpdate, so maybe this is a better change for you to make separately/subsequently?

@Macil
Copy link
Contributor

Macil commented Sep 8, 2015

Is TDZ compliance possible to statically check for in general? Babel only supports using runtime checks because its earlier attempt at static checking had problems.

For what it's worth, Flow already misses TDZ-like issues with vars:

/* @flow */
console.log(s.length);
var s = "abc";
/* @flow */
f();
var s = "abc";
function f() {
  console.log(s.length);
}

match get_entry name scope with
(* nested scope var shadows a lex-scoped lexical *)
| Some prev
when is_lex scope && Entry.is_lex prev ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_lex scope is unnecessary here...

The previous logic was pretty twisted--a side effect of my tacking on
some extra logic hastily.

The key thing here is that we need to actually check for existing
entries in a lex scope, even if we're trying to add a var-scoped
binding, because any shadowing in a lex scope is bad.

Also, it turns out that I didn't handle types shadowing lexicals, so
bully for that.
This resolves an issue which I misdiagnosed, causing me to add
unnecessary cases to Env_js.merge_env, which could mask other real
errors.

What's happening here: We enter a catch clause, which pushes a new lex
scope. Before we can pop the lex scope, we hit an abnormal and jump
away. Then, we merge the env that has a dangling lex scope on top.

In general, we run this risk whenever we push a lex scope and then
process statements that might raise. I have convinced myself that no
other code that calls merge_env will hit this specific case, but a
generalized approach would be nice, too.
We needed these before, when we had code that referenced the whole list,
but that code is gone now.
An earlier commit removed similar code from merge_env, which was caused
by a separate bug.
The added case isn't strictly required, but @bhosmer likes this more,
and that's enough for me.

Also, it's unnecessary to check if the Entry.is_lex, because lex scopes
imply that.
@Gozala Gozala mentioned this pull request Sep 11, 2015
64 tasks
@tikotzky
Copy link

👏 👏 👏 👏 👏
😄

@danawoodman
Copy link

Huzzah! Great work! 🎆 💥 👏 ✋ 👍 👌 🍻

@cesarandreu
Copy link
Contributor

🎉 🎉 🎉

@ronag ronag mentioned this pull request Sep 20, 2015
@iamdustan
Copy link
Contributor

Here’s a quick jscodeshift script you can use to transform all your vars to let or const and most likely not having anything break:

http://felix-kling.de/esprima_ast_explorer/#/tGblyJN52T/6

Flow and unit tests caught the two issues my current project codebase had post-transform. Both are documented in the transform.

I’ll update it again after #831 lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants