X Tutup
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
6 changes: 6 additions & 0 deletions language-extensions/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ declare type LuaMultiReturn<T extends any[]> = T & LuaExtension<"__luaMultiRetur
declare const $range: ((start: number, limit: number, step?: number) => Iterable<number>) &
LuaExtension<"__luaRangeFunctionBrand">;

/**
* Transpiles to the global vararg (`...`)
* For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions
*/
declare const $vararg: string[] & LuaExtension<"__luaVarargConstantBrand">;

/**
* Represents a Lua-style iterator which is returned from a LuaIterable.
* For simple iterators (with no state), this is just a function.
Expand Down
2 changes: 1 addition & 1 deletion src/lualib/ArrayReduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function __TS__ArrayReduce<T>(
this: void,
arr: T[],
callbackFn: (accumulator: T, currentValue: T, index: number, array: T[]) => T,
...initial: Vararg<T[]>
...initial: T[]
): T {
const len = arr.length;

Expand Down
2 changes: 1 addition & 1 deletion src/lualib/ArrayReduceRight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function __TS__ArrayReduceRight<T>(
this: void,
arr: T[],
callbackFn: (accumulator: T, currentValue: T, index: number, array: T[]) => T,
...initial: Vararg<T[]>
...initial: T[]
): T {
const len = arr.length;

Expand Down
2 changes: 1 addition & 1 deletion src/lualib/ArraySplice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.splice
function __TS__ArraySplice<T>(this: void, list: T[], ...args: Vararg<unknown[]>): T[] {
function __TS__ArraySplice<T>(this: void, list: T[], ...args: unknown[]): T[] {
const len = list.length;

const actualArgumentCount = select("#", ...args);
Expand Down
4 changes: 2 additions & 2 deletions src/lualib/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function __TS__GeneratorIterator(this: GeneratorIterator) {
return this;
}

function __TS__GeneratorNext(this: GeneratorIterator, ...args: Vararg<any[]>) {
function __TS__GeneratorNext(this: GeneratorIterator, ...args: any[]) {
const co = this.____coroutine;
if (coroutine.status(co) === "dead") return { done: true };

Expand All @@ -19,7 +19,7 @@ function __TS__GeneratorNext(this: GeneratorIterator, ...args: Vararg<any[]>) {
}

function __TS__Generator(this: void, fn: (this: void, ...args: any[]) => any) {
return function (this: void, ...args: Vararg<any[]>): GeneratorIterator {
return function (this: void, ...args: any[]): GeneratorIterator {
const argsLength = select("#", ...args);
return {
// Using explicit this there, since we don't pass arguments after the first nil and context is likely to be nil
Expand Down
2 changes: 1 addition & 1 deletion src/lualib/New.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function __TS__New(this: void, target: LuaClass, ...args: Vararg<any[]>): any {
function __TS__New(this: void, target: LuaClass, ...args: any[]): any {
const instance: any = setmetatable({}, target.prototype);
instance.____constructor(...args);
return instance;
Expand Down
3 changes: 0 additions & 3 deletions src/lualib/declarations/tstl.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/** @noSelfInFile */

/** @vararg */
type Vararg<T extends unknown[]> = T & { __luaVararg?: never };

interface Metatable {
_descriptors?: Record<string, PropertyDescriptor>;
__index?: any;
Expand Down
4 changes: 4 additions & 0 deletions src/transformation/utils/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const invalidForRangeCall = createErrorDiagnosticFactory(

export const invalidRangeUse = createErrorDiagnosticFactory("$range can only be used in a for...of loop.");

export const invalidVarargUse = createErrorDiagnosticFactory(
"$vararg can only be used in a spread element ('...$vararg') in global scope."
);

export const invalidRangeControlVariable = createErrorDiagnosticFactory(
"For loop using $range must declare a single control variable."
);
Expand Down
9 changes: 6 additions & 3 deletions src/transformation/utils/language-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum ExtensionKind {
MultiFunction = "MultiFunction",
MultiType = "MultiType",
RangeFunction = "RangeFunction",
VarargConstant = "VarargConstant",
IterableType = "IterableType",
AdditionOperatorType = "AdditionOperatorType",
AdditionOperatorMethodType = "AdditionOperatorMethodType",
Expand Down Expand Up @@ -49,15 +50,17 @@ export enum ExtensionKind {
TableSetMethodType = "TableSetMethodType",
}

const extensionKindToFunctionName: { [T in ExtensionKind]?: string } = {
const extensionKindToValueName: { [T in ExtensionKind]?: string } = {
[ExtensionKind.MultiFunction]: "$multi",
[ExtensionKind.RangeFunction]: "$range",
[ExtensionKind.VarargConstant]: "$vararg",
};

const extensionKindToTypeBrand: { [T in ExtensionKind]: string } = {
[ExtensionKind.MultiFunction]: "__luaMultiFunctionBrand",
[ExtensionKind.MultiType]: "__luaMultiReturnBrand",
[ExtensionKind.RangeFunction]: "__luaRangeFunctionBrand",
[ExtensionKind.VarargConstant]: "__luaVarargConstantBrand",
[ExtensionKind.IterableType]: "__luaIterableBrand",
[ExtensionKind.AdditionOperatorType]: "__luaAdditionBrand",
[ExtensionKind.AdditionOperatorMethodType]: "__luaAdditionMethodBrand",
Expand Down Expand Up @@ -107,13 +110,13 @@ export function isExtensionType(type: ts.Type, extensionKind: ExtensionKind): bo
return typeBrand !== undefined && type.getProperty(typeBrand) !== undefined;
}

export function isExtensionFunction(
export function isExtensionValue(
context: TransformationContext,
symbol: ts.Symbol,
extensionKind: ExtensionKind
): boolean {
return (
symbol.getName() === extensionKindToFunctionName[extensionKind] &&
symbol.getName() === extensionKindToValueName[extensionKind] &&
symbol.declarations.some(d => isExtensionType(context.checker.getTypeAtLocation(d), extensionKind))
);
}
2 changes: 2 additions & 0 deletions src/transformation/utils/lua-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export function getNumberLiteralValue(expression?: lua.Expression) {
return undefined;
}

// Prefer use of transformToImmediatelyInvokedFunctionExpression to maintain correct scope. If you use this directly,
// ensure you push/pop a function scope appropriately to avoid incorrect vararg optimization.
export function createImmediatelyInvokedFunctionExpression(
statements: lua.Statement[],
result: lua.Expression | lua.Expression[],
Expand Down
42 changes: 41 additions & 1 deletion src/transformation/utils/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as lua from "../../LuaAST";
import { assert, getOrUpdate, isNonNull } from "../../utils";
import { TransformationContext } from "../context";
import { getSymbolInfo } from "./symbols";
import { getFirstDeclarationInFile } from "./typescript";
import { findFirstNodeAbove, getFirstDeclarationInFile } from "./typescript";

export enum ScopeType {
File = 1 << 0,
Expand All @@ -24,6 +24,7 @@ interface FunctionDefinitionInfo {
export interface Scope {
type: ScopeType;
id: number;
node?: ts.Node;
referencedSymbols?: Map<lua.SymbolId, ts.Node[]>;
variableDeclarations?: lua.VariableDeclarationStatement[];
functionDefinitions?: Map<lua.SymbolId, FunctionDefinitionInfo>;
Expand Down Expand Up @@ -91,6 +92,45 @@ export function popScope(context: TransformationContext): Scope {
return scope;
}

function isDeclaredInScope(symbol: ts.Symbol, scopeNode: ts.Node) {
return symbol?.declarations.some(d => findFirstNodeAbove(d, (n): n is ts.Node => n === scopeNode));
}

// Checks for references to local functions which haven't been defined yet,
// and thus will be hoisted above the current position.
export function hasReferencedUndefinedLocalFunction(context: TransformationContext, scope: Scope) {
if (!scope.referencedSymbols || !scope.node) {
return false;
}
for (const [symbolId, nodes] of scope.referencedSymbols) {
const type = context.checker.getTypeAtLocation(nodes[0]);
if (
!scope.functionDefinitions?.has(symbolId) &&
type.getCallSignatures().length > 0 &&
isDeclaredInScope(type.symbol, scope.node)
) {
return true;
}
}
return false;
}

export function hasReferencedSymbol(context: TransformationContext, scope: Scope, symbol: ts.Symbol) {
if (!scope.referencedSymbols) {
return;
}
for (const nodes of scope.referencedSymbols.values()) {
if (nodes.some(node => context.checker.getSymbolAtLocation(node) === symbol)) {
return true;
}
}
return false;
}

export function isFunctionScopeWithDefinition(scope: Scope): scope is Scope & { node: ts.SignatureDeclaration } {
return scope.node !== undefined && ts.isFunctionLike(scope.node);
}

export function performHoisting(context: TransformationContext, statements: lua.Statement[]): lua.Statement[] {
const scope = peekScope(context);
let result = statements;
Expand Down
7 changes: 6 additions & 1 deletion src/transformation/utils/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { getOrUpdate } from "../../utils";
import { TransformationContext } from "../context";
import { isOptimizedVarArgSpread } from "../visitors/spread";
import { markSymbolAsReferencedInCurrentScopes } from "./scope";

const symbolIdCounters = new WeakMap<TransformationContext, number>();
Expand Down Expand Up @@ -44,7 +45,11 @@ export function trackSymbolReference(
symbolInfo.set(symbolId, { symbol, firstSeenAtPos: identifier.pos });
}

markSymbolAsReferencedInCurrentScopes(context, symbolId, identifier);
// If isOptimizedVarArgSpread returns true, the identifier will not appear in the resulting Lua.
// Only the optimized ellipses (...) will be used.
if (!isOptimizedVarArgSpread(context, symbol, identifier)) {
markSymbolAsReferencedInCurrentScopes(context, symbolId, identifier);
}

return symbolId;
}
Expand Down
22 changes: 22 additions & 0 deletions src/transformation/utils/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { castArray } from "../../utils";
import { TransformationContext } from "../context";
import { createImmediatelyInvokedFunctionExpression } from "./lua-ast";
import { ScopeType, pushScope, popScope } from "./scope";

export interface ImmediatelyInvokedFunctionParameters {
statements: lua.Statement | lua.Statement[];
result: lua.Expression | lua.Expression[];
}

export function transformToImmediatelyInvokedFunctionExpression(
context: TransformationContext,
transformFunction: () => ImmediatelyInvokedFunctionParameters,
tsOriginal?: ts.Node
): lua.CallExpression {
pushScope(context, ScopeType.Function);
const { statements, result } = transformFunction();
popScope(context);
return createImmediatelyInvokedFunctionExpression(castArray(statements), result, tsOriginal);
}
63 changes: 42 additions & 21 deletions src/transformation/visitors/binary-expression/assignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import { TransformationContext } from "../../context";
import { isTupleReturnCall } from "../../utils/annotations";
import { validateAssignment } from "../../utils/assignment-validation";
import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } from "../../utils/export";
import { createImmediatelyInvokedFunctionExpression, createUnpackCall, wrapInTable } from "../../utils/lua-ast";
import { createUnpackCall, wrapInTable } from "../../utils/lua-ast";
import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib";
import { isArrayType, isDestructuringAssignment } from "../../utils/typescript";
import { transformElementAccessArgument } from "../access";
import { transformLuaTablePropertyAccessInAssignment } from "../lua-table";
import { isArrayLength, transformDestructuringAssignment } from "./destructuring-assignments";
import { isMultiReturnCall } from "../language-extensions/multi";
import { popScope, pushScope, ScopeType } from "../../utils/scope";
import {
ImmediatelyInvokedFunctionParameters,
transformToImmediatelyInvokedFunctionExpression,
} from "../../utils/transform";

export function transformAssignmentLeftHandSideExpression(
context: TransformationContext,
Expand Down Expand Up @@ -68,6 +73,25 @@ export function transformAssignment(
];
}

function transformDestructuredAssignmentExpression(
context: TransformationContext,
expression: ts.DestructuringAssignment
): ImmediatelyInvokedFunctionParameters {
const rootIdentifier = lua.createAnonymousIdentifier(expression.left);

let right = context.transformExpression(expression.right);
if (isTupleReturnCall(context, expression.right) || isMultiReturnCall(context, expression.right)) {
right = wrapInTable(right);
}

const statements = [
lua.createVariableDeclarationStatement(rootIdentifier, right),
...transformDestructuringAssignment(context, expression, rootIdentifier),
];

return { statements, result: rootIdentifier };
}

export function transformAssignmentExpression(
context: TransformationContext,
expression: ts.AssignmentExpression<ts.EqualsToken>
Expand All @@ -89,19 +113,11 @@ export function transformAssignmentExpression(
}

if (isDestructuringAssignment(expression)) {
const rootIdentifier = lua.createAnonymousIdentifier(expression.left);

let right = context.transformExpression(expression.right);
if (isTupleReturnCall(context, expression.right) || isMultiReturnCall(context, expression.right)) {
right = wrapInTable(right);
}

const statements = [
lua.createVariableDeclarationStatement(rootIdentifier, right),
...transformDestructuringAssignment(context, expression, rootIdentifier),
];

return createImmediatelyInvokedFunctionExpression(statements, rootIdentifier, expression);
return transformToImmediatelyInvokedFunctionExpression(
context,
() => transformDestructuredAssignmentExpression(context, expression),
expression
);
}

if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) {
Expand All @@ -120,6 +136,7 @@ export function transformAssignmentExpression(
indexParameter,
valueParameter,
]);
pushScope(context, ScopeType.Function);
const objExpression = context.transformExpression(expression.left.expression);
let indexExpression: lua.Expression;
if (ts.isPropertyAccessExpression(expression.left)) {
Expand All @@ -134,15 +151,19 @@ export function transformAssignmentExpression(
}

const args = [objExpression, indexExpression, context.transformExpression(expression.right)];
popScope(context);
return lua.createCallExpression(iife, args, expression);
} else {
// Simple assignment
// (function() ${left} = ${right}; return ${left} end)()
const left = context.transformExpression(expression.left);
const right = context.transformExpression(expression.right);
return createImmediatelyInvokedFunctionExpression(
transformAssignment(context, expression.left, right),
left,
return transformToImmediatelyInvokedFunctionExpression(
context,
() => {
// Simple assignment
// (function() ${left} = ${right}; return ${left} end)()
const left = context.transformExpression(expression.left);
const right = context.transformExpression(expression.right);
const statements = transformAssignment(context, expression.left, right);
return { statements, result: left };
},
expression
);
}
Expand Down
Loading
X Tutup