Skip to content

Revised emit for computed property names, including with decorators#19430

Merged
weswigham merged 13 commits into
microsoft:masterfrom
weswigham:computed-property-rework-mark2
Nov 6, 2017
Merged

Revised emit for computed property names, including with decorators#19430
weswigham merged 13 commits into
microsoft:masterfrom
weswigham:computed-property-rework-mark2

Conversation

@weswigham
Copy link
Copy Markdown
Member

This is a more correct alternative to #18350 based on discussions we had around it.

Computed property names on elided declarations (namely property declarations when transpiling to es6 or below), if they could have sideffects, are now moved into the next expression execution position available for them to execute in. This could be in a computed method name, or after the end of the class declaration/expression. As a sideeffect, the ts transform now must visit methods to place any pending expressions in their computed names, if needed. This is important for compliance with the public fields proposal, as computed names for public fields are supposed to work the same as computed names for methods.

Fixes #17028.

@weswigham weswigham force-pushed the computed-property-rework-mark2 branch from 9ed0edb to b3a1cf2 Compare October 23, 2017 21:29
@weswigham
Copy link
Copy Markdown
Member Author

@rbuckton This PR implements the emit as you suggested in-person. Is anything missing?

Comment thread src/compiler/emitter.ts Outdated
write("enum ");
emit(node.name);
pushNameGenerationScope();
pushNameGenerationScope(node);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose enums aren't really a place you can inject temporary variables either, so they probably shouldn't be a name-generation scope (even though that only really matters for javavascript output and enums are typescript only).

Comment thread src/compiler/types.ts Outdated
Iterator = 1 << 23, // The expression to a `yield*` should be treated as an Iterator when down-leveling, not an Iterable.
NoAsciiEscaping = 1 << 24, // When synthesizing nodes that lack an original node or textSourceNode, we want to write the text on the node with ASCII escaping substitutions.
/*@internal*/ TypeScriptClassWrapper = 1 << 25, // The node is an IIFE class wrapper created by the ts transform.
/*@internal*/ NoNameGenerationScope = 1 << 26, // Used to indicate when a function or other wrapper shouldn't be considered a name generation scope because its generated locals are merged with a higher scope
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We already have ReuseTempVariableScope which is supposed to mean the same thing. I like your approach where you added node to pushNameGenerationScope and popNameGenerationScope. Perhaps we can combine the two approaches and deduplicate the behavior in the emitter?

Comment thread src/compiler/emitter.ts Outdated
if (name.autoGenerateKind === GeneratedIdentifierKind.Node) {
// Node names generate unique names based on their original node
// and are cached based on that node's id.
let flags: TempFlags;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than read twice, the approach we've taken in other places (such as with context flags in the parser) is to do something like this:

if (name.skipNameGenerationScope) {
  const savedTempFlags = tempFlags;
  popNameGenerationScope(/*node*/ undefined);
  const result = generateNameCached(getNodeForGeneratedName(name));
  pushNameGenerationScope(/*node*/ undefined);
  tempFlags = savedTempFlags;
}
else {
  return generateNameCached(getNodeForGeneratedName(name));
}

Comment thread src/compiler/transformers/es2015.ts Outdated
// transformation.
if (getEmitFlags(node) & EmitFlags.Indented) {
setEmitFlags(classFunction, EmitFlags.Indented);
setEmitFlags(classFunction, EmitFlags.Indented | EmitFlags.NoNameGenerationScope);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps collapse/remove the if:

setEmitFlags(classFunction, (getEmitFlags(node) & EmitFlags.Indented) | EmitFlags.NoNameGenerationScope);


case SyntaxKind.PropertyDeclaration:
// TypeScript property declarations are elided.
// TypeScript property declarations are elided. However their names are still visited, and can potentially be retained if they could have sideeffects
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we do this in a visitPropertyDeclaration function instead?

}
return C;
}());
var _a;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We seem to now have an unnecessary variable here.

//// [parserComputedPropertyName31.js]
class C {
}
var _a, _b;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We seem to now have unnecessary variables here.

this[_a] = id++;
}
}
_a = e, _b = e2;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_b isn't even used and e2 itself has no side effects.

//// [parserComputedPropertyName36.js]
class C {
}
var _a;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We seem to now have unnecessary variables here.

this[_a] = 0;
}
[Symbol()]() { }
[_a = Symbol(), _b = Symbol(), Symbol()]() { }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_b is unused.

@weswigham
Copy link
Copy Markdown
Member Author

@rbuckton Got 'em all. Made the consolidations and style changes you requested, and updated the transform to only create an assignment when the assignment result is used, and only retain the expression if there is no assignment use if the expression looks like it could be side-effecting.

}()); }
if (y === void 0) { y = (_a = /** @class */ (function () {
function class_2() {
this[_b] = x;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I suppose this means that computed key names are evaluated only once, at declaration time, instead of at construction time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct, just like computed names for methods already are. We were inconsistent here.

}

function isSimpleInlineableExpression(expression: Expression) {
return !isIdentifier(expression) && isSimpleCopiableExpression(expression) ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would a GeneratedIdentifier be "inlineable"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Only if such a Generatedidentifier has all of its assignments occur before any intended usages of the identifier. Not something we check for in a uniform way, I think, but it may happen to be true for all generated identifiers at present?

*/
function transformInitializedProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) {
const propertyName = visitPropertyNameOfClassElement(property);
const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add comments to explain why we are generating the name here.

);
}

function isSimpleInlineableExpression(expression: Expression) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Document what it means to be a "simple inlineable expression"

Comment thread src/compiler/transformers/ts.ts Outdated

let statements: Statement[] = [classStatement];

// Write any pending expressions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add more context as to what these "pending expressions" are.

Comment thread src/compiler/transformers/ts.ts Outdated
// the body of a class with static initializers.
setEmitFlags(classExpression, EmitFlags.Indented | getEmitFlags(classExpression));
expressions.push(startOnNewLine(createAssignment(temp, classExpression)));
addRange(expressions, pendingExpressions);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As with visitClassDeclaration, add a comment to explain what the "pending expressions" are.

}
}

function getPropertyNameExpressionIfNeeded(name: PropertyName, shouldHoist: boolean, omitSimple: boolean): Expression {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add documentation to explain this function's purpose. Also omitSimple is not very clear, either give it a better name or document it.

expression = createAssignment(generatedName, expression);
let expr = getPropertyNameExpressionIfNeeded(name, some(member.decorators), /*omitSimple*/ false);
if (expr) { // expr only exists if `name` is a computed property name
if (some(pendingExpressions)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add comments to explain why we are inlining the existing pending expressions.

this[_b] = x;
}
return class_2;
}()), _b = x,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should _b = x, be on its own line?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm more befuddled as to why _a appears on the following line, actually (because they should all be inline). AFAIK the comma list doesn't specify multiline (nor should it), so I'm not sure where the break is coming from.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Then again statics appear on their own lines in the same context, apparently, so I suppose so?

}
var f2 = function _a() {
var _newTarget = this && this instanceof _a ? this.constructor : void 0;
var f2 = function _b() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What changed the names here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

_a was always used by the name for the static function expression on A - now there's no name generation scope for a class body, so it's the same scope as here, thereby bumping these names to the next character.

@weswigham
Copy link
Copy Markdown
Member Author

@rbuckton Done. As mentioned in person, I had to migrate startsOnNewLine to emitNode to handle it correctly for the pending expressions.

Comment thread src/compiler/types.ts Outdated
constantValue?: string | number; // The constant value of an expression
externalHelpersModuleName?: Identifier; // The local name for an imported helpers module
helpers?: EmitHelper[]; // Emit helpers for the node
startsOnNewLine?: boolean; // If the ndoe should begin on a new line
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

*node

@weswigham weswigham merged commit 4f48bf8 into microsoft:master Nov 6, 2017
@weswigham weswigham deleted the computed-property-rework-mark2 branch November 6, 2017 20:51
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
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.

3 participants