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

Parse JS Module Blocks proposal #12469

Merged
merged 24 commits into from Feb 21, 2021

Conversation

sosukesuzuki
Copy link
Member

@sosukesuzuki sosukesuzuki commented Dec 9, 2020

Q                       A
Fixed Issues? N/A
Patch: Bug Fix? N
Major: Breaking Change? N
Minor: New Feature? Y
Tests Added + Pass? Yes
License MIT

Add parsing support for JS Module Blocks proposal.

/cc @littledan @surma

@babel-bot
Copy link
Collaborator

babel-bot commented Dec 9, 2020

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/41538/

@JLHwung JLHwung self-requested a review December 9, 2020 20:33
@JLHwung
Copy link
Contributor

JLHwung commented Dec 9, 2020

Can you submit a PR to ESTree on the AST shapes? See also https://github.com/babel/babel/blob/main/CONTRIBUTING.md#creating-a-new-plugin-spec-new

@codesandbox-ci
Copy link

codesandbox-ci bot commented Dec 9, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e53f5fe:

Sandbox Source
babel-repl-custom-plugin Configuration
babel-plugin-multi-config Configuration

const oldExportedIdentifiers = this.state.exportedIdentifiers;
this.state.exportedIdentifiers = [];
this.eat(tt.braceL);
this.scope.enter(SCOPE_OTHER);
Copy link
Contributor

@JLHwung JLHwung Dec 9, 2020

Choose a reason for hiding this comment

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

We should also enter a new production parameter scope with [Await] parameter when topLevelAwait is enabled, e.g.

module { await 42 }

See

this.prodParam.enter(PARAM);
for an example.

Copy link
Member Author

Choose a reason for hiding this comment

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

189bc49 👍

@JLHwung JLHwung added PR: New Feature 🚀 A type of pull request used for our changelog categories pkg: generator pkg: parser Spec: Module Expressions labels Dec 9, 2020

// https://github.com/tc39/proposal-js-module-blocks
parseModuleExpression(): N.ModuleExpression {
this.expectPlugin("jsModuleBlocks");
Copy link
Contributor

Choose a reason for hiding this comment

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

I personally prefer moduleBlocks for brevity.

Copy link
Member Author

Choose a reason for hiding this comment

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

5dd532b 👍

defineType("ModuleExpression", {
visitor: ["body"],
fields: {
body: {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should wrap the module blocks within a BlockStatement. Like we did for DoExpression: https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md#doexpression

The BlockStatement is of Scopeable alias, and many more. So when traversing the module body statements in a module block, path.scope can be well defined, currently it points to the scope when the ModuleExpression defines since it is not Scopeable.

Copy link
Member

Choose a reason for hiding this comment

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

We could also consider Program instead of BlockStatement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Program allows to include Directive in its body. But Module Blocks doesn't seem to allow that. Also current BlockStatement body cannot have ModuleDeclaration.

Should we add new node type? or extends BlockStatement?

Copy link
Member

Choose a reason for hiding this comment

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

I would be really surprised if module blocks don't allow directives, since they are currently also allowed in blocks and functions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh sorry, I misunderstood about directives in ECMAScript spec. Module blocks allows directives. As you said, we can use Program.

Copy link
Member

Choose a reason for hiding this comment

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

Btw, the original reason I proposed Program is that in many transforms we need to inject things to the top-level.
We find the top-level with path.findParent(p => p.isProgram()), and this would nicely work out of the box with module blocks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed at e381be0

this.state.exportedIdentifiers = [];
this.eat(tt.braceL);
this.scope.enter(SCOPE_OTHER);
this.parseBlockOrModuleBlockBody(node.body, undefined, true, tt.braceR);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you a test case about the locality of this.inModule? We should throw for

// sourceType: "script"
module { await: 1 }

because "await" as a label identifier is forbidden when the goal syntax is Module: https://tc39.es/ecma262/#sec-identifiers-static-semantics-early-errors

Copy link
Member Author

Choose a reason for hiding this comment

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

b4adac3 👍

const oldInModule = this.inModule;
this.inModule = true;
const program = this.startNode<N.Program>();
node.body = this.parseProgram(program, tt.braceR, "module");
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also construct new classScope like we did in

this.classScope = new ClassScopeHandler(this.raise.bind(this));

otherwise it will mess up with upper class definition:

class B {
  #p() {
    module {
      class C { [this.#p]; } // it should not have access to `#p` defined across the module block
    }
  }
}

Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be better to introduce a new initializeScopes function, so htat if we have to initialize more things we only have to update one place.

Maybe something like

const oldScopes = this.initializeScopes();
node.body = this.parseProgram(program, tt.braceR, "module"));
Object.assign(this, oldScopes);

and then we re-use initializeScopes in Parser's constructor.

@sosukesuzuki sosukesuzuki force-pushed the parse-module-blocks branch 2 times, most recently from 8645fbc to fa763c2 Compare January 27, 2021 09:06
@nicolo-ribaudo
Copy link
Member

Update: Stage 2!

Comment on lines +60 to +61
/*::
import type { SourceType } from "../options";
*/
Copy link
Member

Choose a reason for hiding this comment

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

Nit: no need to comment this. It was only needed for class fields because Flow didn't support the declare keyword.

Suggested change
/*::
import type { SourceType } from "../options";
*/
import type { SourceType } from "../options";

Copy link
Member Author

Choose a reason for hiding this comment

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

If remove this comment, ESLint warns:

error: 'SourceType' is defined but never used (no-unused-vars) at packages/babel-parser/src/parser/expression.js:58:15:
56 | } from "../util/expression-scope";
57 | import { Errors } from "./error";
> 58 | import type { SourceType } from "../options";
   |               ^
59 | 
60 | export default class ExpressionParser extends LValParser {
61 |   // Forward-declaration: defined in statement.js

Sorry I'm not familiar with Flow, what should we do?

Copy link
Member

Choose a reason for hiding this comment

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

It's ok like this then.

if (this.isContextual("module")) {
const lookahead = this.lookahead();
if (
lookahead.type === tt.braceL &&
Copy link
Member

Choose a reason for hiding this comment

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

We can use this.lookaheadCharCode() === charCodes.rightCurlyBrace and avoid the expensive this.lokahead().

Copy link
Member Author

Choose a reason for hiding this comment

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

Also we can avoid lookahead via using new hasFollowingLineBreak, thank you. 370953a

const oldInModule = this.inModule;
this.inModule = true;
const program = this.startNode<N.Program>();
node.body = this.parseProgram(program, tt.braceR, "module");
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be better to introduce a new initializeScopes function, so htat if we have to initialize more things we only have to update one place.

Maybe something like

const oldScopes = this.initializeScopes();
node.body = this.parseProgram(program, tt.braceR, "module"));
Object.assign(this, oldScopes);

and then we re-use initializeScopes in Parser's constructor.

@@ -170,13 +170,16 @@ export default class ScopeHandler<IScope: Scope = Scope> {
}

checkLocalExport(id: N.Identifier) {
const currentScope = this.currentScope();
const scope =
currentScope.flags & SCOPE_PROGRAM ? currentScope : this.scopeStack[0];
Copy link
Member

Choose a reason for hiding this comment

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

Can currentScope.flags & SCOPE_PROGRAM ever be false?

Copy link
Member Author

Choose a reason for hiding this comment

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

I seem to have made a mistake when I added this... 15c8306

Comment on lines 1 to 3
module {
export { foo }
}
Copy link
Member

Choose a reason for hiding this comment

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

Another test:

let foo;
module {
  export { foo };
}

@sosukesuzuki
Copy link
Member Author

@nicolo-ribaudo

I wonder if it would be better to introduce a new initializeScopes function, so htat if we have to initialize more things we only have to update one place.

What do you think about the interface as below? f651952

const revertScopnes = this.initializeScopes();
node.body = this.parseProgram(program, tt.braceR, "module"));
revertScopes();

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 12, 2021

We should use try/finally around this.parseProgram (since sometimes we throw errors to "revert" part of the parsing process), but it looks good 👍

EDIT: I was suggesting to use initializeScopes also in

const ScopeHandler = this.getScopeHandler();
this.options = options;
this.inModule = this.options.sourceType === "module";
this.scope = new ScopeHandler(this.raise.bind(this), this.inModule);
this.prodParam = new ProductionParameterHandler();
this.classScope = new ClassScopeHandler(this.raise.bind(this));
this.expressionScope = new ExpressionScopeHandler(this.raise.bind(this));
, so that we don't have to maintain two places where we initialize the same things.

@surma
Copy link

surma commented Feb 13, 2021

Just wanted to thank y’all for being on top of my proposal! This is amazing to see. Thanks to this PR, I was able to prototype a babel transform to shim Module Blocks.

❤️

@nicolo-ribaudo
Copy link
Member

@sosukesuzuki Can we mark this PR as ready?

@sosukesuzuki sosukesuzuki marked this pull request as ready for review February 13, 2021 13:08
Copy link

@littledan littledan left a comment

Choose a reason for hiding this comment

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

Parsing algorithm and AST LGTM.

Nit: Consider calling the AST node "ModuleBlock" instead of "ModuleExpression"? (and ditto parseModuleBlock, etc)

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 13, 2021

Nit: Consider calling the AST node "ModuleBlock" instead of "ModuleExpression"?

Currently all the expression node names end with "Expression" (both in Babel and in the spec), so I'd prefer to keep it as ModuleExpression even if the proposal is called "module blocks" (I also think that the final production in the spec should/will be called ModuleExpression or something similar).

This was referenced Mar 14, 2021
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label May 28, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 28, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue pkg: generator pkg: parser pkg: types PR: New Feature 🚀 A type of pull request used for our changelog categories PR: Ready to be Merged A pull request with already two approvals, but waiting for the next minor release Spec: Module Expressions
Development

Successfully merging this pull request may close these issues.

None yet

7 participants