Skip to content

Commit bb284e1

Browse files
committed
initial commit
0 parents  commit bb284e1

File tree

7 files changed

+518
-0
lines changed

7 files changed

+518
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.js
2+
node_modules/

Compiler.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as ts from "typescript";
2+
import {readFileSync,writeFileSync} from "fs";
3+
4+
import {LuaTranspiler} from "./Transpiler";
5+
import {TSHelper as tsEx} from "./TSHelper";
6+
7+
function compile(fileNames: string[], options: ts.CompilerOptions): void {
8+
fileNames.forEach(fileName => {
9+
const sourceFile = ts.createSourceFile(fileName, readFileSync(fileName).toString(), ts.ScriptTarget.ES2017);
10+
11+
// Print AST for debugging
12+
printAST(sourceFile, 0);
13+
14+
// Transpile AST
15+
console.log(LuaTranspiler.transpileSourceFile(sourceFile));
16+
});
17+
18+
process.exit();
19+
}
20+
21+
function printAST(node: ts.Node, indent: number) {
22+
let indentStr = "";
23+
for (let i=0;i<indent;i++) indentStr += " ";
24+
25+
console.log(indentStr + tsEx.enumName(node.kind, ts.SyntaxKind));
26+
node.forEachChild(child => printAST(child, indent + 1));
27+
}
28+
29+
compile(process.argv.slice(2), {
30+
noEmitOnError: true, noImplicitAny: true,
31+
target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
32+
});

TSHelper.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as ts from "typescript";
2+
3+
export class TSHelper {
4+
// Get all children of a node, required until microsoft fixes node.getChildren()
5+
static getChildren(node: ts.Node): ts.Node[] {
6+
const children = [];
7+
node.forEachChild(child => {
8+
children.push(child);
9+
});
10+
return children;
11+
}
12+
13+
// Get children filtered by function and cast to predefined type
14+
static getChildrenOfType<T>(node: ts.Node, typeFilter: (node: ts.Node) => boolean): T[] {
15+
return <T[]><any>this.getChildren(node).filter(typeFilter);
16+
}
17+
18+
static getFirstChildOfType<T>(node: ts.Node, typeFilter: (node: ts.Node) => boolean): T {
19+
return this.getChildrenOfType<T>(node, typeFilter)[0];
20+
}
21+
22+
// Reverse lookup of enum key by value
23+
static enumName(needle, haystack) {
24+
for (var name in haystack) {
25+
if (haystack[name] == needle) {
26+
return name;
27+
}
28+
}
29+
return "unknown"
30+
}
31+
32+
static isValueType(node: ts.Node): boolean {
33+
return ts.isIdentifier(node) || ts.isLiteralExpression(node) || ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node);
34+
}
35+
}

Transpiler.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import * as ts from "typescript";
2+
3+
import {TSHelper as tsEx} from "./TSHelper";
4+
5+
class TranspileError extends Error {
6+
node: ts.Node;
7+
constructor(message: string, node: ts.Node) {
8+
super(message);
9+
this.node = node;
10+
}
11+
}
12+
13+
export class LuaTranspiler {
14+
// Transpile a source file
15+
static transpileSourceFile(node: ts.SourceFile): string {
16+
return this.transpileBlock(node, "");
17+
}
18+
19+
// Transpile a block
20+
static transpileBlock(node: ts.Node, indent: string): string {
21+
let result = "";
22+
23+
node.forEachChild(child => {
24+
result += this.transpileNode(child, indent);
25+
})
26+
27+
return result;
28+
}
29+
30+
// Transpile a node of unknown kind.
31+
static transpileNode(node: ts.Node, indent: string): string {
32+
//Ignore declarations
33+
if (tsEx.getChildrenOfType(node, child => child.kind == ts.SyntaxKind.DeclareKeyword).length > 0) return "";
34+
35+
switch (node.kind) {
36+
case ts.SyntaxKind.ClassDeclaration:
37+
return this.transpileClass(<ts.ClassDeclaration>node, indent);
38+
case ts.SyntaxKind.FunctionDeclaration:
39+
return this.transpileFunctionDeclaration(<ts.FunctionDeclaration>node, "", indent);
40+
case ts.SyntaxKind.VariableStatement:
41+
return indent + this.transpileVariableStatement(<ts.VariableStatement>node) + "\n";
42+
case ts.SyntaxKind.ExpressionStatement:
43+
return indent + this.transpileExpression(<ts.Expression>tsEx.getChildren(node)[0]) + "\n";
44+
case ts.SyntaxKind.ReturnStatement:
45+
return indent + this.transpileReturn(<ts.ReturnStatement>node) + "\n";
46+
case ts.SyntaxKind.EndOfFileToken:
47+
return "";
48+
default:
49+
throw new TranspileError("Unsupported node kind: " + tsEx.enumName(node.kind, ts.SyntaxKind), node);
50+
}
51+
}
52+
53+
static transpileReturn(node: ts.ReturnStatement): string {
54+
return "return " + this.transpileExpression(tsEx.getChildren(node)[0]);
55+
}
56+
57+
static transpileExpression(node: ts.Node): string {
58+
switch (node.kind) {
59+
case ts.SyntaxKind.BinaryExpression:
60+
return this.transpileBinaryExpression(<ts.BinaryExpression>node);
61+
case ts.SyntaxKind.CallExpression:
62+
return this.transpileCallExpression(<ts.CallExpression>node);
63+
case ts.SyntaxKind.PropertyAccessExpression:
64+
return this.transpilePropertyAccessExpression(<ts.PropertyAccessExpression>node);
65+
case ts.SyntaxKind.Identifier:
66+
// For identifiers simply return their name
67+
return (<ts.Identifier>node).text;
68+
case ts.SyntaxKind.StringLiteral:
69+
const text = (<ts.StringLiteral>node).text;
70+
return `"${text}"`;
71+
case ts.SyntaxKind.NumericLiteral:
72+
return (<ts.NumericLiteral>node).text;
73+
case ts.SyntaxKind.TrueKeyword:
74+
return "true";
75+
case ts.SyntaxKind.FalseKeyword:
76+
return "false";
77+
case ts.SyntaxKind.ArrayLiteralExpression:
78+
return this.transpileArrayLiteral(node);
79+
case ts.SyntaxKind.ObjectLiteralExpression:
80+
return this.transpileObjectLiteral(node)
81+
default:
82+
throw new TranspileError("Unsupported expression kind: " + tsEx.enumName(node.kind, ts.SyntaxKind), node);
83+
}
84+
}
85+
86+
static transpileBinaryExpression(node: ts.BinaryExpression): string {
87+
let [lhs, operator, rhs] = tsEx.getChildren(node);
88+
return this.transpileExpression(lhs) + ts.tokenToString(operator.kind) + this.transpileExpression(rhs);
89+
}
90+
91+
static transpileCallExpression(node: ts.CallExpression): string {
92+
const children = tsEx.getChildren(node);
93+
94+
let callPath = this.transpileExpression(children[0]);
95+
// Remove last . with :
96+
if (callPath.indexOf(".") > -1) {
97+
callPath = callPath.substr(0, callPath.lastIndexOf(".")) + ":" + callPath.substr(callPath.lastIndexOf(".") + 1);
98+
}
99+
100+
const parameters = [];
101+
children.slice(1).forEach(param => {
102+
parameters.push(this.transpileExpression(param));
103+
})
104+
105+
return `${callPath}(${parameters.join(",")})`;
106+
}
107+
108+
static transpilePropertyAccessExpression(node: ts.PropertyAccessExpression): string {
109+
let parts = [];
110+
node.forEachChild(child => {
111+
switch(child.kind) {
112+
case ts.SyntaxKind.ThisKeyword:
113+
parts.push("self");
114+
break;
115+
case ts.SyntaxKind.Identifier:
116+
parts.push((<ts.Identifier>child).escapedText);
117+
break;
118+
case ts.SyntaxKind.CallExpression:
119+
parts.push(this.transpileCallExpression(<ts.CallExpression>child));
120+
break;
121+
case ts.SyntaxKind.PropertyAccessExpression:
122+
parts.push(this.transpilePropertyAccessExpression(<ts.PropertyAccessExpression>child));
123+
break;
124+
default:
125+
throw new TranspileError("Unsupported access kind: " + tsEx.enumName(child.kind, ts.SyntaxKind), node);
126+
}
127+
});
128+
return parts.join(".");
129+
}
130+
131+
// Transpile a variable statement
132+
static transpileVariableStatement(node: ts.VariableStatement): string {
133+
let result = "";
134+
135+
let list = tsEx.getFirstChildOfType<ts.VariableDeclarationList>(node, ts.isVariableDeclarationList);
136+
list.forEachChild(declaration => {
137+
result += this.transpileVariableDeclaration(<ts.VariableDeclaration>declaration);
138+
});
139+
140+
return result;
141+
}
142+
143+
static transpileVariableDeclaration(node: ts.VariableDeclaration): string {
144+
// Find variable identifier
145+
const identifier = tsEx.getFirstChildOfType<ts.Identifier>(node, ts.isIdentifier);
146+
const valueLiteral = tsEx.getChildren(node)[1];
147+
148+
const value = this.transpileExpression(valueLiteral);
149+
150+
return `local ${identifier.escapedText} = ${value}`;
151+
}
152+
153+
static transpileFunctionDeclaration(node: ts.Declaration, path: string, indent: string): string {
154+
let result = "";
155+
const identifier = tsEx.getFirstChildOfType<ts.Identifier>(node, ts.isIdentifier);
156+
const methodName = identifier.escapedText;
157+
const parameters = tsEx.getChildrenOfType<ts.ParameterDeclaration>(node, ts.isParameter);
158+
const block = tsEx.getFirstChildOfType<ts.Block>(node, ts.isBlock);
159+
160+
// Build parameter string
161+
let paramNames = [];
162+
parameters.forEach(param => {
163+
paramNames.push(tsEx.getFirstChildOfType<ts.Identifier>(param, ts.isIdentifier).escapedText);
164+
});
165+
166+
// Build function header
167+
result += indent + `function ${path}${methodName}(${paramNames.join(",")})\n`;
168+
169+
result += this.transpileBlock(block, indent + " ");
170+
171+
// Close function block
172+
result += indent + "end\n";
173+
174+
return result;
175+
}
176+
177+
// Transpile a class declaration
178+
static transpileClass(node: ts.ClassDeclaration, indent: string): string {
179+
180+
// Find first identifier
181+
const identifierNode = tsEx.getFirstChildOfType<ts.Identifier>(node, child => ts.isIdentifier(child));
182+
183+
// Write class declaration
184+
const className = <string>identifierNode.escapedText;
185+
let result = indent + `${className} = ${className} or {}\n`;
186+
187+
// Get all properties
188+
const properties = tsEx.getChildrenOfType<ts.PropertyDeclaration>(node, ts.isPropertyDeclaration);
189+
190+
let staticFields: ts.PropertyDeclaration[] = [];
191+
let instanceFields: ts.PropertyDeclaration[] = [];
192+
193+
// Divide properties in static and instance fields
194+
for (const p of properties) {
195+
// Check if property is static
196+
const isStatic = tsEx.getChildrenOfType(p, child => child.kind == ts.SyntaxKind.StaticKeyword).length > 0;
197+
198+
// Find value assignment
199+
const assignments = tsEx.getChildrenOfType(p, tsEx.isValueType);
200+
201+
// [0] is name literal, [1] is value, ignore fields with no assigned value
202+
if (assignments.length < 2)
203+
continue;
204+
205+
if (isStatic) {
206+
staticFields.push(p);
207+
} else {
208+
instanceFields.push(p);
209+
}
210+
}
211+
212+
// Add static declarations
213+
for (const f of staticFields) {
214+
// Get identifier
215+
const fieldIdentifier = tsEx.getFirstChildOfType<ts.Identifier>(f, ts.isIdentifier);
216+
const fieldName = fieldIdentifier.escapedText;
217+
218+
// Get value at index 1 (index 0 is field name)
219+
const valueNode = tsEx.getChildrenOfType<ts.Node>(f, tsEx.isValueType)[1];
220+
let value = this.transpileExpression(valueNode);
221+
222+
// Build lua assignment string
223+
result += indent + `${className}.${fieldName} = ${value}\n`;
224+
}
225+
226+
// Try to find constructor
227+
const constructor = tsEx.getFirstChildOfType<ts.ConstructorDeclaration>(node, ts.isConstructorDeclaration);
228+
if (constructor) {
229+
// Add constructor plus initialisation of instance fields
230+
result += indent + this.transpileConstructor(constructor, className, instanceFields, indent);
231+
} else {
232+
// No constructor, make one to set all instance fields if there are any
233+
if (instanceFields.length > 0) {
234+
// Create empty constructor and add instance fields
235+
result += indent + this.transpileConstructor(ts.createConstructor([],[],[], ts.createBlock([],true)), className, instanceFields, indent);
236+
}
237+
}
238+
239+
// Find all methods
240+
const methods = tsEx.getChildrenOfType<ts.MethodDeclaration>(node, ts.isMethodDeclaration);
241+
methods.forEach(method => {
242+
result += this.transpileFunctionDeclaration(method, `${className}:`, indent);
243+
});
244+
245+
return result;
246+
}
247+
248+
static transpileConstructor(node: ts.ConstructorDeclaration, className: string, instanceFields: ts.PropertyDeclaration[], indent: string): string {
249+
let result = indent + `function ${className}:constructor()\n`;
250+
251+
// Add in instance field declarations
252+
for (const f of instanceFields) {
253+
// Get identifier
254+
const fieldIdentifier = tsEx.getFirstChildOfType<ts.Identifier>(f, ts.isIdentifier);
255+
const fieldName = fieldIdentifier.escapedText;
256+
257+
// Get value at index 1 (index 0 is field name)
258+
const valueNode = tsEx.getChildrenOfType<ts.Node>(f, tsEx.isValueType)[1];
259+
let value = this.transpileExpression(valueNode);
260+
261+
result += indent + ` self.${fieldName} = ${value}\n`;
262+
}
263+
264+
// Transpile constructor body
265+
tsEx.getChildrenOfType<ts.Block>(node, ts.isBlock).forEach(child => {
266+
result += this.transpileBlock(child, indent + " ");
267+
});
268+
269+
return result + `${indent}end\n`;
270+
}
271+
272+
static transpileArrayLiteral(node: ts.Node): string {
273+
let values = [];
274+
275+
node.forEachChild(child => {
276+
values.push(this.transpileExpression(child));
277+
});
278+
279+
return "{" + values.join(",") + "}";
280+
}
281+
282+
static transpileObjectLiteral(node: ts.Node): string {
283+
let properties = [];
284+
// Add all property assignments
285+
tsEx.getChildrenOfType<ts.PropertyAssignment>(node, ts.isPropertyAssignment).forEach(assignment => {
286+
const [key, value] = tsEx.getChildren(assignment);
287+
if (ts.isIdentifier(key)) {
288+
properties.push(`["${key.escapedText}"]=`+this.transpileExpression(value));
289+
} else {
290+
const index = this.transpileExpression(<ts.Expression>key);
291+
properties.push(`[${index}]=`+this.transpileExpression(value));
292+
}
293+
});
294+
295+
return "{" + properties.join(",") + "}";
296+
}
297+
}

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)