Skip to content

Commit 2757741

Browse files
committed
2 parents 7facc11 + 93ff05d commit 2757741

32 files changed

Lines changed: 1181 additions & 236 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.12.0
4+
5+
- Reworked how tstl detects and rewrites `require` statements during dependency resolution. This should reduce the amount of false-positive matches of require statements: require statements in string literals or comments should no longer be detected by tstl. This means require statements in string literals or comments can survive the transpiler without causing a 'could not resolve lua sources' error or getting rewritten into nonsense.
6+
- Now using `math.mod` for Lua 5.0 modulo operations.
7+
38
## 1.11.0
49

510
- **[Breaking]** Upgraded TypeScript to 4.9.

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "typescript-to-lua",
3-
"version": "1.11.1",
3+
"version": "1.12.1",
44
"description": "A generic TypeScript to Lua transpiler. Write your code in TypeScript and publish Lua!",
55
"repository": "https://github.com/TypeScriptToLua/TypeScriptToLua",
66
"homepage": "https://typescripttolua.github.io/",

src/CompilerOptions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export interface TransformerImport {
1212
after?: boolean;
1313
afterDeclarations?: boolean;
1414
type?: "program" | "config" | "checker" | "raw" | "compilerOptions";
15+
1516
[option: string]: any;
1617
}
1718

1819
export interface LuaPluginImport {
1920
name: string;
2021
import?: string;
22+
2123
[option: string]: any;
2224
}
2325

@@ -49,6 +51,7 @@ export enum LuaLibImportKind {
4951
None = "none",
5052
Inline = "inline",
5153
Require = "require",
54+
RequireMinimal = "require-minimal",
5255
}
5356

5457
export enum LuaTarget {

src/LuaLib.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from "path";
22
import { EmitHost } from "./transpilation";
33
import * as lua from "./LuaAST";
44
import { LuaTarget } from "./CompilerOptions";
5+
import { getOrUpdate } from "./utils";
56

67
export enum LuaLibFeature {
78
ArrayConcat = "ArrayConcat",
@@ -109,6 +110,7 @@ export interface LuaLibFeatureInfo {
109110
dependencies?: LuaLibFeature[];
110111
exports: string[];
111112
}
113+
112114
export type LuaLibModulesInfo = Record<LuaLibFeature, LuaLibFeatureInfo>;
113115

114116
export function resolveLuaLibDir(luaTarget: LuaTarget) {
@@ -117,27 +119,55 @@ export function resolveLuaLibDir(luaTarget: LuaTarget) {
117119
}
118120

119121
export const luaLibModulesInfoFileName = "lualib_module_info.json";
120-
const luaLibModulesInfo = new Map<string, LuaLibModulesInfo>();
122+
const luaLibModulesInfo = new Map<LuaTarget, LuaLibModulesInfo>();
123+
121124
export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost): LuaLibModulesInfo {
122-
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
123-
if (!luaLibModulesInfo.has(lualibPath)) {
125+
if (!luaLibModulesInfo.has(luaTarget)) {
126+
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
124127
const result = emitHost.readFile(lualibPath);
125128
if (result !== undefined) {
126-
luaLibModulesInfo.set(lualibPath, JSON.parse(result) as LuaLibModulesInfo);
129+
luaLibModulesInfo.set(luaTarget, JSON.parse(result) as LuaLibModulesInfo);
127130
} else {
128131
throw new Error(`Could not load lualib dependencies from '${lualibPath}'`);
129132
}
130133
}
131-
return luaLibModulesInfo.get(lualibPath) as LuaLibModulesInfo;
134+
return luaLibModulesInfo.get(luaTarget)!;
135+
}
136+
137+
// This caches the names of lualib exports to their LuaLibFeature, avoiding a linear search for every lookup
138+
const lualibExportToFeature = new Map<LuaTarget, ReadonlyMap<string, LuaLibFeature>>();
139+
140+
export function getLuaLibExportToFeatureMap(
141+
luaTarget: LuaTarget,
142+
emitHost: EmitHost
143+
): ReadonlyMap<string, LuaLibFeature> {
144+
if (!lualibExportToFeature.has(luaTarget)) {
145+
const luaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost);
146+
const map = new Map<string, LuaLibFeature>();
147+
for (const [feature, info] of Object.entries(luaLibModulesInfo)) {
148+
for (const exportName of info.exports) {
149+
map.set(exportName, feature as LuaLibFeature);
150+
}
151+
}
152+
lualibExportToFeature.set(luaTarget, map);
153+
}
154+
155+
return lualibExportToFeature.get(luaTarget)!;
132156
}
133157

158+
const lualibFeatureCache = new Map<LuaTarget, Map<LuaLibFeature, string>>();
159+
134160
export function readLuaLibFeature(feature: LuaLibFeature, luaTarget: LuaTarget, emitHost: EmitHost): string {
135-
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
136-
const luaLibFeature = emitHost.readFile(featurePath);
137-
if (luaLibFeature === undefined) {
138-
throw new Error(`Could not load lualib feature from '${featurePath}'`);
161+
const featureMap = getOrUpdate(lualibFeatureCache, luaTarget, () => new Map());
162+
if (!featureMap.has(feature)) {
163+
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
164+
const luaLibFeature = emitHost.readFile(featurePath);
165+
if (luaLibFeature === undefined) {
166+
throw new Error(`Could not load lualib feature from '${featurePath}'`);
167+
}
168+
featureMap.set(feature, luaLibFeature);
139169
}
140-
return luaLibFeature;
170+
return featureMap.get(feature)!;
141171
}
142172

143173
export function resolveRecursiveLualibFeatures(
@@ -173,14 +203,9 @@ export function loadInlineLualibFeatures(
173203
luaTarget: LuaTarget,
174204
emitHost: EmitHost
175205
): string {
176-
let result = "";
177-
178-
for (const feature of resolveRecursiveLualibFeatures(features, luaTarget, emitHost)) {
179-
const luaLibFeature = readLuaLibFeature(feature, luaTarget, emitHost);
180-
result += luaLibFeature + "\n";
181-
}
182-
183-
return result;
206+
return resolveRecursiveLualibFeatures(features, luaTarget, emitHost)
207+
.map(feature => readLuaLibFeature(feature, luaTarget, emitHost))
208+
.join("\n");
184209
}
185210

186211
export function loadImportedLualibFeatures(
@@ -191,13 +216,13 @@ export function loadImportedLualibFeatures(
191216
const luaLibModuleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
192217

193218
const imports = Array.from(features).flatMap(feature => luaLibModuleInfo[feature].exports);
219+
if (imports.length === 0) {
220+
return [];
221+
}
194222

195223
const requireCall = lua.createCallExpression(lua.createIdentifier("require"), [
196224
lua.createStringLiteral("lualib_bundle"),
197225
]);
198-
if (imports.length === 0) {
199-
return [];
200-
}
201226

202227
const luaLibId = lua.createIdentifier("____lualib");
203228
const importStatement = lua.createVariableDeclarationStatement(luaLibId, requireCall);
@@ -215,6 +240,7 @@ export function loadImportedLualibFeatures(
215240
}
216241

217242
const luaLibBundleContent = new Map<string, string>();
243+
218244
export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): string {
219245
const lualibPath = path.join(resolveLuaLibDir(luaTarget), "lualib_bundle.lua");
220246
if (!luaLibBundleContent.has(lualibPath)) {
@@ -228,3 +254,42 @@ export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): strin
228254

229255
return luaLibBundleContent.get(lualibPath) as string;
230256
}
257+
258+
export function getLualibBundleReturn(exportedValues: string[]): string {
259+
return `\nreturn {\n${exportedValues.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`;
260+
}
261+
262+
export function buildMinimalLualibBundle(
263+
features: Iterable<LuaLibFeature>,
264+
luaTarget: LuaTarget,
265+
emitHost: EmitHost
266+
): string {
267+
const code = loadInlineLualibFeatures(features, luaTarget, emitHost);
268+
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
269+
const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports);
270+
271+
return code + getLualibBundleReturn(exports);
272+
}
273+
274+
export function findUsedLualibFeatures(
275+
luaTarget: LuaTarget,
276+
emitHost: EmitHost,
277+
luaContents: string[]
278+
): Set<LuaLibFeature> {
279+
const features = new Set<LuaLibFeature>();
280+
const exportToFeatureMap = getLuaLibExportToFeatureMap(luaTarget, emitHost);
281+
282+
for (const lua of luaContents) {
283+
const regex = /^local (\w+) = ____lualib\.(\w+)$/gm;
284+
while (true) {
285+
const match = regex.exec(lua);
286+
if (!match) break;
287+
const [, localName, exportName] = match;
288+
if (localName !== exportName) continue;
289+
const feature = exportToFeatureMap.get(exportName);
290+
if (feature) features.add(feature);
291+
}
292+
}
293+
294+
return features;
295+
}

src/LuaPrinter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ export class LuaPrinter {
235235

236236
const luaTarget = this.options.luaTarget ?? LuaTarget.Universal;
237237
const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require;
238-
if (luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0) {
238+
if (
239+
(luaLibImport === LuaLibImportKind.Require || luaLibImport === LuaLibImportKind.RequireMinimal) &&
240+
file.luaLibFeatures.size > 0
241+
) {
239242
// Import lualib features
240243
sourceChunks = this.printStatementArray(
241244
loadImportedLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost)

src/lualib-build/plugin.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { SourceNode } from "source-map";
22
import * as ts from "typescript";
33
import * as tstl from "..";
44
import * as path from "path";
5-
import { LuaLibFeature, LuaLibModulesInfo, luaLibModulesInfoFileName, resolveRecursiveLualibFeatures } from "../LuaLib";
5+
import {
6+
getLualibBundleReturn,
7+
LuaLibFeature,
8+
LuaLibModulesInfo,
9+
luaLibModulesInfoFileName,
10+
resolveRecursiveLualibFeatures,
11+
} from "../LuaLib";
612
import { EmitHost, ProcessedFile } from "../transpilation/utils";
713
import {
814
isExportAlias,
@@ -63,7 +69,7 @@ class LuaLibPlugin implements tstl.Plugin {
6369
// Concatenate lualib files into bundle with exports table and add lualib_bundle.lua to results
6470
let lualibBundle = orderedFeatures.map(f => exportedLualibFeatures.get(LuaLibFeature[f])).join("\n");
6571
const exports = allFeatures.flatMap(feature => luaLibModuleInfo[feature].exports);
66-
lualibBundle += `\nreturn {\n${exports.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`;
72+
lualibBundle += getLualibBundleReturn(exports);
6773
result.push({ fileName: "lualib_bundle.lua", code: lualibBundle });
6874

6975
return diagnostics;

src/transformation/builtins/array.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TransformationContext } from "../context";
55
import { unsupportedProperty } from "../utils/diagnostics";
66
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
77
import { transformArguments, transformCallAndArguments } from "../visitors/call";
8-
import { findFirstNonOuterParent, typeAlwaysHasSomeOfFlags } from "../utils/typescript";
8+
import { expressionResultIsUsed, typeAlwaysHasSomeOfFlags } from "../utils/typescript";
99
import { moveToPrecedingTemp } from "../visitors/expression-list";
1010
import { isUnpackCall, wrapInTable } from "../utils/lua-ast";
1111

@@ -54,8 +54,6 @@ function transformSingleElementArrayPush(
5454
caller: lua.Expression,
5555
param: lua.Expression
5656
): lua.Expression {
57-
const expressionIsUsed = !ts.isExpressionStatement(findFirstNonOuterParent(node));
58-
5957
const arrayIdentifier = lua.isIdentifier(caller) ? caller : moveToPrecedingTemp(context, caller);
6058

6159
// #array + 1
@@ -65,6 +63,7 @@ function transformSingleElementArrayPush(
6563
lua.SyntaxKind.AdditionOperator
6664
);
6765

66+
const expressionIsUsed = expressionResultIsUsed(node);
6867
if (expressionIsUsed) {
6968
// store length in a temp
7069
lengthExpression = moveToPrecedingTemp(context, lengthExpression);

src/transformation/utils/diagnostics.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,11 @@ export const unsupportedOptionalCompileMembersOnly = createErrorDiagnosticFactor
155155
export const undefinedInArrayLiteral = createErrorDiagnosticFactory(
156156
"Array literals may not contain undefined or null."
157157
);
158+
159+
export const invalidMethodCallExtensionUse = createErrorDiagnosticFactory(
160+
"This language extension must be called as a method."
161+
);
162+
163+
export const invalidSpreadInCallExtension = createErrorDiagnosticFactory(
164+
"Spread elements are not supported in call extensions."
165+
);

src/transformation/utils/language-extensions.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ts from "typescript";
22
import { TransformationContext } from "../context";
3+
import { invalidMethodCallExtensionUse, invalidSpreadInCallExtension } from "./diagnostics";
34

45
export enum ExtensionKind {
56
MultiFunction = "MultiFunction",
@@ -53,6 +54,7 @@ export enum ExtensionKind {
5354
TableAddKeyType = "TableAddKey",
5455
TableAddKeyMethodType = "TableAddKeyMethod",
5556
}
57+
5658
const extensionValues: Set<string> = new Set(Object.values(ExtensionKind));
5759

5860
export function getExtensionKindForType(context: TransformationContext, type: ts.Type): ExtensionKind | undefined {
@@ -119,3 +121,78 @@ export function getIterableExtensionKindForNode(
119121
const type = context.checker.getTypeAtLocation(node);
120122
return getIterableExtensionTypeForType(context, type);
121123
}
124+
125+
export const methodExtensionKinds: ReadonlySet<ExtensionKind> = new Set<ExtensionKind>([
126+
ExtensionKind.AdditionOperatorMethodType,
127+
ExtensionKind.SubtractionOperatorMethodType,
128+
ExtensionKind.MultiplicationOperatorMethodType,
129+
ExtensionKind.DivisionOperatorMethodType,
130+
ExtensionKind.ModuloOperatorMethodType,
131+
ExtensionKind.PowerOperatorMethodType,
132+
ExtensionKind.FloorDivisionOperatorMethodType,
133+
ExtensionKind.BitwiseAndOperatorMethodType,
134+
ExtensionKind.BitwiseOrOperatorMethodType,
135+
ExtensionKind.BitwiseExclusiveOrOperatorMethodType,
136+
ExtensionKind.BitwiseLeftShiftOperatorMethodType,
137+
ExtensionKind.BitwiseRightShiftOperatorMethodType,
138+
ExtensionKind.ConcatOperatorMethodType,
139+
ExtensionKind.LessThanOperatorMethodType,
140+
ExtensionKind.GreaterThanOperatorMethodType,
141+
ExtensionKind.NegationOperatorMethodType,
142+
ExtensionKind.BitwiseNotOperatorMethodType,
143+
ExtensionKind.LengthOperatorMethodType,
144+
ExtensionKind.TableDeleteMethodType,
145+
ExtensionKind.TableGetMethodType,
146+
ExtensionKind.TableHasMethodType,
147+
ExtensionKind.TableSetMethodType,
148+
ExtensionKind.TableAddKeyMethodType,
149+
]);
150+
151+
export function getNaryCallExtensionArgs(
152+
context: TransformationContext,
153+
node: ts.CallExpression,
154+
kind: ExtensionKind,
155+
numArgs: number
156+
): readonly ts.Expression[] | undefined {
157+
let expressions: readonly ts.Expression[];
158+
if (node.arguments.some(ts.isSpreadElement)) {
159+
context.diagnostics.push(invalidSpreadInCallExtension(node));
160+
return undefined;
161+
}
162+
if (methodExtensionKinds.has(kind)) {
163+
if (!(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))) {
164+
context.diagnostics.push(invalidMethodCallExtensionUse(node));
165+
return undefined;
166+
}
167+
if (node.arguments.length < numArgs - 1) {
168+
// assumed to be TS error
169+
return undefined;
170+
}
171+
expressions = [node.expression.expression, ...node.arguments];
172+
} else {
173+
if (node.arguments.length < numArgs) {
174+
// assumed to be TS error
175+
return undefined;
176+
}
177+
expressions = node.arguments;
178+
}
179+
return expressions;
180+
}
181+
182+
export function getUnaryCallExtensionArg(
183+
context: TransformationContext,
184+
node: ts.CallExpression,
185+
kind: ExtensionKind
186+
): ts.Expression | undefined {
187+
return getNaryCallExtensionArgs(context, node, kind, 1)?.[0];
188+
}
189+
190+
export function getBinaryCallExtensionArgs(
191+
context: TransformationContext,
192+
node: ts.CallExpression,
193+
kind: ExtensionKind
194+
): readonly [ts.Expression, ts.Expression] | undefined {
195+
const expressions = getNaryCallExtensionArgs(context, node, kind, 2);
196+
if (expressions === undefined) return undefined;
197+
return [expressions[0], expressions[1]];
198+
}

0 commit comments

Comments
 (0)