Skip to content

Commit a076c17

Browse files
authored
Port ?? binary operator emit (#1375)
1 parent 685faad commit a076c17

File tree

101 files changed

+466
-741
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+466
-741
lines changed

internal/ast/ast.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2074,7 +2074,9 @@ func (node *Token) computeSubtreeFacts() SubtreeFacts {
20742074
return SubtreeContainsClassFields
20752075
case KindAsteriskAsteriskToken, KindAsteriskAsteriskEqualsToken:
20762076
return SubtreeContainsExponentiationOperator
2077-
case KindQuestionQuestionToken, KindQuestionDotToken:
2077+
case KindQuestionQuestionToken:
2078+
return SubtreeContainsNullishCoalescing
2079+
case KindQuestionDotToken:
20782080
return SubtreeContainsOptionalChaining
20792081
case KindQuestionQuestionEqualsToken, KindBarBarEqualsToken, KindAmpersandAmpersandEqualsToken:
20802082
return SubtreeContainsLogicalAssignments

internal/printer/emitcontext.go

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,19 @@ type EmitContext struct {
2727
emitHelpers collections.OrderedSet[*EmitHelper]
2828
}
2929

30+
type environmentFlags int
31+
32+
const (
33+
environmentFlagsNone environmentFlags = 0
34+
environmentFlagsInParameters environmentFlags = 1 << 0 // currently visiting a parameter list
35+
environmentFlagsVariablesHoistedInParameters environmentFlags = 1 << 1 // a temp variable was hoisted while visiting a parameter list
36+
)
37+
3038
type varScope struct {
31-
variables []*ast.VariableDeclarationNode
32-
functions []*ast.FunctionDeclarationNode
39+
variables []*ast.VariableDeclarationNode
40+
functions []*ast.FunctionDeclarationNode
41+
flags environmentFlags
42+
initializationStatements []*ast.Node
3343
}
3444

3545
func NewEmitContext() *EmitContext {
@@ -105,12 +115,18 @@ func (c *EmitContext) StartVariableEnvironment() {
105115
func (c *EmitContext) EndVariableEnvironment() []*ast.Statement {
106116
scope := c.varScopeStack.Pop()
107117
var statements []*ast.Statement
118+
if len(scope.functions) > 0 {
119+
statements = slices.Clone(scope.functions)
120+
}
108121
if len(scope.variables) > 0 {
109122
varDeclList := c.Factory.NewVariableDeclarationList(ast.NodeFlagsNone, c.Factory.NewNodeList(scope.variables))
110123
varStatement := c.Factory.NewVariableStatement(nil /*modifiers*/, varDeclList)
111124
c.SetEmitFlags(varStatement, EFCustomPrologue)
112125
statements = append(statements, varStatement)
113126
}
127+
if len(scope.initializationStatements) > 0 {
128+
statements = append(statements, scope.initializationStatements...)
129+
}
114130
return append(statements, c.EndLexicalEnvironment()...)
115131
}
116132

@@ -148,6 +164,9 @@ func (c *EmitContext) AddVariableDeclaration(name *ast.IdentifierNode) {
148164
c.SetEmitFlags(varDecl, EFNoNestedSourceMaps)
149165
scope := c.varScopeStack.Peek()
150166
scope.variables = append(scope.variables, varDecl)
167+
if scope.flags&environmentFlagsInParameters != 0 {
168+
scope.flags |= environmentFlagsVariablesHoistedInParameters
169+
}
151170
}
152171

153172
// Adds a hoisted function declaration to the current VariableEnvironment
@@ -721,11 +740,135 @@ func (c *EmitContext) VisitVariableEnvironment(nodes *ast.StatementList, visitor
721740

722741
func (c *EmitContext) VisitParameters(nodes *ast.ParameterList, visitor *ast.NodeVisitor) *ast.ParameterList {
723742
c.StartVariableEnvironment()
743+
scope := c.varScopeStack.Peek()
744+
oldFlags := scope.flags
745+
scope.flags |= environmentFlagsInParameters
724746
nodes = visitor.VisitNodes(nodes)
747+
748+
// As of ES2015, any runtime execution of that occurs in for a parameter (such as evaluating an
749+
// initializer or a binding pattern), occurs in its own lexical scope. As a result, any expression
750+
// that we might transform that introduces a temporary variable would fail as the temporary variable
751+
// exists in a different lexical scope. To address this, we move any binding patterns and initializers
752+
// in a parameter list to the body if we detect a variable being hoisted while visiting a parameter list
753+
// when the emit target is greater than ES2015. (Which is now all targets.)
754+
if scope.flags&environmentFlagsVariablesHoistedInParameters != 0 {
755+
nodes = c.addDefaultValueAssignmentsIfNeeded(nodes)
756+
}
757+
scope.flags = oldFlags
725758
// !!! c.suspendVariableEnvironment()
726759
return nodes
727760
}
728761

762+
func (c *EmitContext) addDefaultValueAssignmentsIfNeeded(nodeList *ast.ParameterList) *ast.ParameterList {
763+
if nodeList == nil {
764+
return nodeList
765+
}
766+
var result []*ast.Node
767+
nodes := nodeList.Nodes
768+
for i, parameter := range nodes {
769+
updated := c.addDefaultValueAssignmentIfNeeded(parameter.AsParameterDeclaration())
770+
if updated != parameter {
771+
if result == nil {
772+
result = slices.Clone(nodes)
773+
}
774+
result[i] = updated
775+
}
776+
}
777+
if result != nil {
778+
res := c.Factory.NewNodeList(result)
779+
res.Loc = nodeList.Loc
780+
return res
781+
}
782+
return nodeList
783+
}
784+
785+
func (c *EmitContext) addDefaultValueAssignmentIfNeeded(parameter *ast.ParameterDeclaration) *ast.Node {
786+
// A rest parameter cannot have a binding pattern or an initializer,
787+
// so let's just ignore it.
788+
if parameter.DotDotDotToken != nil {
789+
return parameter.AsNode()
790+
} else if ast.IsBindingPattern(parameter.Name()) {
791+
return c.addDefaultValueAssignmentForBindingPattern(parameter)
792+
} else if parameter.Initializer != nil {
793+
return c.addDefaultValueAssignmentForInitializer(parameter, parameter.Name(), parameter.Initializer)
794+
}
795+
return parameter.AsNode()
796+
}
797+
798+
func (c *EmitContext) addDefaultValueAssignmentForBindingPattern(parameter *ast.ParameterDeclaration) *ast.Node {
799+
var initNode *ast.Node
800+
if parameter.Initializer != nil {
801+
initNode = c.Factory.NewConditionalExpression(
802+
c.Factory.NewStrictEqualityExpression(
803+
c.Factory.NewGeneratedNameForNode(parameter.AsNode()),
804+
c.Factory.NewVoidZeroExpression(),
805+
),
806+
c.Factory.NewToken(ast.KindQuestionToken),
807+
parameter.Initializer,
808+
c.Factory.NewToken(ast.KindColonToken),
809+
c.Factory.NewGeneratedNameForNode(parameter.AsNode()),
810+
)
811+
} else {
812+
initNode = c.Factory.NewGeneratedNameForNode(parameter.AsNode())
813+
}
814+
c.AddInitializationStatement(c.Factory.NewVariableStatement(
815+
nil,
816+
c.Factory.NewVariableDeclarationList(ast.NodeFlagsNone, c.Factory.NewNodeList([]*ast.Node{c.Factory.NewVariableDeclaration(
817+
parameter.Name(),
818+
nil,
819+
parameter.Type,
820+
initNode,
821+
)})),
822+
))
823+
return c.Factory.UpdateParameterDeclaration(
824+
parameter,
825+
parameter.Modifiers(),
826+
parameter.DotDotDotToken,
827+
c.Factory.NewGeneratedNameForNode(parameter.AsNode()),
828+
parameter.QuestionToken,
829+
parameter.Type,
830+
nil,
831+
)
832+
}
833+
834+
func (c *EmitContext) addDefaultValueAssignmentForInitializer(parameter *ast.ParameterDeclaration, name *ast.Node, initializer *ast.Node) *ast.Node {
835+
c.AddEmitFlags(initializer, EFNoSourceMap|EFNoComments)
836+
nameClone := name.Clone(c.Factory)
837+
c.AddEmitFlags(nameClone, EFNoSourceMap)
838+
initAssignment := c.Factory.NewAssignmentExpression(
839+
nameClone,
840+
initializer,
841+
)
842+
initAssignment.Loc = parameter.Loc
843+
c.AddEmitFlags(initAssignment, EFNoComments)
844+
initBlock := c.Factory.NewBlock(c.Factory.NewNodeList([]*ast.Node{c.Factory.NewExpressionStatement(initAssignment)}), false)
845+
initBlock.Loc = parameter.Loc
846+
c.AddEmitFlags(initBlock, EFSingleLine|EFNoTrailingSourceMap|EFNoTokenSourceMaps|EFNoComments)
847+
c.AddInitializationStatement(c.Factory.NewIfStatement(
848+
c.Factory.NewTypeCheck(name.Clone(c.Factory), "undefined"),
849+
initBlock,
850+
nil,
851+
))
852+
return c.Factory.UpdateParameterDeclaration(
853+
parameter,
854+
parameter.Modifiers(),
855+
parameter.DotDotDotToken,
856+
parameter.Name(),
857+
parameter.QuestionToken,
858+
parameter.Type,
859+
nil,
860+
)
861+
}
862+
863+
func (c *EmitContext) AddInitializationStatement(node *ast.Node) {
864+
scope := c.varScopeStack.Peek()
865+
if scope == nil {
866+
panic("Tried to add an initialization statement without a surrounding variable scope")
867+
}
868+
c.AddEmitFlags(node, EFCustomPrologue)
869+
scope.initializationStatements = append(scope.initializationStatements, node)
870+
}
871+
729872
func (c *EmitContext) VisitFunctionBody(node *ast.BlockOrExpression, visitor *ast.NodeVisitor) *ast.BlockOrExpression {
730873
// !!! c.resumeVariableEnvironment()
731874
updated := visitor.VisitNode(node)

internal/printer/factory.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ func (f *NodeFactory) NewLogicalORExpression(left *ast.Expression, right *ast.Ex
219219
// func (f *NodeFactory) NewBitwiseORExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
220220
// func (f *NodeFactory) NewBitwiseXORExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
221221
// func (f *NodeFactory) NewBitwiseANDExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
222-
// func (f *NodeFactory) NewStrictEqualityExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
222+
func (f *NodeFactory) NewStrictEqualityExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
223+
return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindEqualsEqualsEqualsToken), right)
224+
}
223225

224226
func (f *NodeFactory) NewStrictInequalityExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
225227
return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindExclamationEqualsEqualsToken), right)
@@ -272,6 +274,16 @@ func (f *NodeFactory) InlineExpressions(expressions []*ast.Expression) *ast.Expr
272274
// Utilities
273275
//
274276

277+
func (f *NodeFactory) NewTypeCheck(value *ast.Node, tag string) *ast.Node {
278+
if tag == "null" {
279+
return f.NewStrictEqualityExpression(value, f.NewKeywordExpression(ast.KindNullKeyword))
280+
} else if tag == "undefined" {
281+
return f.NewStrictEqualityExpression(value, f.NewVoidZeroExpression())
282+
} else {
283+
return f.NewStrictEqualityExpression(f.NewTypeOfExpression(value), f.NewStringLiteral(tag))
284+
}
285+
}
286+
275287
func (f *NodeFactory) NewMethodCall(object *ast.Node, methodName *ast.Node, argumentsList []*ast.Node) *ast.Node {
276288
// Preserve the optionality of `object`.
277289
if ast.IsCallExpression(object) && (object.Flags&ast.NodeFlagsOptionalChain != 0) {

internal/transformers/estransforms/nullishcoalescing.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,37 @@ type nullishCoalescingTransformer struct {
1111
}
1212

1313
func (ch *nullishCoalescingTransformer) visit(node *ast.Node) *ast.Node {
14-
return node // !!!
14+
if node.SubtreeFacts()&ast.SubtreeContainsNullishCoalescing == 0 {
15+
return node
16+
}
17+
switch node.Kind {
18+
case ast.KindBinaryExpression:
19+
return ch.visitBinaryExpression(node.AsBinaryExpression())
20+
default:
21+
return ch.Visitor().VisitEachChild(node)
22+
}
23+
}
24+
25+
func (ch *nullishCoalescingTransformer) visitBinaryExpression(node *ast.BinaryExpression) *ast.Node {
26+
switch node.OperatorToken.Kind {
27+
case ast.KindQuestionQuestionToken:
28+
left := ch.Visitor().VisitNode(node.Left)
29+
right := left
30+
if !transformers.IsSimpleCopiableExpression(left) {
31+
right = ch.Factory().NewTempVariable()
32+
ch.EmitContext().AddVariableDeclaration(right)
33+
left = ch.Factory().NewAssignmentExpression(right, left)
34+
}
35+
return ch.Factory().NewConditionalExpression(
36+
createNotNullCondition(ch.EmitContext(), left, right, false),
37+
ch.Factory().NewToken(ast.KindQuestionToken),
38+
right,
39+
ch.Factory().NewToken(ast.KindColonToken),
40+
ch.Visitor().VisitNode(node.Right),
41+
)
42+
default:
43+
return ch.Visitor().VisitEachChild(node.AsNode())
44+
}
1545
}
1646

1747
func newNullishCoalescingTransformer(emitContext *printer.EmitContext) *transformers.Transformer {

internal/transformers/estransforms/utilities.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,32 @@ func convertClassDeclarationToClassExpression(emitContext *printer.EmitContext,
1818
updated.Loc = node.Loc
1919
return updated
2020
}
21+
22+
func createNotNullCondition(emitContext *printer.EmitContext, left *ast.Node, right *ast.Node, invert bool) *ast.Node {
23+
token := ast.KindExclamationEqualsEqualsToken
24+
op := ast.KindAmpersandAmpersandToken
25+
if invert {
26+
token = ast.KindEqualsEqualsEqualsToken
27+
op = ast.KindBarBarToken
28+
}
29+
30+
return emitContext.Factory.NewBinaryExpression(
31+
nil,
32+
emitContext.Factory.NewBinaryExpression(
33+
nil,
34+
left,
35+
nil,
36+
emitContext.Factory.NewToken(token),
37+
emitContext.Factory.NewKeywordExpression(ast.KindNullKeyword),
38+
),
39+
nil,
40+
emitContext.Factory.NewToken(op),
41+
emitContext.Factory.NewBinaryExpression(
42+
nil,
43+
right,
44+
nil,
45+
emitContext.Factory.NewToken(token),
46+
emitContext.Factory.NewVoidZeroExpression(),
47+
),
48+
)
49+
}

testdata/baselines/reference/submodule/compiler/discriminatedUnionJsxElement.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ function ListItem(_data: IListItemData) {
2626

2727
//// [discriminatedUnionJsxElement.jsx]
2828
function Menu(data) {
29-
const listItemVariant = data.menuItemsVariant ?? ListItemVariant.OneLine;
29+
var _a;
30+
const listItemVariant = (_a = data.menuItemsVariant) !== null && _a !== void 0 ? _a : ListItemVariant.OneLine;
3031
return <ListItem variant={listItemVariant}/>;
3132
}
3233
var ListItemVariant;

testdata/baselines/reference/submodule/compiler/discriminatedUnionJsxElement.js.diff

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@
77
-"use strict";
88
-// Repro from #46021
99
function Menu(data) {
10-
- var _a;
11-
- const listItemVariant = (_a = data.menuItemsVariant) !== null && _a !== void 0 ? _a : ListItemVariant.OneLine;
12-
+ const listItemVariant = data.menuItemsVariant ?? ListItemVariant.OneLine;
13-
return <ListItem variant={listItemVariant}/>;
14-
}
15-
var ListItemVariant;
16-
@@= skipped -18, +15 lines =@@
10+
var _a;
11+
const listItemVariant = (_a = data.menuItemsVariant) !== null && _a !== void 0 ? _a : ListItemVariant.OneLine;
12+
@@= skipped -18, +16 lines =@@
1713

1814

1915
//// [discriminatedUnionJsxElement.d.ts]

testdata/baselines/reference/submodule/compiler/enumMemberReduction.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ var MyStringEnumWithEmpty;
7272
MyStringEnumWithEmpty["C"] = "c";
7373
})(MyStringEnumWithEmpty || (MyStringEnumWithEmpty = {}));
7474
function fn(optionalEnum) {
75-
return optionalEnum ?? MyEnum.A;
75+
return optionalEnum !== null && optionalEnum !== void 0 ? optionalEnum : MyEnum.A;
7676
}
7777
function fn2(optionalEnum) {
7878
return optionalEnum || MyEnum.B;
7979
}
8080
function fn3(optionalEnum) {
81-
return optionalEnum ?? MyEnum.A;
81+
return optionalEnum !== null && optionalEnum !== void 0 ? optionalEnum : MyEnum.A;
8282
}
8383
function fn4(optionalEnum) {
8484
return optionalEnum || MyEnum.B;

testdata/baselines/reference/submodule/compiler/enumMemberReduction.js.diff

Lines changed: 0 additions & 18 deletions
This file was deleted.

testdata/baselines/reference/submodule/compiler/getterSetterSubtypeAssignment.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ numberOrObject.x /= 1;
9090
//// [getterSetterSubtypeAssignment.js]
9191
class NumberOrUndefined {
9292
_x;
93-
get x() { return this._x ?? 0; }
93+
get x() { var _a; return (_a = this._x) !== null && _a !== void 0 ? _a : 0; }
9494
set x(value) { this._x = value; }
9595
additionAssignment() {
9696
this.x += 1;

0 commit comments

Comments
 (0)