Last active
December 7, 2023 04:54
-
-
Save Cheatoid/ea4573c6bd1992fc4940090543ec9380 to your computer and use it in GitHub Desktop.
TSTL plugins (unsafe_cast, goto/label, continue, next, vararg, methodsof, prototypeof, inline, typedparams) ✨
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Emits a Lua continue statement. | |
* @internal | |
* @emits `continue` | |
*/ | |
declare function __continue(this: void): void; | |
/** | |
* Emits a Lua goto statement (requires Lua 5.2+ or JIT). | |
* @param label - The label name to jump to. | |
* @internal | |
* @emits `goto label` | |
*/ | |
declare function __goto(this: void, label: string): void; | |
/** | |
* Inlines the body of the function in-place (useful in hot-path for high-performance code). | |
* @param body - The function to inline (must be within chunk's scope). | |
* @internal | |
* @emits `body` | |
* @remarks | |
* This is currently experimental. | |
*/ | |
declare function __inline(this: void, body: ((this: void) => void)): void; | |
declare function __inline<T>(this: void, body: ((this: void, arg: T) => void), arg: T): void; | |
declare function __inline<T1, T2>(this: void, body: ((this: void, arg1: T1, arg2: T2) => void), arg1: T1, arg2: T2): void; | |
declare function __inline<T1, T2, T3>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3) => void), arg1: T1, arg2: T2, arg3: T3): void; | |
declare function __inline<T1, T2, T3, T4>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4): void; | |
declare function __inline<T1, T2, T3, T4, T5>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14, arg15: T15) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14, arg15: T15): void; | |
declare function __inline<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this: void, body: ((this: void, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14, arg15: T15, arg16: T16) => void), arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, arg7: T7, arg8: T8, arg9: T9, arg10: T10, arg11: T11, arg12: T12, arg13: T13, arg14: T14, arg15: T15, arg16: T16): void; | |
/** | |
* Emits a Lua label statement (requires Lua 5.2+ or JIT). | |
* @param label - The label name to define. | |
* @internal | |
* @emits `::label::` | |
*/ | |
declare function __label(this: void, label: string): void; | |
/** | |
* Emits a class method iterator. Use in conjunction with for-of loop. | |
* @typeParam TClass - The class to iterate. | |
* @internal | |
* @emits `next, TClass.prototype` (skipping non-functions, metamethods and constructor) | |
*/ | |
declare function __methodsof<TClass extends AnyNotNil>(this: void): LuaIterable<LuaMultiReturn<[string, ((this: void, ...args: any[]) => any)]>>; | |
/** | |
* Emits a Lua next iterator. Use in conjunction with for-of loop. | |
* @param t - The table to iterate. | |
* @internal | |
* @emits `next, t, index` | |
*/ | |
declare function __next<TKey extends AnyNotNil, TValue>(this: void, t: LuaTable<TKey, TValue>, index?: any): LuaIterable<LuaMultiReturn<[TKey, NonNullable<TValue>]>>; | |
/** | |
* Emits a Lua next iterator. Use in conjunction with for-of loop. | |
* @param t - The object to iterate. | |
* @internal | |
* @emits `next, t, index` | |
*/ | |
declare function __next<T>(this: void, t: T, index?: any): LuaIterable<LuaMultiReturn<[keyof T, NonNullable<T[keyof T]>]>>; | |
/** | |
* Emits a class prototype iterator. Use in conjunction with for-of loop. | |
* @typeParam TClass - The class to iterate. | |
* @internal | |
* @emits `next, TClass.prototype` | |
*/ | |
declare function __prototypeof<TClass extends AnyNotNil>(this: void): LuaIterable<LuaMultiReturn<[string, AnyNotNil]>>; | |
/** | |
* Provides an ability to preserve (and validate) parameters' type information of the parent function. | |
* @param validator - The callback function which will receive parameters' type information. | |
* @remarks | |
* This is currently experimental. Must be called directly in the main scope of the function. | |
*/ | |
declare function __typedparams(this: void, validator: (this: void, ...info: [/*parameter name*/string, /*full type*/string, /*dotDotDotToken*/boolean, /*questionToken*/boolean, /*type*/string, /*initializer*/string?][]) => void): void; | |
/** | |
* Emits a Lua vararg operator `...`. | |
* @internal | |
* @emits `...` | |
*/ | |
declare function __vararg(this: void): LuaIterable<LuaMultiReturn<[...any[]]>>; | |
/** | |
* Unsafely casts a given value as a value of type T. | |
* @typeParam T - The type to cast to. | |
* @param value - The value to be cast. | |
* @returns Returns the given value. | |
* @internal | |
* @emits `value` | |
* @remarks | |
* This is useful in-place replacement for "as any" casting, because it allows to "find all references" quickly. | |
*/ | |
declare function unsafe_cast<T>(this: void, value: unknown): T; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as ts from "typescript"; | |
import * as tstl from "typescript-to-lua"; | |
import { createSerialDiagnosticFactory } from "typescript-to-lua/dist/utils"; | |
//#region Courtesy of TSTL codebase :-) | |
// https://github.com/TypeScriptToLua/TypeScriptToLua/blob/master/src/transformation/utils/diagnostics.ts | |
type MessageProvider<TArgs extends any[]> = string | ((...args: TArgs) => string); | |
const createDiagnosticFactory = <TArgs extends any[]>( | |
category: ts.DiagnosticCategory, | |
message: MessageProvider<TArgs> | |
) => | |
createSerialDiagnosticFactory((node: ts.Node, ...args: TArgs) => ({ | |
file: ts.getOriginalNode(node).getSourceFile(), | |
start: ts.getOriginalNode(node).getStart(), | |
length: ts.getOriginalNode(node).getWidth(), | |
messageText: typeof message === "string" ? message : message(...args), | |
category, | |
})); | |
const createErrorDiagnosticFactory = <TArgs extends any[]>(message: MessageProvider<TArgs>) => | |
createDiagnosticFactory(ts.DiagnosticCategory.Error, message); | |
const createWarningDiagnosticFactory = <TArgs extends any[]>(message: MessageProvider<TArgs>) => | |
createDiagnosticFactory(ts.DiagnosticCategory.Warning, message); | |
const getLuaTargetName = (version: tstl.LuaTarget) => (version === tstl.LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); | |
const unsupportedForTarget = createErrorDiagnosticFactory( | |
(functionality: string, version: tstl.LuaTarget) => | |
`${functionality} is/are not supported for target ${getLuaTargetName(version)}.` | |
); | |
//#endregion | |
const expectedStringLiteralInGoto = createErrorDiagnosticFactory( | |
"Expected a string literal in '__goto'." | |
); | |
const expectedFunctionExpressionInInline = createErrorDiagnosticFactory( | |
"Expected a function expression in '__inline'." | |
); | |
const expectedStringLiteralInLabel = createErrorDiagnosticFactory( | |
"Expected a string literal in '__label'." | |
); | |
const expectedAnArgumentInUnsafeCast = createErrorDiagnosticFactory( | |
"Expected a value in 'unsafe_cast'." | |
); | |
const expectedClassTypeNameInMethodsOf = createErrorDiagnosticFactory( | |
"Expected a class type name in '__methodsof'." | |
); | |
const expectedAnArgumentInNext = createErrorDiagnosticFactory( | |
"Expected an object in '__next'." | |
); | |
const expectedClassTypeNameInPrototypeOf = createErrorDiagnosticFactory( | |
"Expected a class type name in '__prototypeof'." | |
); | |
const typedParamsUsedOutsideOfFunction = createErrorDiagnosticFactory( | |
"'__typedparams' can not be used outside a function." | |
); | |
const plugin: tstl.Plugin = { | |
visitors: { | |
[ts.SyntaxKind.CallExpression](node, context) { | |
const result = context.superTransformExpression(node); | |
if (tstl.isCallExpression(result) && tstl.isIdentifier(result.expression)) { | |
switch (result.expression.text) { | |
case "unsafe_cast": { | |
if (result.params.length === 1) { | |
return result.params[0]; | |
} | |
context.diagnostics.push(expectedAnArgumentInUnsafeCast(node)); | |
break; | |
} | |
case "__vararg": { | |
return tstl.createDotsLiteral(node); | |
} | |
} | |
} | |
return result; | |
}, | |
[ts.SyntaxKind.ExpressionStatement](node, context) { | |
const result = context.superTransformStatements(node); | |
if (ts.isExpressionStatement(node)) { | |
const expr = node.expression; | |
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) { | |
switch (expr.expression.text) { | |
case "__continue": { | |
return tstl.createExpressionStatement(tstl.createIdentifier("continue", node), node); | |
} | |
case "__goto": { | |
if (context.luaTarget === tstl.LuaTarget.Lua50 || context.luaTarget === tstl.LuaTarget.Lua51) { | |
context.diagnostics.push(unsupportedForTarget(node, "goto", context.luaTarget)); | |
break; | |
} | |
if (expr.arguments.length === 1 && ts.isStringLiteral(expr.arguments[0])) { | |
return tstl.createGotoStatement(expr.arguments[0].text, node); | |
} | |
context.diagnostics.push(expectedStringLiteralInGoto(node)); | |
break; | |
} | |
case "__inline": { | |
if (expr.arguments.length > 0) { | |
let bodyArg: ts.Expression | ts.Declaration | undefined = expr.arguments[0]; | |
if (ts.isIdentifier(bodyArg)) { | |
try { | |
bodyArg = context.checker.getSymbolAtLocation(bodyArg)!.getDeclarations()![0]; | |
} catch (error) { | |
context.diagnostics.push(expectedFunctionExpressionInInline(node)); | |
break; | |
} | |
if (!bodyArg) { | |
context.diagnostics.push(expectedFunctionExpressionInInline(node)); | |
break; | |
} | |
} | |
const paramNames: string[] = []; | |
let funcExpr: tstl.FunctionExpression | undefined; | |
if (ts.isFunctionLike(bodyArg)) { | |
const bodyNode: tstl.Node | undefined = context.transformNode(bodyArg)[0]; | |
if (!bodyNode) { | |
context.diagnostics.push(expectedFunctionExpressionInInline(node)); | |
break; | |
} | |
if (tstl.isVariableDeclarationStatement(bodyNode) && bodyNode.right) { | |
if (tstl.isFunctionExpression(bodyNode.right[0])) { | |
funcExpr = bodyNode.right[0]; | |
} | |
} | |
paramNames.push(...bodyArg.parameters.map(p => p.name.getText())); | |
} | |
for (const stmt of result) { | |
if (tstl.isExpressionStatement(stmt)) { | |
const callExpr = stmt.expression; | |
if (tstl.isCallExpression(callExpr) && tstl.isIdentifier(callExpr.expression) && | |
callExpr.expression.text === expr.expression.text) { | |
const paramCount = callExpr.params.length; | |
if (paramCount > 0) { | |
let body: tstl.Expression | undefined = callExpr.params[0]; | |
if (tstl.isIdentifier(body)) { | |
body = funcExpr; | |
} | |
if (body && tstl.isFunctionExpression(body)) { | |
const statements: tstl.Statement[] = body.body.statements; | |
for (let index = 1; index < paramCount; ++index) { // Skip the body parameter. | |
const param = callExpr.params[index]; | |
statements.unshift( | |
tstl.createVariableDeclarationStatement([tstl.createIdentifier(paramNames[index - 1])], [param]) | |
); | |
} | |
return tstl.createDoStatement(statements, node); | |
} | |
} | |
} | |
} | |
} | |
} | |
context.diagnostics.push(expectedFunctionExpressionInInline(node)); | |
break; | |
} | |
case "__label": { | |
if (context.luaTarget === tstl.LuaTarget.Lua50 || context.luaTarget === tstl.LuaTarget.Lua51) { | |
context.diagnostics.push(unsupportedForTarget(node, "label", context.luaTarget)); | |
break; | |
} | |
if (expr.arguments.length === 1 && ts.isStringLiteral(expr.arguments[0])) { | |
return tstl.createLabelStatement(expr.arguments[0].text, node); | |
} | |
context.diagnostics.push(expectedStringLiteralInLabel(node)); | |
break; | |
} | |
case "__typedparams": { | |
// TODO: Consider supporting any depth within function | |
if (ts.isBlock(node.parent) && ts.isFunctionLike(node.parent.parent) && expr.arguments.length === 1) { | |
const typedParams: tstl.TableExpression[] = []; | |
for (const param of node.parent.parent.parameters) { | |
// /*parameter name*/string, /*full type*/string, /*dotDotDotToken*/boolean, /*questionToken*/boolean, /*type*/string, /*initializer*/string? | |
const paramEntry: tstl.Expression[] = [ | |
tstl.createStringLiteral(param.name.getText()), | |
tstl.createStringLiteral(`${param.dotDotDotToken?.getText() ?? ""}${param.type?.getText() ?? "any"}${param.questionToken ? "?" : ""}`), | |
tstl.createBooleanLiteral(param.dotDotDotToken ? true : false), | |
tstl.createBooleanLiteral(param.questionToken ? true : false), | |
tstl.createStringLiteral(param.type?.getText() ?? "any") | |
]; | |
if (param.initializer) { | |
paramEntry.push( | |
tstl.createStringLiteral(param.initializer.getText()) | |
); | |
} | |
typedParams.push( | |
tstl.createTableExpression([...paramEntry.map((e) => tstl.createTableFieldExpression(e))]) | |
); | |
} | |
for (const stmt of result) { | |
if (tstl.isExpressionStatement(stmt)) { | |
const callExpr = stmt.expression; | |
if (tstl.isCallExpression(callExpr) && tstl.isIdentifier(callExpr.expression) && | |
callExpr.expression.text === expr.expression.text && callExpr.params.length === 1) { | |
return tstl.createExpressionStatement(tstl.createCallExpression(callExpr.params[0], typedParams), node); | |
} | |
} | |
} | |
} | |
context.diagnostics.push(typedParamsUsedOutsideOfFunction(node)); | |
break; | |
} | |
} | |
} | |
} | |
return result; | |
}, | |
[ts.SyntaxKind.ForOfStatement](node, context) { | |
const result = context.superTransformStatements(node); | |
const expr = node.expression; | |
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && ts.isBlock(node.statement)) { | |
switch (expr.expression.text) { | |
case "__methodsof": { | |
if (expr.typeArguments && expr.typeArguments.length === 1) { | |
const typeArg = expr.typeArguments[0]; | |
if (ts.isTypeReferenceNode(typeArg)) { | |
const typeInfo = context.checker.getTypeAtLocation(typeArg); // Thanks Perry 😎 | |
if (typeInfo.isClass()) { | |
const escapedName = typeInfo.symbol.escapedName.toString(); | |
for (const stmt of result) { | |
if (tstl.isForInStatement(stmt) && stmt.names.length === 2) { | |
stmt.expressions.splice(0); | |
stmt.expressions.push(tstl.createIdentifier("next"), tstl.createIdentifier(`${escapedName}.prototype`)); | |
stmt.body.statements.push(tstl.createIfStatement( | |
tstl.createBinaryExpression( | |
// Skip non functions | |
tstl.createBinaryExpression( | |
tstl.createCallExpression(tstl.createIdentifier("type"), [stmt.names[1]]), | |
tstl.createStringLiteral("function"), | |
tstl.SyntaxKind.EqualityOperator | |
), | |
// Skip metamethods (and constructor) | |
tstl.createBinaryExpression( | |
tstl.createCallExpression(tstl.createIdentifier("string.sub"), [stmt.names[0], tstl.createNumericLiteral(1), tstl.createNumericLiteral(2)]), | |
tstl.createStringLiteral("__"), | |
tstl.SyntaxKind.InequalityOperator | |
), | |
tstl.SyntaxKind.AndOperator | |
), | |
tstl.createBlock(stmt.body.statements.splice(0)) | |
)); | |
} | |
} | |
break; | |
} | |
} | |
} | |
context.diagnostics.push(expectedClassTypeNameInMethodsOf(node)); | |
break; | |
} | |
case "__next": { | |
for (const stmt of result) { | |
if (tstl.isForInStatement(stmt)) { | |
const spliced = stmt.expressions.splice(0); | |
if (spliced.length === 1) { | |
const callExpr = spliced[0]; | |
if (tstl.isCallExpression(callExpr)) { | |
stmt.expressions.push(tstl.createIdentifier("next"), ...callExpr.params); | |
continue; | |
} | |
} | |
context.diagnostics.push(expectedAnArgumentInNext(node)); | |
break; | |
} | |
} | |
break; | |
} | |
case "__prototypeof": { // Stripped down version of __methodsof | |
if (expr.typeArguments && expr.typeArguments.length === 1) { | |
const typeArg = expr.typeArguments[0]; | |
if (ts.isTypeReferenceNode(typeArg)) { | |
const typeInfo = context.checker.getTypeAtLocation(typeArg); // Thanks Perry 😎 | |
if (typeInfo.isClass()) { | |
const escapedName = typeInfo.symbol.escapedName.toString(); | |
for (const stmt of result) { | |
if (tstl.isForInStatement(stmt)) { | |
stmt.expressions.splice(0); | |
stmt.expressions.push(tstl.createIdentifier("next"), tstl.createIdentifier(`${escapedName}.prototype`)); | |
} | |
} | |
break; | |
} | |
} | |
} | |
context.diagnostics.push(expectedClassTypeNameInPrototypeOf(node)); | |
break; | |
} | |
} | |
} | |
return result; | |
}, | |
[ts.SyntaxKind.ContinueStatement](node, context) { | |
// FIXME: Custom tsconfig option check doesn't work (despite being specified in the tsconfig file) | |
//const luaContinueSupport = (<ts.CompilerOptions & { luaContinueSupport?: boolean; }>context.program.getCompilerOptions()).luaContinueSupport; // context.options.luaContinueSupport | |
const luaContinueSupport = false; // Change this to true at your own peril (if your target Lua environment supports continue statement as-is). | |
if (luaContinueSupport) { | |
return tstl.createExpressionStatement(tstl.createIdentifier("continue", node), node); | |
} | |
const result = context.superTransformStatements(node); | |
return result; | |
} | |
} | |
}; | |
export default plugin; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is an example TS code to show usages of the above TSTL plugin:
The following is an output Lua code generated after transpilation: