import * as path from "path";
import { EmitHost } from "./transpilation";
import * as lua from "./LuaAST";
import { LuaTarget } from "./CompilerOptions";
import { getOrUpdate } from "./utils";
export enum LuaLibFeature {
ArrayAt = "ArrayAt",
ArrayConcat = "ArrayConcat",
ArrayEntries = "ArrayEntries",
ArrayEvery = "ArrayEvery",
ArrayFill = "ArrayFill",
ArrayFilter = "ArrayFilter",
ArrayForEach = "ArrayForEach",
ArrayFind = "ArrayFind",
ArrayFindIndex = "ArrayFindIndex",
ArrayFrom = "ArrayFrom",
ArrayIncludes = "ArrayIncludes",
ArrayIndexOf = "ArrayIndexOf",
ArrayIsArray = "ArrayIsArray",
ArrayJoin = "ArrayJoin",
ArrayMap = "ArrayMap",
ArrayPush = "ArrayPush",
ArrayPushArray = "ArrayPushArray",
ArrayReduce = "ArrayReduce",
ArrayReduceRight = "ArrayReduceRight",
ArrayReverse = "ArrayReverse",
ArrayUnshift = "ArrayUnshift",
ArraySort = "ArraySort",
ArraySlice = "ArraySlice",
ArraySome = "ArraySome",
ArraySplice = "ArraySplice",
ArrayToObject = "ArrayToObject",
ArrayFlat = "ArrayFlat",
ArrayFlatMap = "ArrayFlatMap",
ArraySetLength = "ArraySetLength",
ArrayToReversed = "ArrayToReversed",
ArrayToSorted = "ArrayToSorted",
ArrayToSpliced = "ArrayToSpliced",
ArrayWith = "ArrayWith",
Await = "Await",
Class = "Class",
ClassExtends = "ClassExtends",
CloneDescriptor = "CloneDescriptor",
CountVarargs = "CountVarargs",
Decorate = "Decorate",
DecorateLegacy = "DecorateLegacy",
DecorateParam = "DecorateParam",
Delete = "Delete",
DelegatedYield = "DelegatedYield",
DescriptorGet = "DescriptorGet",
DescriptorSet = "DescriptorSet",
Error = "Error",
FunctionBind = "FunctionBind",
Generator = "Generator",
InstanceOf = "InstanceOf",
InstanceOfObject = "InstanceOfObject",
Iterator = "Iterator",
LuaIteratorSpread = "LuaIteratorSpread",
Map = "Map",
MapGroupBy = "MapGroupBy",
Match = "Match",
MathAtan2 = "MathAtan2",
MathModf = "MathModf",
MathSign = "MathSign",
MathTrunc = "MathTrunc",
New = "New",
Number = "Number",
NumberIsFinite = "NumberIsFinite",
NumberIsInteger = "NumberIsInteger",
NumberIsNaN = "NumberIsNaN",
NumberParseInt = "ParseInt",
NumberParseFloat = "ParseFloat",
NumberToString = "NumberToString",
NumberToFixed = "NumberToFixed",
ObjectAssign = "ObjectAssign",
ObjectDefineProperty = "ObjectDefineProperty",
ObjectEntries = "ObjectEntries",
ObjectFromEntries = "ObjectFromEntries",
ObjectGetOwnPropertyDescriptor = "ObjectGetOwnPropertyDescriptor",
ObjectGetOwnPropertyDescriptors = "ObjectGetOwnPropertyDescriptors",
ObjectGroupBy = "ObjectGroupBy",
ObjectKeys = "ObjectKeys",
ObjectRest = "ObjectRest",
ObjectValues = "ObjectValues",
ParseFloat = "ParseFloat",
ParseInt = "ParseInt",
Promise = "Promise",
PromiseAll = "PromiseAll",
PromiseAllSettled = "PromiseAllSettled",
PromiseAny = "PromiseAny",
PromiseRace = "PromiseRace",
Set = "Set",
SetDescriptor = "SetDescriptor",
SparseArrayNew = "SparseArrayNew",
SparseArrayPush = "SparseArrayPush",
SparseArraySpread = "SparseArraySpread",
WeakMap = "WeakMap",
WeakSet = "WeakSet",
SourceMapTraceBack = "SourceMapTraceBack",
Spread = "Spread",
StringAccess = "StringAccess",
StringCharAt = "StringCharAt",
StringCharCodeAt = "StringCharCodeAt",
StringEndsWith = "StringEndsWith",
StringIncludes = "StringIncludes",
StringPadEnd = "StringPadEnd",
StringPadStart = "StringPadStart",
StringReplace = "StringReplace",
StringReplaceAll = "StringReplaceAll",
StringSlice = "StringSlice",
StringSplit = "StringSplit",
StringStartsWith = "StringStartsWith",
StringSubstr = "StringSubstr",
StringSubstring = "StringSubstring",
StringTrim = "StringTrim",
StringTrimEnd = "StringTrimEnd",
StringTrimStart = "StringTrimStart",
Symbol = "Symbol",
SymbolRegistry = "SymbolRegistry",
TypeOf = "TypeOf",
Unpack = "Unpack",
Using = "Using",
UsingAsync = "UsingAsync",
}
export interface LuaLibFeatureInfo {
dependencies?: LuaLibFeature[];
exports: string[];
}
export type LuaLibModulesInfo = Record;
export function resolveLuaLibDir(luaTarget: LuaTarget) {
const luaLibDir = luaTarget === LuaTarget.Lua50 ? "5.0" : "universal";
return path.resolve(__dirname, path.join("..", "dist", "lualib", luaLibDir));
}
export const luaLibModulesInfoFileName = "lualib_module_info.json";
const luaLibModulesInfo = new Map();
export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost): LuaLibModulesInfo {
if (!luaLibModulesInfo.has(luaTarget)) {
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
const result = emitHost.readFile(lualibPath);
if (result !== undefined) {
luaLibModulesInfo.set(luaTarget, JSON.parse(result) as LuaLibModulesInfo);
} else {
throw new Error(`Could not load lualib dependencies from '${lualibPath}'`);
}
}
return luaLibModulesInfo.get(luaTarget)!;
}
// This caches the names of lualib exports to their LuaLibFeature, avoiding a linear search for every lookup
const lualibExportToFeature = new Map>();
export function getLuaLibExportToFeatureMap(
luaTarget: LuaTarget,
emitHost: EmitHost
): ReadonlyMap {
if (!lualibExportToFeature.has(luaTarget)) {
const luaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost);
const map = new Map();
for (const [feature, info] of Object.entries(luaLibModulesInfo)) {
for (const exportName of info.exports) {
map.set(exportName, feature as LuaLibFeature);
}
}
lualibExportToFeature.set(luaTarget, map);
}
return lualibExportToFeature.get(luaTarget)!;
}
const lualibFeatureCache = new Map>();
export function readLuaLibFeature(feature: LuaLibFeature, luaTarget: LuaTarget, emitHost: EmitHost): string {
const featureMap = getOrUpdate(lualibFeatureCache, luaTarget, () => new Map());
if (!featureMap.has(feature)) {
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
const luaLibFeature = emitHost.readFile(featurePath);
if (luaLibFeature === undefined) {
throw new Error(`Could not load lualib feature from '${featurePath}'`);
}
featureMap.set(feature, luaLibFeature);
}
return featureMap.get(feature)!;
}
export function resolveRecursiveLualibFeatures(
features: Iterable,
luaTarget: LuaTarget,
emitHost: EmitHost,
luaLibModulesInfo: LuaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost)
): LuaLibFeature[] {
const loadedFeatures = new Set();
const result: LuaLibFeature[] = [];
function load(feature: LuaLibFeature): void {
if (loadedFeatures.has(feature)) return;
loadedFeatures.add(feature);
const dependencies = luaLibModulesInfo[feature]?.dependencies;
if (dependencies) {
dependencies.forEach(load);
}
result.push(feature);
}
for (const feature of features) {
load(feature);
}
return result;
}
export function loadInlineLualibFeatures(
features: Iterable,
luaTarget: LuaTarget,
emitHost: EmitHost
): string {
return resolveRecursiveLualibFeatures(features, luaTarget, emitHost)
.map(feature => readLuaLibFeature(feature, luaTarget, emitHost))
.join("\n");
}
export function loadImportedLualibFeatures(
features: Iterable,
luaTarget: LuaTarget,
emitHost: EmitHost
): lua.Statement[] {
const luaLibModuleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
const imports = Array.from(features).flatMap(feature => luaLibModuleInfo[feature].exports);
if (imports.length === 0) {
return [];
}
const requireCall = lua.createCallExpression(lua.createIdentifier("require"), [
lua.createStringLiteral("lualib_bundle"),
]);
const luaLibId = lua.createIdentifier("____lualib");
const importStatement = lua.createVariableDeclarationStatement(luaLibId, requireCall);
const statements: lua.Statement[] = [importStatement];
// local = ____luaLib.
for (const item of imports) {
statements.push(
lua.createVariableDeclarationStatement(
lua.createIdentifier(item),
lua.createTableIndexExpression(luaLibId, lua.createStringLiteral(item))
)
);
}
return statements;
}
const luaLibBundleContent = new Map();
export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): string {
const lualibPath = path.join(resolveLuaLibDir(luaTarget), "lualib_bundle.lua");
if (!luaLibBundleContent.has(lualibPath)) {
const result = emitHost.readFile(lualibPath);
if (result !== undefined) {
luaLibBundleContent.set(lualibPath, result);
} else {
throw new Error(`Could not load lualib bundle from '${lualibPath}'`);
}
}
return luaLibBundleContent.get(lualibPath) as string;
}
export function getLualibBundleReturn(exportedValues: string[]): string {
return `\nreturn {\n${exportedValues.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`;
}
export function buildMinimalLualibBundle(
features: Iterable,
luaTarget: LuaTarget,
emitHost: EmitHost
): string {
const code = loadInlineLualibFeatures(features, luaTarget, emitHost);
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports);
return code + getLualibBundleReturn(exports);
}
export function findUsedLualibFeatures(
luaTarget: LuaTarget,
emitHost: EmitHost,
luaContents: string[]
): Set {
const features = new Set();
const exportToFeatureMap = getLuaLibExportToFeatureMap(luaTarget, emitHost);
for (const lua of luaContents) {
const regex = /^local (\w+) = ____lualib\.(\w+)$/gm;
while (true) {
const match = regex.exec(lua);
if (!match) break;
const [, localName, exportName] = match;
if (localName !== exportName) continue;
const feature = exportToFeatureMap.get(exportName);
if (feature) features.add(feature);
}
}
return features;
}