Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export enum LuaLibFeature {
StringReplace = "StringReplace",
StringSplit = "StringSplit",
Symbol = "Symbol",
Ternary = "Ternary",
}

const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = {
Expand Down
24 changes: 24 additions & 0 deletions src/TSHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,28 @@ export class TSHelper {
public static isDefaultArrayPropertyName(methodName: string): boolean {
return defaultArrayPropertyNames.has(methodName);
}

public static isFalsible(type: ts.Type, strictNullChecks: boolean): boolean {
const falsibleFlags = ts.TypeFlags.Boolean
| ts.TypeFlags.BooleanLiteral
| ts.TypeFlags.Undefined
| ts.TypeFlags.Null
| ts.TypeFlags.Never
| ts.TypeFlags.Void
| ts.TypeFlags.Any;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will this catch null-able types?

declare function foo(): string | null;
const x = condition ? foo() : "bar";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Currently only literal types are allowed to be non-falsible as then we know the exact value of the expression. As we already excluded undefined and boolean literals before we now know that whenTrue must always be a valid non falsible expression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: now the above only applies when strictNullChecks are not enabled. Otherwise I recursively iterate through all union types and check if they are all non-falsible.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reading this I realize why I got confused. Will a null literal get caught by those flags?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, I wasn't aware that there is a difference between null and undefined. I added an additional check for it.


if (type.flags & falsibleFlags) {
return true;
} else if (!strictNullChecks && !type.isLiteral()) {
return true;
} else if (type.isUnion()) {
for (const subType of type.types) {
if (this.isFalsible(subType, strictNullChecks)) {
return true;
}
}
}

return false;
}
}
17 changes: 13 additions & 4 deletions src/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ export abstract class LuaTranspiler {
return this.transpileBinaryExpression(node as ts.BinaryExpression, brackets);
case ts.SyntaxKind.ConditionalExpression:
// Add brackets to preserve ordering
return this.transpileConditionalExpression(node as ts.ConditionalExpression, brackets);
return this.transpileConditionalExpression(node as ts.ConditionalExpression);
case ts.SyntaxKind.CallExpression:
return this.transpileCallExpression(node as ts.CallExpression);
case ts.SyntaxKind.PropertyAccessExpression:
Expand Down Expand Up @@ -1030,13 +1030,22 @@ export abstract class LuaTranspiler {
return parts.join("..");
}

public transpileConditionalExpression(node: ts.ConditionalExpression, brackets?: boolean): string {
public transpileProtectedConditionalExpression(node: ts.ConditionalExpression): string {
const condition = this.transpileExpression(node.condition);
const val1 = this.transpileExpression(node.whenTrue);
const val2 = this.transpileExpression(node.whenFalse);
return `((${condition}) and function() return ${val1}; end or function() return ${val2}; end)()`;
}

return this.transpileLuaLibFunction(LuaLibFeature.Ternary, condition,
`function() return ${val1} end`, `function() return ${val2} end`);
public transpileConditionalExpression(node: ts.ConditionalExpression): string {
const isStrict = this.options.strict || this.options.strictNullChecks;
if (tsHelper.isFalsible(this.checker.getTypeAtLocation(node.whenTrue), isStrict)) {
return this.transpileProtectedConditionalExpression(node);
}
const condition = this.transpileExpression(node.condition);
const val1 = this.transpileExpression(node.whenTrue);
const val2 = this.transpileExpression(node.whenFalse);
return `((${condition}) and (${val1}) or (${val2}))`;
}

public transpileBinaryAssignmentExpression(
Expand Down
7 changes: 0 additions & 7 deletions src/lualib/Ternary.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/targets/Transpiler.JIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ export class LuaTranspilerJIT extends LuaTranspiler52 {
public transpileSpreadElement(node: ts.SpreadElement): string {
return "unpack(" + this.transpileExpression(node.expression) + ")";
}

/** @override */
public transpileProtectedConditionalExpression(node: ts.ConditionalExpression): string {
const condition = this.transpileExpression(node.condition);
const val1 = this.transpileExpression(node.whenTrue);
const val2 = this.transpileExpression(node.whenFalse);
return `((${condition}) and {${val1}} or {${val2}})[1]`;
}
}
36 changes: 33 additions & 3 deletions test/unit/expressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,36 @@ export class ExpressionTests {
Expect(util.transpileString("undefined")).toBe("nil;");
}

@TestCase("true ? 'a' : 'b'", "a")
@TestCase("false ? 'a' : 'b'", "b")
@TestCase("true ? false : true", false)
@TestCase("false ? false : true", true)
@TestCase("true ? literalValue : true", "literal")
@TestCase("true ? variableValue : true", undefined)
@TestCase("true ? maybeUndefinedValue : true", undefined)
@TestCase("true ? maybeBooleanValue : true", false)
@TestCase("true ? maybeUndefinedValue : true", undefined, { strictNullChecks: true })
@TestCase("true ? maybeBooleanValue : true", false, { strictNullChecks: true })
@TestCase("true ? undefined : true", undefined, { strictNullChecks: true })
@TestCase("true ? null : true", undefined, { strictNullChecks: true })
@TestCase("true ? false : true", false, { luaTarget: LuaTarget.Lua51 })
@TestCase("false ? false : true", true, { luaTarget: LuaTarget.Lua51 })
@TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.Lua51 })
@TestCase("true ? false : true", false, { luaTarget: LuaTarget.LuaJIT })
@TestCase("false ? false : true", true, { luaTarget: LuaTarget.LuaJIT })
@TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.LuaJIT })
@Test("Ternary operator")
public ternaryOperator(input: string, expected: any, options?: ts.CompilerOptions): void {
const source = `const literalValue = 'literal';`
+ `let variableValue:string;`
+ `let maybeBooleanValue:string|boolean = false;`
+ `let maybeUndefinedValue:string|undefined;`
+ `return ${input};`;
const lua = util.transpileString(source, options);
const result = util.executeLua(lua);
Expect(result).toBe(expected);
}

@TestCase("inst.field", 8)
@TestCase("inst.field + 3", 8 + 3)
@TestCase("inst.field * 3", 8 * 3)
Expand Down Expand Up @@ -260,7 +290,7 @@ export class ExpressionTests {
@TestCase("inst.superBaseField", 4)
@Test("Inherited accessors")
public inheritedAccessors(expression: string, expected: any): void {
const source = `class MyBaseClass {`
const source = `class MyBaseClass {`
+ ` public _baseField: number;`
+ ` public get baseField(): number { return this._baseField + 6; }`
+ ` public set baseField(v: number) { this._baseField = v; }`
Expand All @@ -269,13 +299,13 @@ export class ExpressionTests {
+ ` public _field: number;`
+ ` public get field(): number { return this._field + 4; }`
+ ` public set field(v: number) { this._field = v; }`
+ `}`
+ `}`
+ `class MySuperClass extends MyClass {`
+ ` public _superField: number;`
+ ` public get superField(): number { return this._superField + 2; }`
+ ` public set superField(v: number) { this._superField = v; }`
+ ` public get superBaseField() { return this.baseField - 3; }`
+ `}`
+ `}`
+ `var inst = new MySuperClass();`
+ `inst.baseField = 1;`
+ `inst.field = 2;`
Expand Down