Skip to content

Commit 38b0715

Browse files
committed
inline - first version
1 parent ef946a3 commit 38b0715

12 files changed

Lines changed: 825 additions & 15 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"main": "dist/index.js",
2424
"types": "dist/index.d.ts",
2525
"scripts": {
26+
"raw-build": "tsc",
2627
"build": "tsc && npm run build-lualib",
2728
"build-lualib": "node dist/tstl.js -p src/lualib/tsconfig.json && node dist/tstl.js -p src/lualib/tsconfig.lua50.json",
2829
"pretest": "npm run lint && npm run check:language-extensions && npm run build-lualib",

src/transformation/context/context.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ import { SymbolInfo } from "../utils/symbols";
1010
import { LuaLibFeature } from "../../LuaLib";
1111
import { Scope, ScopeType } from "../utils/scope";
1212
import { ClassSuperInfo } from "../visitors/class";
13+
import { NodeArray, ParameterDeclaration } from "typescript";
14+
15+
export interface InlineFunctionInfo {
16+
node: ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction;
17+
parameters: NodeArray<ParameterDeclaration>;
18+
body: ts.ConciseBody;
19+
sourceFile: ts.SourceFile;
20+
isProcessing: boolean;
21+
}
22+
23+
// Shared registry of inline functions across all files in a program
24+
const programInlineFunctions = new WeakMap<ts.Program, Map<ts.Symbol, InlineFunctionInfo>>();
25+
26+
export function getOrCreateProgramInlineFunctions(program: ts.Program): Map<ts.Symbol, InlineFunctionInfo> {
27+
let inlineFunctions = programInlineFunctions.get(program);
28+
if (!inlineFunctions) {
29+
inlineFunctions = new Map();
30+
programInlineFunctions.set(program, inlineFunctions);
31+
}
32+
return inlineFunctions;
33+
}
1334

1435
export const tempSymbolId = -1 as lua.SymbolId;
1536

@@ -43,12 +64,18 @@ export class TransformationContext {
4364
(this.options.alwaysStrict ?? this.options.strict) ||
4465
(this.isModule && this.options.target !== undefined && this.options.target >= ts.ScriptTarget.ES2015);
4566

67+
// Reference to the shared inline functions registry for this program
68+
public readonly inlineFunctions: Map<ts.Symbol, InlineFunctionInfo>;
69+
4670
constructor(public program: ts.Program, public sourceFile: ts.SourceFile, private visitorMap: VisitorMap) {
4771
// Use `getParseTreeNode` to get original SourceFile node, before it was substituted by custom transformers.
4872
// It's required because otherwise `getEmitResolver` won't use cached diagnostics, produced in `emitWorker`
4973
// and would try to re-analyze the file, which would fail because of replaced nodes.
5074
const originalSourceFile = ts.getParseTreeNode(sourceFile, ts.isSourceFile) ?? sourceFile;
5175
this.resolver = this.checker.getEmitResolver(originalSourceFile);
76+
77+
// Initialize reference to shared inline functions registry
78+
this.inlineFunctions = getOrCreateProgramInlineFunctions(program);
5279
}
5380

5481
private currentNodeVisitors: ReadonlyArray<FunctionVisitor<ts.Node>> = [];

src/transformation/index.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import * as ts from "typescript";
22
import * as lua from "../LuaAST";
33
import { getOrUpdate } from "../utils";
4-
import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context";
4+
import {
5+
ObjectVisitor,
6+
TransformationContext,
7+
VisitorMap,
8+
Visitors,
9+
getOrCreateProgramInlineFunctions,
10+
} from "./context";
511
import { standardVisitors } from "./visitors";
612
import { usingTransformer } from "./pre-transformers/using-transformer";
13+
import { AnnotationKind, getSymbolAnnotations } from "./utils/annotations";
14+
15+
// Track which programs have been scanned for inline functions
16+
const scannedPrograms = new WeakSet<ts.Program>();
717

818
export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
919
const objectVisitorMap: Map<ts.SyntaxKind, Array<ObjectVisitor<ts.Node>>> = new Map();
@@ -31,7 +41,81 @@ export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
3141
return result;
3242
}
3343

44+
function collectInlineFunctionsFromSourceFile(program: ts.Program, sourceFile: ts.SourceFile): void {
45+
const inlineFunctions = getOrCreateProgramInlineFunctions(program);
46+
const checker = program.getTypeChecker();
47+
48+
function visit(node: ts.Node): void {
49+
// Handle function declarations
50+
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
51+
const symbol = checker.getSymbolAtLocation(node.name);
52+
if (symbol) {
53+
const annotations = getSymbolAnnotations(symbol);
54+
if (annotations.has(AnnotationKind.Inline)) {
55+
console.log("Inline function:", symbol.name)
56+
inlineFunctions.set(symbol, {
57+
node,
58+
parameters: node.parameters,
59+
body: node.body,
60+
sourceFile,
61+
isProcessing: false,
62+
});
63+
}
64+
}
65+
}
66+
67+
// Handle const/let/var declarations with arrow functions or function expressions
68+
if (ts.isVariableStatement(node)) {
69+
for (const declaration of node.declarationList.declarations) {
70+
if (
71+
declaration.initializer &&
72+
(ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) &&
73+
ts.isIdentifier(declaration.name)
74+
) {
75+
const symbol = checker.getSymbolAtLocation(declaration.name);
76+
if (symbol) {
77+
const annotations = getSymbolAnnotations(symbol);
78+
if (annotations.has(AnnotationKind.Inline) && declaration.initializer.body) {
79+
inlineFunctions.set(symbol, {
80+
node: declaration.initializer,
81+
parameters: declaration.initializer.parameters,
82+
body: declaration.initializer.body,
83+
sourceFile,
84+
isProcessing: false,
85+
});
86+
}
87+
}
88+
}
89+
}
90+
}
91+
92+
ts.forEachChild(node, visit);
93+
}
94+
95+
visit(sourceFile);
96+
}
97+
98+
function ensureInlineFunctionsCollected(program: ts.Program): void {
99+
if (scannedPrograms.has(program)) {
100+
return; // Already scanned this program
101+
}
102+
103+
// Mark as scanned first to prevent infinite recursion
104+
scannedPrograms.add(program);
105+
106+
// Scan all source files for inline functions
107+
const allSourceFiles = program.getSourceFiles();
108+
for (const sourceFile of allSourceFiles) {
109+
if (!sourceFile.isDeclarationFile) {
110+
collectInlineFunctionsFromSourceFile(program, sourceFile);
111+
}
112+
}
113+
}
114+
34115
export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) {
116+
// Ensure all inline functions are collected before transforming any file
117+
ensureInlineFunctionsCollected(program);
118+
35119
const context = new TransformationContext(program, sourceFile, visitorMap);
36120

37121
// TS -> TS pre-transformation

src/transformation/utils/annotations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum AnnotationKind {
77
NoSelf = "noSelf",
88
CustomName = "customName",
99
NoSelfInFile = "noSelfInFile",
10+
Inline = "inline",
1011
}
1112

1213
const annotationValues = new Map(Object.values(AnnotationKind).map(k => [k.toLowerCase(), k]));

src/transformation/utils/diagnostics.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,17 @@ export const incompleteFieldDecoratorWarning = createWarningDiagnosticFactory(
176176
export const unsupportedArrayWithLengthConstructor = createErrorDiagnosticFactory(
177177
`Constructing new Array with length is not supported.`
178178
);
179+
180+
export const inlineRecursiveCall = createErrorDiagnosticFactory("Inline functions cannot call themselves recursively.");
181+
182+
export const inlineNestedInlineCall = createErrorDiagnosticFactory(
183+
"Inline functions cannot call other inline functions."
184+
);
185+
186+
export const inlineComplexBody = createErrorDiagnosticFactory(
187+
"Inline functions must have a simple return expression body (no statements, loops, or complex control flow)."
188+
);
189+
190+
export const inlineMethodNotSupported = createErrorDiagnosticFactory(
191+
"Inline annotation is not supported on class methods and accessors."
192+
);

src/transformation/utils/inline.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import ts, {SignatureDeclaration} from "typescript";
2+
import { TransformationContext } from "../context";
3+
import { InlineFunctionInfo } from "../context";
4+
import * as lua from "../../LuaAST";
5+
import {isMultiFunctionCall} from "../visitors/language-extensions/multi";
6+
import {AnnotationKind, getNodeAnnotations, getSymbolAnnotations} from "./annotations";
7+
8+
interface InlineBodyResult {
9+
paramAssignments: lua.Statement[];
10+
bodyStatements: lua.Statement[];
11+
returnExpressions: lua.Expression[];
12+
hasMultiReturn: boolean;
13+
}
14+
15+
// AST transformer to substitute parameter identifiers with temp variables
16+
function createParameterSubstitutionTransformer(
17+
paramReplacements: Map<string, string>
18+
): ts.TransformerFactory<ts.Node> {
19+
return (context: ts.TransformationContext) => {
20+
const visit = (node: ts.Node): ts.Node => {
21+
// Replace identifier if it matches a parameter
22+
if (ts.isIdentifier(node)) {
23+
const replacementName = paramReplacements.get(node.text);
24+
if (replacementName) {
25+
return ts.factory.createIdentifier(replacementName);
26+
}
27+
}
28+
29+
// Recursively visit children
30+
return ts.visitEachChild(node, visit, context);
31+
};
32+
33+
return visit;
34+
};
35+
}
36+
export function prepareInlineBody(
37+
context: TransformationContext,
38+
inlineInfo: InlineFunctionInfo,
39+
args: ts.NodeArray<ts.Expression>
40+
): InlineBodyResult {
41+
if (inlineInfo.isProcessing) {
42+
throw new Error("Recursive inline call detected (should be caught earlier)");
43+
}
44+
45+
inlineInfo.isProcessing = true;
46+
try {
47+
const { body, parameters } = inlineInfo;
48+
const paramReplacements = new Map<string, string>();
49+
const paramAssignments: lua.Statement[] = [];
50+
51+
let argIndex = 0;
52+
for (const param of parameters) {
53+
if (ts.isIdentifier(param.name) && param.name.text !== "this") {
54+
const paramName = param.name.text;
55+
const tempName = context.createTempName(paramName);
56+
paramReplacements.set(paramName, tempName);
57+
58+
const arg = argIndex < args.length ? args[argIndex] : undefined;
59+
const transformedArg = arg ? context.transformExpression(arg) : lua.createNilLiteral();
60+
paramAssignments.push(
61+
lua.createVariableDeclarationStatement(
62+
lua.createIdentifier(tempName),
63+
transformedArg
64+
)
65+
);
66+
argIndex++;
67+
}
68+
}
69+
70+
// Substitute in body
71+
const substitutedBody = ts.transform(body, [
72+
createParameterSubstitutionTransformer(paramReplacements)
73+
]).transformed[0] as ts.ConciseBody;
74+
75+
// Extract body statements and return expressions
76+
let bodyStatements: lua.Statement[] = [];
77+
let returnExpressions: lua.Expression[] = [];
78+
let hasMultiReturn = false;
79+
80+
if (!ts.isBlock(substitutedBody)) {
81+
returnExpressions = [context.transformExpression(substitutedBody)];
82+
} else {
83+
bodyStatements = context.transformStatements(
84+
substitutedBody.statements.filter(s => !ts.isReturnStatement(s))
85+
);
86+
const returnStmt = substitutedBody.statements.find(ts.isReturnStatement);
87+
const returnExpr = returnStmt?.expression;
88+
if (returnExpr) {
89+
const unwrappedExpr = ts.skipOuterExpressions(returnExpr, ts.OuterExpressionKinds.Assertions);
90+
if (ts.isCallExpression(unwrappedExpr) && isMultiFunctionCall(context, unwrappedExpr)) {
91+
hasMultiReturn = true;
92+
returnExpressions = unwrappedExpr.arguments.map(arg => context.transformExpression(arg));
93+
} else {
94+
returnExpressions = [context.transformExpression(returnExpr)];
95+
}
96+
}
97+
}
98+
99+
return { paramAssignments, bodyStatements, returnExpressions, hasMultiReturn };
100+
} finally {
101+
inlineInfo.isProcessing = false;
102+
}
103+
}
104+
105+
export function embedInlineResult(
106+
context: TransformationContext,
107+
paramAndBodyStmts: lua.Statement[],
108+
returnExprs: lua.Expression[],
109+
hasMulti: boolean,
110+
target?: { // если target задан, то результат присваивается ему
111+
kind: 'variables'; // может быть массив или одна переменная
112+
vars: lua.Identifier[];
113+
},
114+
isReturnContext?: boolean // true, если вызов был внутри return
115+
): lua.Expression {
116+
const allStmts = [...paramAndBodyStmts];
117+
118+
if (isReturnContext) {
119+
// В контексте return: просто вставляем return в do...end
120+
allStmts.push(lua.createReturnStatement(hasMulti ? returnExprs : returnExprs));
121+
context.addPrecedingStatements([lua.createDoStatement(allStmts)]);
122+
return lua.createNilLiteral(); // сам return уже внутри
123+
}
124+
125+
if (target) {
126+
if (hasMulti) {
127+
allStmts.push(lua.createAssignmentStatement(target.vars, returnExprs));
128+
} else {
129+
allStmts.push(lua.createAssignmentStatement(target.vars[0], returnExprs[0]));
130+
}
131+
if (target.vars.length > 1) {
132+
// Для деструктуризации возвращаем nil, объявление переменных снаружи
133+
context.addPrecedingStatements([lua.createDoStatement(allStmts)]);
134+
return lua.createNilLiteral();
135+
} else {
136+
// Для одной переменной: возвращаем её, чтобы использовать как expression
137+
context.addPrecedingStatements([lua.createDoStatement(allStmts)]);
138+
return target.vars[0];
139+
}
140+
}
141+
142+
// Контекст выражения (не присваивание)
143+
const tempVar = lua.createIdentifier(context.createTempName("inline_result"));
144+
allStmts.push(lua.createAssignmentStatement(tempVar, hasMulti ? returnExprs[0] : returnExprs[0]));
145+
context.addPrecedingStatements([lua.createDoStatement(allStmts)]);
146+
return tempVar;
147+
}
148+
149+
export function createInlineAssignment(
150+
paramAndBodyStmts: lua.Statement[],
151+
returnExprs: lua.Expression[],
152+
hasMulti: boolean,
153+
targetVars: lua.Identifier[]
154+
): lua.DoStatement {
155+
const allStmts = [...paramAndBodyStmts];
156+
if (hasMulti) {
157+
allStmts.push(lua.createAssignmentStatement(targetVars, returnExprs));
158+
} else {
159+
allStmts.push(lua.createAssignmentStatement(targetVars[0], returnExprs[0]));
160+
}
161+
return lua.createDoStatement(allStmts);
162+
}
163+
164+
export function isInlineFunctionCandidate(
165+
context: TransformationContext,
166+
node: SignatureDeclaration
167+
): boolean {
168+
// Check for @inline annotation
169+
const symbol = node.name ? context.checker.getSymbolAtLocation(node.name) : undefined;
170+
if (symbol) {
171+
const annotations = getSymbolAnnotations(symbol);
172+
if (annotations.has(AnnotationKind.Inline)) {
173+
return true;
174+
}
175+
}
176+
177+
// Also check node annotations (for cases where symbol might not be available)
178+
const nodeAnnotations = getNodeAnnotations(node);
179+
return nodeAnnotations.has(AnnotationKind.Inline);
180+
}

0 commit comments

Comments
 (0)