Skip to content
This repository was archived by the owner on May 19, 2018. It is now read-only.
/ babylon Public archive

Export default declaration as expression#630

Closed
jridgewell wants to merge 4 commits intobabel:masterfrom
jridgewell:export-default-function-expression
Closed

Export default declaration as expression#630
jridgewell wants to merge 4 commits intobabel:masterfrom
jridgewell:export-default-function-expression

Conversation

@jridgewell
Copy link
Copy Markdown
Member

@jridgewell jridgewell commented Jul 14, 2017

Q A
Bug fix? no
Breaking change? yes
New feature? no
Deprecations? no
Spec compliancy? Breaks spec
Tests added/pass? yes
Fixed tickets
License MIT

What's a FunctionDeclaration? It has an id. It's callable in scope. Same for ClassDeclarations.

// How do I call my default function? What's its id?
export default function() {}

These aren't declarations, they're expressions. Sometimes the spec is stupid.

@jridgewell jridgewell changed the title Export default function expression Export default declaration as expression Jul 14, 2017
@Jessidhia
Copy link
Copy Markdown
Member

Jessidhia commented Jul 14, 2017

These are not expressions, they are declarations. Parsing them as expressions will have incorrect behavior.

The specification specifically lists functions (HoistableDeclaration) and classes as being parsed as declarations when they are on the right side of a export default. Treating them as expressions and not declarations has implications for live bindings.

https://tc39.github.io/ecma262/#sec-exports

I believe it was done like that as a workaround for not being able to directly name a function or class as default. export class default {} would have the exact same runtime behavior as export default class {} if it was valid syntax.

@jridgewell
Copy link
Copy Markdown
Member Author

I know, I'm intentionally breaking from the spec here. Is there an observable difference in live bindings?

@Jessidhia
Copy link
Copy Markdown
Member

Jessidhia commented Jul 14, 2017

This will also break babel module transforms and make it harder to write them correctly, particularly in the hoisted declaration case.

I would prefer very much to not deviate from the spec unless it's not possible to represent correctly. The point of a function expression is not that its name is optional, it is that it is an expression, and it is not hoisted.

@Jessidhia
Copy link
Copy Markdown
Member

As for the live bindings, it is probably not observable in "normal code", but in the runtime behaviour it is important when there are import cycles.

// a.js (runs first)
import b from './b'

b() // declaration: fine; expression: TypeError
// b.js
import './a'

export default function () {}

@jridgewell
Copy link
Copy Markdown
Member Author

Prefect, that's the example I needed to see. I still think this is ridiculous.

@jridgewell
Copy link
Copy Markdown
Member Author

jridgewell commented Jul 14, 2017

Why are these cases different?

Case 1

(Included because I understand the argument for hoisting)

test();

const a = 1;

function test() {
  console.log(a); // TDZ Error
}

Case 2

// a.js (runs first)
import b from './b'

b() // Fine
// b.js
import './a'

const a = 1;

export default function () {
  console.log(a); // TDZ
}

Case 3

// a.js (runs first)
import b from './b'

b() // Not a function
// b.js
import './a'

const a = 1;
function test() {
  console.log(a); // Fine
}

export default test;

@Jessidhia
Copy link
Copy Markdown
Member

Jessidhia commented Jul 14, 2017

Case 1 and Case 2 are essentially identical.

However, an export default (arg) whose argument is not one of the two special cased declaration cases behaves as a let *default* = (arg); export { *default* as default } (with *default* as a "valid" identifier for explanation sake).

Thinking about it, this means Case 3 actually should be a TDZ error on the b() call site (as well as my example if the function was not a declaration).

@jridgewell
Copy link
Copy Markdown
Member Author

Yah, that's why I included case 1. Maybe this would be better explained as Classes?

Case 1

new Test(); // TDZ

class Test {
}

Case 2

// a.js (runs first)
import B from './b'

new B() // Fine
// b.js
import './a'

export default class Test {
}

Case 3

// a.js (runs first)
import B from './b'

new B() // TDZ? Not a class, at least
// b.js
import './a'

class Test {
}

export default Test;

Aside: Is referencing a live-binding before it's declared a TDZ error? Makes sense if it was.

@Jessidhia
Copy link
Copy Markdown
Member

Jessidhia commented Jul 14, 2017

It's harder to see the difference with class, as they are always lexical bindings. Case 2 also should be a TDZ error.

With class, the different behavior is seen if the class is reassigned, but that can only be done (assuming spec modules) if it was named to begin with.

export default class Foo {}

Foo = class Bar {}
// with 'export default ClassDeclaration': default value is updated
// with 'export default ClassExpression': ReferenceError

@jridgewell
Copy link
Copy Markdown
Member Author

Case 2 also should be a TDZ error.

What?! Why is it a declaration?! 🙄


With that example specifically, ClassDeclaration is just a variable declarator, and you can't export default a declarator.

// `class Foo {}` is really just `let Foo = class Foo {}`
// `export default let Foo = class Foo {}` is an error
// Instead, it's really transpiled as
let Foo = class Foo {};
export default Foo;

Foo = class Bar {}

I don't mean to be picking at your arguments. I think I'm cranky from my hate of circular dependencies. 😬

@Jessidhia
Copy link
Copy Markdown
Member

Jessidhia commented Jul 14, 2017

nope, export default class Foo {} is a ClassDeclaration that is valid; that may also be why there is a special case for export default ClassDeclaration. 😄

The runtime semantics even take into account the fact that the ClassDeclaration might be named: https://tc39.github.io/ecma262/#sec-exports-runtime-semantics-evaluation

If you want to get into how it's "transpiled", it would be equivalent to let Foo = class Foo {}; export { Foo as default }.

@loganfsmyth
Copy link
Copy Markdown
Member

Even ignoring circular dependencies and what imports what, even just accessing the class by name would be affected. e.g.

export default class Example {}
// or
class Example {}

console.log(Example); // Logs the class constructor

if it's a declaration, the Example binding is exposed in the top-level scope of the file, whereas with expressions their names are only scoped inside the class body, e.g.

(class Example {});

console.log(Example); // Throws on non-existant variable

@jridgewell
Copy link
Copy Markdown
Member Author

So I'm not arguing against export default class Foo {}, only export default class {}. There's nothing to reference in that case, which is why I think it should be a ClassExpression

@loganfsmyth
Copy link
Copy Markdown
Member

Gotcha, then yeah I don't think there is a way to observe the different between an anonymous default-export class declaration vs expression. I think it's good to have anonymous class declaration be consistent with function declaration though.

@bakkot
Copy link
Copy Markdown
Contributor

bakkot commented Jul 14, 2017

A difference not yet mentioned is that export default ClassDeclaration does not have a trailing semicolon. Which means that the following syntax is legal, if horrifying:

export default class {} export {};

whereas the following is not:

export default (class {}) export {};

It really is a declaration, albeit an unnamed one, and trying to pretend otherwise is going to lead to confusion at the very least.


I also don't understand why you want to make this change. For classes the difference in semantics is not observable (I think), but it trivially is for functions, as explained above.


You can also test this yourself in a recent browser or with a command line JS engine: e.g., with a recent build of v8, rund8 --module a.js on:

// a.js
import a from './a.js';
a(); // succeeds
export default function(){}

vs

// a.js
import a from './a.js';
a(); // fails
export default (function(){});

This may also help you when considering examples. For example, in your cases 1/2/3 for functions above, case 2 and 3 actually both work fine: b.js finishes evaluating before a.js gets past the import, so there's no TDZ issues.

@Jessidhia
Copy link
Copy Markdown
Member

Oh, right, I got the order of execution for circular imports wrong 😆

b finishes executing first before a executes because when b tries to import a a is already executing. If the export default was moved to a instead and referenced from b we'd get the behavior I was describing.

@ghost
Copy link
Copy Markdown

ghost commented Jul 14, 2017

Related: #502

@Jessidhia
Copy link
Copy Markdown
Member

Interestingly, the spec dealt differently with The names of each declaration type. Classes get an implicit name *default* which is renamed at runtime to default; functions are actually anonymous and get a name later.

@jridgewell
Copy link
Copy Markdown
Member Author

trying to pretend otherwise is going to lead to confusion at the very least.

How many transforms will assume declarations have an id? The parsing and runtime rules can be followed while still preserving what Babel considers to be a FunctionDeclaration and ClassDeclaration.

export default (function(){});

Why isn't a recursive parenthesis rule used like delete operators? https://www.ecma-international.org/ecma-262/6.0/#sec-delete-operator-static-semantics-early-errors

@loganfsmyth
Copy link
Copy Markdown
Member

How many transforms will assume declarations have an id?

At the end of the day, these things are not expressions, they are syntactically declarations. We should not diverge the AST from the spec because some plugins may make assumptions that are incorrect. There are an infinite number of things people could do wrong, we can't possibly hope to defend agains that.

@bakkot
Copy link
Copy Markdown
Contributor

bakkot commented Jul 15, 2017

The parsing and runtime rules can be followed while still preserving what Babel considers to be a FunctionDeclaration

No, they can't. As explained above, a function declaration and a function expressions in that position have observably different semantics. They cannot have the same AST.

Why isn't a recursive parenthesis rule used like delete operators?

Why would it be? Parentheses are already commonly used to distinguish function expressions from declarations. Not respecting them here would be weird.

jridgewell added a commit to jridgewell/babel that referenced this pull request Jul 15, 2017
@jridgewell
Copy link
Copy Markdown
Member Author

No, they can't. As explained above, a function declaration and a function expressions in that position have observably different semantics. They cannot have the same AST.

Our node representation has nothing to do with the code we transform into. All we needed to do is test if an (unparenthesized) FunctionExpression is the child of a ExportDefaultDeclaration. That was my point.

It seems this isn't going to happen. Instead, how about catching these unnamed declarations before any transformers have the chance to run and giving them an id? babel/babel@7.0...jridgewell:export-default-expression

@jridgewell jridgewell closed this Jul 15, 2017
@loganfsmyth
Copy link
Copy Markdown
Member

All we needed to do is test if an (unparenthesized) FunctionExpression is the child of a ExportDefaultDeclaration.

But there are syntactic differences in how the code is serialized. Once it is in the AST, we don't know if there were parens or not. And it would mean there's no way to programmatically create one or the other of the cases.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants