Skip to content

Commit 89f3413

Browse files
authored
Feature/nullish coalescing (#866)
* Early null-coalescing * Fixed type predicate, added tests for side effects
1 parent 244633b commit 89f3413

File tree

4 files changed

+127
-23
lines changed

4 files changed

+127
-23
lines changed

src/transformation/utils/diagnostics.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ export const unsupportedAccessorInObjectLiteral = createDiagnosticFactory(
9797
"Accessors in object literal are not supported."
9898
);
9999

100-
export const unsupportedNullishCoalescing = createDiagnosticFactory("Nullish coalescing is not supported.");
101-
102100
export const unsupportedRightShiftOperator = createDiagnosticFactory(
103101
"Right shift operator is not supported for target Lua 5.3. Use `>>>` instead."
104102
);

src/transformation/utils/typescript/types.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,57 @@ import * as ts from "typescript";
22
import { TransformationContext } from "../../context";
33

44
export function isTypeWithFlags(context: TransformationContext, type: ts.Type, flags: ts.TypeFlags): boolean {
5-
if (type.symbol) {
6-
const baseConstraint = context.checker.getBaseConstraintOfType(type);
7-
if (baseConstraint && baseConstraint !== type) {
8-
return isTypeWithFlags(context, baseConstraint, flags);
5+
const predicate = (type: ts.Type) => {
6+
if (type.symbol) {
7+
const baseConstraint = context.checker.getBaseConstraintOfType(type);
8+
if (baseConstraint && baseConstraint !== type) {
9+
return isTypeWithFlags(context, baseConstraint, flags);
10+
}
911
}
12+
return (type.flags & flags) !== 0;
13+
};
14+
15+
return typeAlwaysSatisfies(context, type, predicate);
16+
}
17+
18+
export function typeAlwaysSatisfies(
19+
context: TransformationContext,
20+
type: ts.Type,
21+
predicate: (type: ts.Type) => boolean
22+
): boolean {
23+
if (predicate(type)) {
24+
return true;
25+
}
26+
27+
if (type.isUnion()) {
28+
return type.types.every(t => typeAlwaysSatisfies(context, t, predicate));
29+
}
30+
31+
if (type.isIntersection()) {
32+
return type.types.some(t => typeAlwaysSatisfies(context, t, predicate));
33+
}
34+
35+
return false;
36+
}
37+
38+
export function typeCanSatisfy(
39+
context: TransformationContext,
40+
type: ts.Type,
41+
predicate: (type: ts.Type) => boolean
42+
): boolean {
43+
if (predicate(type)) {
44+
return true;
1045
}
1146

1247
if (type.isUnion()) {
13-
return type.types.every(t => isTypeWithFlags(context, t, flags));
48+
return type.types.some(t => typeCanSatisfy(context, t, predicate));
1449
}
1550

1651
if (type.isIntersection()) {
17-
return type.types.some(t => isTypeWithFlags(context, t, flags));
52+
return type.types.some(t => typeCanSatisfy(context, t, predicate));
1853
}
1954

20-
return (type.flags & flags) !== 0;
55+
return false;
2156
}
2257

2358
export function isStringType(context: TransformationContext, type: ts.Type): boolean {
@@ -52,7 +87,7 @@ function isExplicitArrayType(context: TransformationContext, type: ts.Type): boo
5287
/**
5388
* Iterate over a type and its bases until the callback returns true.
5489
*/
55-
function forTypeOrAnySupertype(
90+
export function forTypeOrAnySupertype(
5691
context: TransformationContext,
5792
type: ts.Type,
5893
predicate: (type: ts.Type) => boolean

src/transformation/visitors/binary-expression/index.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@ import * as ts from "typescript";
22
import * as lua from "../../../LuaAST";
33
import { FunctionVisitor, TransformationContext } from "../../context";
44
import { AnnotationKind, getTypeAnnotations } from "../../utils/annotations";
5-
import {
6-
extensionInvalidInstanceOf,
7-
luaTableInvalidInstanceOf,
8-
unsupportedNullishCoalescing,
9-
} from "../../utils/diagnostics";
5+
import { extensionInvalidInstanceOf, luaTableInvalidInstanceOf } from "../../utils/diagnostics";
106
import { createImmediatelyInvokedFunctionExpression, wrapInToStringForConcat } from "../../utils/lua-ast";
117
import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib";
12-
import { isStandardLibraryType, isStringType } from "../../utils/typescript";
8+
import { isStandardLibraryType, isStringType, typeCanSatisfy } from "../../utils/typescript";
139
import { transformTypeOfBinaryExpression } from "../typeof";
1410
import { transformAssignmentExpression, transformAssignmentStatement } from "./assignments";
1511
import { BitOperator, isBitOperator, transformBinaryBitOperation } from "./bit";
@@ -137,13 +133,7 @@ export const transformBinaryExpression: FunctionVisitor<ts.BinaryExpression> = (
137133
}
138134

139135
case ts.SyntaxKind.QuestionQuestionToken: {
140-
context.diagnostics.push(unsupportedNullishCoalescing(node.operatorToken));
141-
return lua.createBinaryExpression(
142-
context.transformExpression(node.left),
143-
context.transformExpression(node.right),
144-
lua.SyntaxKind.OrOperator,
145-
node
146-
);
136+
return transformNullishCoalescingExpression(context, node);
147137
}
148138

149139
default:
@@ -185,3 +175,42 @@ export function transformBinaryExpressionStatement(
185175
return lua.createDoStatement(statements, expression);
186176
}
187177
}
178+
179+
function transformNullishCoalescingExpression(
180+
context: TransformationContext,
181+
node: ts.BinaryExpression
182+
): lua.Expression {
183+
const lhsType = context.checker.getTypeAtLocation(node.left);
184+
185+
// Check if we can take a shortcut to 'lhs or rhs' if the left-hand side cannot be 'false'.
186+
const typeCanBeFalse = (type: ts.Type) =>
187+
(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Boolean)) !== 0 ||
188+
(type.flags & ts.TypeFlags.BooleanLiteral & ts.TypeFlags.PossiblyFalsy) !== 0;
189+
if (typeCanSatisfy(context, lhsType, typeCanBeFalse)) {
190+
// lhs can be false, transform to IIFE
191+
const lhsIdentifier = lua.createIdentifier("____lhs");
192+
const nilComparison = lua.createBinaryExpression(
193+
lua.cloneIdentifier(lhsIdentifier),
194+
lua.createNilLiteral(),
195+
lua.SyntaxKind.EqualityOperator
196+
);
197+
// if ____ == nil then return rhs else return ____ end
198+
const ifStatement = lua.createIfStatement(
199+
nilComparison,
200+
lua.createBlock([lua.createReturnStatement([context.transformExpression(node.right)])]),
201+
lua.createBlock([lua.createReturnStatement([lua.cloneIdentifier(lhsIdentifier)])])
202+
);
203+
// (function(lhs') if lhs' == nil then return rhs else return lhs' end)(lhs)
204+
return lua.createCallExpression(lua.createFunctionExpression(lua.createBlock([ifStatement]), [lhsIdentifier]), [
205+
context.transformExpression(node.left),
206+
]);
207+
} else {
208+
// lhs or rhs
209+
return lua.createBinaryExpression(
210+
context.transformExpression(node.left),
211+
context.transformExpression(node.right),
212+
lua.SyntaxKind.OrOperator,
213+
node
214+
);
215+
}
216+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as util from "../util";
2+
3+
test.each(["null", "undefined"])("nullish-coalesing operator returns rhs", nullishValue => {
4+
util.testExpression`${nullishValue} ?? "Hello, World!"`.expectToMatchJsResult();
5+
});
6+
7+
test.each([3, "foo", {}, [], true, false])("nullish-coalesing operator returns lhs", value => {
8+
util.testExpression`${util.formatCode(value)} ?? "Hello, World!"`.expectToMatchJsResult();
9+
});
10+
11+
test.each(["any", "unknown"])("nullish-coalesing operator with any/unknown type", type => {
12+
util.testFunction`
13+
const unknownType = false as ${type};
14+
return unknownType ?? "This should not be returned!";
15+
`.expectToMatchJsResult();
16+
});
17+
18+
test.each(["boolean | string", "number | false", "undefined | true"])(
19+
"nullish-coalesing operator with union type",
20+
unionType => {
21+
util.testFunction`
22+
const unknownType = false as ${unionType};
23+
return unknownType ?? "This should not be returned!";
24+
`.expectToMatchJsResult();
25+
}
26+
);
27+
28+
test("nullish-coalescing operator with side effect lhs", () => {
29+
util.testFunction`
30+
let i = 0;
31+
const incI = () => ++i;
32+
return [i, incI() ?? 3, i];
33+
`.expectToMatchJsResult();
34+
});
35+
36+
test("nullish-coalescing operator with side effect rhs", () => {
37+
util.testFunction`
38+
let i = 0;
39+
const incI = () => ++i;
40+
return [i, undefined ?? incI(), i];
41+
`.expectToMatchJsResult();
42+
});

0 commit comments

Comments
 (0)