@tilua/compiler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -0
- package/dist/chunk-S5C4VH6A.js +2310 -0
- package/dist/cli.cjs +2344 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +54 -0
- package/dist/index.cjs +2369 -0
- package/dist/index.d.cts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +55 -0
- package/package.json +55 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,2344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_node_fs2 = require("fs");
|
|
6
|
+
var import_node_path3 = require("path");
|
|
7
|
+
|
|
8
|
+
// src/bundle.ts
|
|
9
|
+
var import_node_fs = require("fs");
|
|
10
|
+
var import_node_path2 = require("path");
|
|
11
|
+
var import_parser3 = require("@tilua/parser");
|
|
12
|
+
var import_luau_parser2 = require("luau-parser");
|
|
13
|
+
|
|
14
|
+
// src/config.ts
|
|
15
|
+
var import_node_path = require("path");
|
|
16
|
+
var import_parser = require("@tilua/parser");
|
|
17
|
+
function resolveConfig(input, entry2, cwd = process.cwd()) {
|
|
18
|
+
if (input === void 0) return (0, import_parser.findConfig)(entry2);
|
|
19
|
+
if (typeof input === "string") return (0, import_parser.loadConfig)((0, import_node_path.resolve)(cwd, input));
|
|
20
|
+
const path = (0, import_node_path.resolve)(cwd, "tilua.config.json");
|
|
21
|
+
const text = JSON.stringify(input, null, 2);
|
|
22
|
+
return (0, import_parser.loadConfig)(path, { readFile: (file) => file === path ? text : import_parser.nodeHost.readFile(file) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lower.ts
|
|
26
|
+
var import_parser2 = require("@tilua/parser");
|
|
27
|
+
var import_luau_parser = require("luau-parser");
|
|
28
|
+
|
|
29
|
+
// src/luau.ts
|
|
30
|
+
var NO_SPAN = { line: { start: 0, end: 0 }, column: { start: 0, end: 0 } };
|
|
31
|
+
var span = () => ({ line: { ...NO_SPAN.line }, column: { ...NO_SPAN.column } });
|
|
32
|
+
var identifier = (name) => ({ type: "Identifier", name, ...span() });
|
|
33
|
+
var nil = () => ({ type: "NilLiteral", ...span() });
|
|
34
|
+
var boolean = (value) => ({ type: "BooleanLiteral", value, ...span() });
|
|
35
|
+
var number = (value, raw = String(value)) => ({ type: "NumberLiteral", value, raw, ...span() });
|
|
36
|
+
var string = (value) => ({ type: "StringLiteral", value, raw: "", ...span() });
|
|
37
|
+
var member = (object, property) => isLuauName(property) ? { type: "MemberExpression", object, property: identifier(property), ...span() } : index(object, string(property));
|
|
38
|
+
var index = (object, key) => ({ type: "IndexExpression", object, index: key, ...span() });
|
|
39
|
+
var call = (callee, args2) => ({ type: "CallExpression", callee, arguments: args2, ...span() });
|
|
40
|
+
var methodCall = (object, method, args2) => ({ type: "MethodCallExpression", object, method: identifier(method), arguments: args2, ...span() });
|
|
41
|
+
var functionExpression = (func) => ({ type: "FunctionExpression", func, ...span() });
|
|
42
|
+
var binary = (operator, left, right) => ({ type: "BinaryExpression", operator, left, right, ...span() });
|
|
43
|
+
var unary = (operator, argument) => ({ type: "UnaryExpression", operator, argument, ...span() });
|
|
44
|
+
var parenthesized = (expression) => ({ type: "ParenthesizedExpression", expression, ...span() });
|
|
45
|
+
var table = (fields) => ({ type: "TableExpression", fields, ...span() });
|
|
46
|
+
var field = (key, value) => isLuauName(key) ? { type: "TableFieldNamed", name: identifier(key), value } : { type: "TableFieldComputed", key: string(key), value };
|
|
47
|
+
var block = (statements) => ({ type: "Block", statements, ...span() });
|
|
48
|
+
var local = (names, init) => ({
|
|
49
|
+
type: "LocalStatement",
|
|
50
|
+
names: names.map((name) => ({ type: "TypedIdentifier", name, ...span() })),
|
|
51
|
+
init,
|
|
52
|
+
...span()
|
|
53
|
+
});
|
|
54
|
+
var assign = (targets, values) => ({ type: "AssignmentStatement", targets, values, ...span() });
|
|
55
|
+
var callStatement = (expression) => ({ type: "CallStatement", expression, ...span() });
|
|
56
|
+
var ifThen = (condition, body) => ({
|
|
57
|
+
type: "IfStatement",
|
|
58
|
+
clauses: [{ type: "IfClause", condition, body: block(body), ...span() }],
|
|
59
|
+
...span()
|
|
60
|
+
});
|
|
61
|
+
var doBlock = (body) => ({ type: "DoStatement", body: block(body), ...span() });
|
|
62
|
+
var breakStatement = () => ({ type: "BreakStatement", ...span() });
|
|
63
|
+
var repeatUntil = (body, condition) => ({ type: "RepeatStatement", body: block(body), condition, ...span() });
|
|
64
|
+
var genericFor = (variables, iterators, body) => ({
|
|
65
|
+
type: "GenericForStatement",
|
|
66
|
+
variables: variables.map((name) => ({ type: "TypedIdentifier", name, ...span() })),
|
|
67
|
+
iterators,
|
|
68
|
+
body: block(body),
|
|
69
|
+
...span()
|
|
70
|
+
});
|
|
71
|
+
var numericFor = (variable, start, end, body) => ({
|
|
72
|
+
type: "NumericForStatement",
|
|
73
|
+
variable: { type: "TypedIdentifier", name: variable, ...span() },
|
|
74
|
+
start,
|
|
75
|
+
end,
|
|
76
|
+
body: block(body),
|
|
77
|
+
...span()
|
|
78
|
+
});
|
|
79
|
+
var returns = (args2) => ({ type: "ReturnStatement", arguments: args2, ...span() });
|
|
80
|
+
var functionBody = (params, statements, hasVarargs = false) => ({
|
|
81
|
+
type: "FunctionBody",
|
|
82
|
+
generics: [],
|
|
83
|
+
params: params.map((name) => ({ type: "FunctionParameter", name, ...span() })),
|
|
84
|
+
hasVarargs,
|
|
85
|
+
body: block(statements),
|
|
86
|
+
...span()
|
|
87
|
+
});
|
|
88
|
+
var localFunction = (name, func) => ({ type: "LocalFunctionStatement", name: identifier(name), func, ...span() });
|
|
89
|
+
var LUAU_KEYWORDS = /* @__PURE__ */ new Set([
|
|
90
|
+
"and",
|
|
91
|
+
"break",
|
|
92
|
+
"do",
|
|
93
|
+
"else",
|
|
94
|
+
"elseif",
|
|
95
|
+
"end",
|
|
96
|
+
"false",
|
|
97
|
+
"for",
|
|
98
|
+
"function",
|
|
99
|
+
"if",
|
|
100
|
+
"in",
|
|
101
|
+
"local",
|
|
102
|
+
"nil",
|
|
103
|
+
"not",
|
|
104
|
+
"or",
|
|
105
|
+
"repeat",
|
|
106
|
+
"return",
|
|
107
|
+
"then",
|
|
108
|
+
"true",
|
|
109
|
+
"until",
|
|
110
|
+
"while"
|
|
111
|
+
]);
|
|
112
|
+
function isLuauName(name) {
|
|
113
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/console.ts
|
|
117
|
+
var CONSOLE_RUNTIME = `
|
|
118
|
+
local __NAME__ = (function()
|
|
119
|
+
local console = {}
|
|
120
|
+
local MAX_DEPTH = 4
|
|
121
|
+
local ESCAPES = { ["\\n"] = "\\\\n", ["\\r"] = "\\\\r", ["\\t"] = "\\\\t", ["\\""] = "\\\\\\"", ["\\\\"] = "\\\\\\\\" }
|
|
122
|
+
|
|
123
|
+
local function quote(text)
|
|
124
|
+
return '"' .. text:gsub('[%c"\\\\]', function(c) return ESCAPES[c] or ("\\\\%d"):format(c:byte()) end) .. '"'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
-- A table with 1..n and nothing else prints as a list, as it was written.
|
|
128
|
+
local function isArray(value)
|
|
129
|
+
local count = 0
|
|
130
|
+
for key in pairs(value) do
|
|
131
|
+
if type(key) ~= "number" then return false end
|
|
132
|
+
count = count + 1
|
|
133
|
+
end
|
|
134
|
+
return count == #value
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
local function isIdentifier(key)
|
|
138
|
+
return type(key) == "string" and key:match("^[A-Za-z_][A-Za-z0-9_]*$") ~= nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
local render
|
|
142
|
+
|
|
143
|
+
local function renderTable(value, meta, seen, depth, indent)
|
|
144
|
+
if seen[value] then return "<cycle>" end
|
|
145
|
+
if depth > MAX_DEPTH then return "..." end
|
|
146
|
+
seen[value] = true
|
|
147
|
+
local inner = indent .. " "
|
|
148
|
+
local parts = {}
|
|
149
|
+
if isArray(value) then
|
|
150
|
+
for i = 1, #value do
|
|
151
|
+
parts[#parts + 1] = inner .. render(value[i], nil, seen, depth + 1, inner)
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
local keys = {}
|
|
155
|
+
for key in pairs(value) do keys[#keys + 1] = key end
|
|
156
|
+
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
|
157
|
+
for _, key in ipairs(keys) do
|
|
158
|
+
local name = isIdentifier(key) and key or ("[" .. render(key, nil, seen, depth + 1, inner) .. "]")
|
|
159
|
+
parts[#parts + 1] = inner .. name .. " = " .. render(value[key], nil, seen, depth + 1, inner)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
seen[value] = nil
|
|
163
|
+
if #parts == 0 then return "{}" end
|
|
164
|
+
local open = isArray(value) and "[" or "{"
|
|
165
|
+
local close = isArray(value) and "]" or "}"
|
|
166
|
+
return open .. "\\n" .. table.concat(parts, ",\\n") .. "\\n" .. indent .. close
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
-- \`meta\` is what the compiler knew about this value and the runtime cannot:
|
|
170
|
+
-- a function's type, and the code it was written as.
|
|
171
|
+
render = function(value, meta, seen, depth, indent)
|
|
172
|
+
local kind = type(value)
|
|
173
|
+
if kind == "string" then return quote(value) end
|
|
174
|
+
if kind == "number" or kind == "boolean" or kind == "nil" then return tostring(value) end
|
|
175
|
+
if kind == "function" then
|
|
176
|
+
if meta == nil then return "function" end
|
|
177
|
+
if meta[2] == nil or meta[2] == "" then return meta[1] end
|
|
178
|
+
return meta[1] .. " " .. meta[2]
|
|
179
|
+
end
|
|
180
|
+
if kind == "table" then
|
|
181
|
+
local shown = rawget(value, "__tostring") == nil and getmetatable(value)
|
|
182
|
+
-- A value with its own \`__tostring\` (a Roblox Vector3, a class
|
|
183
|
+
-- instance that says how it reads) is trusted to describe itself.
|
|
184
|
+
if shown ~= nil and rawget(shown, "__tostring") ~= nil then return tostring(value) end
|
|
185
|
+
return renderTable(value, meta, seen, depth, indent)
|
|
186
|
+
end
|
|
187
|
+
return tostring(value)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
local function describe(meta, index, value)
|
|
191
|
+
return render(value, meta ~= nil and meta[index] or nil, {}, 1, "")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
local function join(meta, count, ...)
|
|
195
|
+
local parts = {}
|
|
196
|
+
for i = 1, count do
|
|
197
|
+
parts[#parts + 1] = describe(meta, i, (select(i, ...)))
|
|
198
|
+
end
|
|
199
|
+
return table.concat(parts, " ")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
-- A line of the bundle, as the place in the project that wrote it.
|
|
203
|
+
local lines = __LINES__
|
|
204
|
+
local function place(line)
|
|
205
|
+
local origin = lines ~= nil and lines[tonumber(line)]
|
|
206
|
+
if origin == nil then return nil end
|
|
207
|
+
return origin[1] .. ":" .. tostring(origin[2])
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
-- Luau names the bundle in every frame; the project's files are what the
|
|
211
|
+
-- reader knows. Frames the map has nothing for are dropped: they are this
|
|
212
|
+
-- runtime's own, and the bundle's.
|
|
213
|
+
local function trace(level)
|
|
214
|
+
local raw = debug.traceback(nil, level)
|
|
215
|
+
local out = {}
|
|
216
|
+
for frame in raw:gmatch("[^\\n]+") do
|
|
217
|
+
local line = frame:match(":(%d+)")
|
|
218
|
+
local at = line ~= nil and place(line) or nil
|
|
219
|
+
if at ~= nil then
|
|
220
|
+
local name = frame:match("function ([%w_.:]+)")
|
|
221
|
+
out[#out + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
return table.concat(out, "\\n")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
-- Never on the caller's thread: reporting a problem must not become one.
|
|
228
|
+
local detach = (task ~= nil and task.spawn) or function(f) return f() end
|
|
229
|
+
|
|
230
|
+
local function raise(prefix, meta, count, ...)
|
|
231
|
+
local message = prefix .. join(meta, count, ...)
|
|
232
|
+
local where = trace(3)
|
|
233
|
+
detach(function()
|
|
234
|
+
error(where ~= "" and (message .. "\\n" .. where) or message, 0)
|
|
235
|
+
end)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
function console.log(meta, ...)
|
|
239
|
+
print(join(meta, select("#", ...), ...))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
function console.warn(meta, ...)
|
|
243
|
+
raise("[warn] ", meta, select("#", ...), ...)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
function console.error(meta, ...)
|
|
247
|
+
raise("", meta, select("#", ...), ...)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
return console
|
|
251
|
+
end)()
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
// src/names.ts
|
|
255
|
+
var Names = class _Names {
|
|
256
|
+
constructor(taken) {
|
|
257
|
+
this.taken = taken;
|
|
258
|
+
}
|
|
259
|
+
taken;
|
|
260
|
+
/** The names `programs` mention — a bundle takes every module's, so that
|
|
261
|
+
* a name shared across modules (the module table) collides with none. */
|
|
262
|
+
static from(...programs) {
|
|
263
|
+
const taken = /* @__PURE__ */ new Set(["select", "pairs", "table", "tostring", "setmetatable", "error"]);
|
|
264
|
+
const walk = (value) => {
|
|
265
|
+
if (!value || typeof value !== "object") return;
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
for (const item of value) walk(item);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const node = value;
|
|
271
|
+
if (typeof node.name === "string" && node.name) taken.add(node.name);
|
|
272
|
+
for (const [key, child] of Object.entries(value)) {
|
|
273
|
+
if (key !== "line" && key !== "column") walk(child);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
for (const program of programs) walk(program);
|
|
277
|
+
return new _Names(taken);
|
|
278
|
+
}
|
|
279
|
+
/** A copy that goes on to take names independently — one per module, each
|
|
280
|
+
* starting from what the whole bundle already takes. */
|
|
281
|
+
fork() {
|
|
282
|
+
return new _Names(new Set(this.taken));
|
|
283
|
+
}
|
|
284
|
+
/** `base`, or `base2`, `base3`, ... — whichever is free first. */
|
|
285
|
+
fresh(base) {
|
|
286
|
+
let name = base;
|
|
287
|
+
for (let n = 2; this.taken.has(name); n++) name = `${base}${n}`;
|
|
288
|
+
this.taken.add(name);
|
|
289
|
+
return name;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/lower.ts
|
|
294
|
+
function lower(program, scopes, options = {}) {
|
|
295
|
+
return new Lowerer(program, scopes, options).run();
|
|
296
|
+
}
|
|
297
|
+
var Lowerer = class {
|
|
298
|
+
constructor(source, scopes, options) {
|
|
299
|
+
this.source = source;
|
|
300
|
+
this.scopes = scopes;
|
|
301
|
+
this.options = options;
|
|
302
|
+
this.lua51 = options.target === "lua51";
|
|
303
|
+
this.names = options.names ? options.names.fork() : Names.from(source);
|
|
304
|
+
this.exportsName = this.names.fresh("exports");
|
|
305
|
+
for (const binding of scopes.bindings.values()) {
|
|
306
|
+
if (binding.declarationNode) this.bindingByDeclaration.set(binding.declarationNode, binding);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
source;
|
|
310
|
+
scopes;
|
|
311
|
+
options;
|
|
312
|
+
names;
|
|
313
|
+
diagnostics = [];
|
|
314
|
+
helpers = /* @__PURE__ */ new Map();
|
|
315
|
+
/** The binding each declaration node creates. */
|
|
316
|
+
bindingByDeclaration = /* @__PURE__ */ new Map();
|
|
317
|
+
/** tilua names that are Luau keywords, and what they are called instead. */
|
|
318
|
+
renamed = /* @__PURE__ */ new Map();
|
|
319
|
+
/** Bindings read through something else: an export through `exports.x`,
|
|
320
|
+
* an import through its module's table. */
|
|
321
|
+
rewrites = /* @__PURE__ */ new Map();
|
|
322
|
+
exportsName;
|
|
323
|
+
/** Globals generated code calls, renamed where the source shadows them. */
|
|
324
|
+
builtins = /* @__PURE__ */ new Map();
|
|
325
|
+
/** Whether the output has to be plain Lua 5.1 rather than Luau. */
|
|
326
|
+
lua51;
|
|
327
|
+
/** The source line each emitted statement came from. */
|
|
328
|
+
origins = /* @__PURE__ */ new WeakMap();
|
|
329
|
+
run() {
|
|
330
|
+
const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
|
|
331
|
+
const helpers = this.helperDefinitions();
|
|
332
|
+
const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
|
|
333
|
+
return {
|
|
334
|
+
statements: [...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
335
|
+
exportsName: this.exportsName,
|
|
336
|
+
module: this.info,
|
|
337
|
+
diagnostics: this.diagnostics,
|
|
338
|
+
origins: this.origins
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
report(node, message) {
|
|
342
|
+
this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
|
|
343
|
+
}
|
|
344
|
+
// --------------------------------------------------------
|
|
345
|
+
// Names
|
|
346
|
+
// --------------------------------------------------------
|
|
347
|
+
/** A tilua name as Luau can write it: `local` is a keyword there. */
|
|
348
|
+
name(name) {
|
|
349
|
+
if (!LUAU_KEYWORDS.has(name)) return name;
|
|
350
|
+
let renamed = this.renamed.get(name);
|
|
351
|
+
if (!renamed) {
|
|
352
|
+
renamed = this.names.fresh(`${name}_`);
|
|
353
|
+
this.renamed.set(name, renamed);
|
|
354
|
+
}
|
|
355
|
+
return renamed;
|
|
356
|
+
}
|
|
357
|
+
bindingIdOf(node) {
|
|
358
|
+
return this.scopes.bindingOf.get(node) ?? this.bindingByDeclaration.get(node)?.id;
|
|
359
|
+
}
|
|
360
|
+
/** How Luau refers to a name: its own local, or what it was rewritten to. */
|
|
361
|
+
reference(node) {
|
|
362
|
+
const id = this.bindingIdOf(node);
|
|
363
|
+
const rewrite = id === void 0 ? void 0 : this.rewrites.get(id);
|
|
364
|
+
return rewrite ? rewrite() : identifier(this.name(node.name));
|
|
365
|
+
}
|
|
366
|
+
/** A global the generated code calls — `pairs`, `table`, ... — under a name
|
|
367
|
+
* the source cannot have shadowed. */
|
|
368
|
+
builtin(global) {
|
|
369
|
+
let local2 = this.builtins.get(global);
|
|
370
|
+
if (!local2) {
|
|
371
|
+
const shadowed = [...this.scopes.bindings.values()].some((b) => b.name === global && b.kind !== "global");
|
|
372
|
+
if (!shadowed) return identifier(global);
|
|
373
|
+
local2 = this.names.fresh(`tilua_${global}`);
|
|
374
|
+
this.builtins.set(global, local2);
|
|
375
|
+
}
|
|
376
|
+
return identifier(local2);
|
|
377
|
+
}
|
|
378
|
+
helper(kind) {
|
|
379
|
+
let name = this.helpers.get(kind);
|
|
380
|
+
if (!name) {
|
|
381
|
+
name = this.names.fresh(`tilua_${kind}`);
|
|
382
|
+
this.helpers.set(kind, name);
|
|
383
|
+
}
|
|
384
|
+
return identifier(name);
|
|
385
|
+
}
|
|
386
|
+
/** The local a library's runtime table gets in this file, emitting it the
|
|
387
|
+
* first time something asks. Keyed by library and by the key the library
|
|
388
|
+
* itself chose, so two libraries' tables never collide. */
|
|
389
|
+
loweringRuntime(index2, runtime2) {
|
|
390
|
+
const key = `${index2}:${runtime2}`;
|
|
391
|
+
let name = this.loweringNames.get(key);
|
|
392
|
+
if (!name) {
|
|
393
|
+
name = this.names.fresh(`tilua_${runtime2.replace(/[^A-Za-z0-9_]/g, "_")}`);
|
|
394
|
+
this.loweringNames.set(key, name);
|
|
395
|
+
this.loweringUsed.push({ index: index2, runtime: runtime2, name });
|
|
396
|
+
}
|
|
397
|
+
return name;
|
|
398
|
+
}
|
|
399
|
+
loweringNames = /* @__PURE__ */ new Map();
|
|
400
|
+
loweringUsed = [];
|
|
401
|
+
/** What a library says `receiver:method(...)` is. The last library
|
|
402
|
+
* loaded is asked first, so a project's own library can answer for a
|
|
403
|
+
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
404
|
+
* method call, which is what a value that answers to the method itself
|
|
405
|
+
* wants (`text:upper()`). */
|
|
406
|
+
/** `console:log(a, f)` and its two siblings.
|
|
407
|
+
*
|
|
408
|
+
* Handled here rather than through a library's lowering because what they
|
|
409
|
+
* need is not a different callee but *more than was written*: a function
|
|
410
|
+
* prints as its type and its code, and neither survives to runtime. The
|
|
411
|
+
* compiler is the only thing that has both, so it passes them along:
|
|
412
|
+
*
|
|
413
|
+
* console:log(a, double)
|
|
414
|
+
* -- becomes
|
|
415
|
+
* tilua_console.log({ [2] = { "(x: number) => number", "(x: number) => x * 2" } }, a, double)
|
|
416
|
+
*
|
|
417
|
+
* The meta table is `nil` when nothing was worth saying, which is most
|
|
418
|
+
* calls. `console` itself is never passed: it holds nothing. */
|
|
419
|
+
consoleCall(node) {
|
|
420
|
+
if (node.object.type !== "Identifier" || node.object.name !== "console") return void 0;
|
|
421
|
+
const method = node.method.name;
|
|
422
|
+
if (method !== "log" && method !== "warn" && method !== "error") return void 0;
|
|
423
|
+
const binding = this.scopes.bindingOf.get(node.object);
|
|
424
|
+
if (binding !== void 0 && this.scopes.bindings.get(binding)?.kind !== "global") return void 0;
|
|
425
|
+
const meta = this.consoleMeta(node.arguments);
|
|
426
|
+
return call(
|
|
427
|
+
member(this.helper("console"), method),
|
|
428
|
+
[meta ?? nil(), ...this.values(node.arguments)]
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
/** What the compiler knows about the arguments and the runtime cannot:
|
|
432
|
+
* for each function argument, the type it was inferred as and the code it
|
|
433
|
+
* was written as. Keyed by position, so an argument that needs nothing
|
|
434
|
+
* costs nothing. */
|
|
435
|
+
consoleMeta(args2) {
|
|
436
|
+
const fields = [];
|
|
437
|
+
args2.forEach((argument, index2) => {
|
|
438
|
+
const type = this.options.types?.typeOf.get(argument);
|
|
439
|
+
if (!type || !isFunctionType(type)) return;
|
|
440
|
+
const written = this.writtenSource(this.declarationOf(argument) ?? argument);
|
|
441
|
+
fields.push({
|
|
442
|
+
type: "TableFieldComputed",
|
|
443
|
+
key: number(index2 + 1),
|
|
444
|
+
value: table([
|
|
445
|
+
{ type: "TableFieldPositional", value: string((0, import_parser2.formatType)(type)) },
|
|
446
|
+
{ type: "TableFieldPositional", value: string(written) }
|
|
447
|
+
])
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
return fields.length ? table(fields) : void 0;
|
|
451
|
+
}
|
|
452
|
+
/** What a name was declared as: the value of `const f = ...`, or the whole
|
|
453
|
+
* `function f() { ... }`. Built once, on the first `console` call that
|
|
454
|
+
* needs it — most files have none. */
|
|
455
|
+
declarationOf(argument) {
|
|
456
|
+
if (argument.type !== "Identifier") return void 0;
|
|
457
|
+
if (!this.declarations) {
|
|
458
|
+
this.declarations = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const node of allNodes(this.source.body)) {
|
|
460
|
+
const statement = node;
|
|
461
|
+
if (statement.type === "VariableDeclaration") {
|
|
462
|
+
statement.names.forEach((target, index2) => {
|
|
463
|
+
const value = statement.init[index2];
|
|
464
|
+
if (target.type !== "IdentifierPattern" || !value) return;
|
|
465
|
+
const binding = this.bindingByDeclaration.get(target);
|
|
466
|
+
if (binding) this.declarations.set(binding.id, value);
|
|
467
|
+
});
|
|
468
|
+
} else if (statement.type === "FunctionDeclaration") {
|
|
469
|
+
const binding = this.bindingByDeclaration.get(statement.name);
|
|
470
|
+
if (binding) this.declarations.set(binding.id, statement);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const id = this.scopes.bindingOf.get(argument);
|
|
475
|
+
return id === void 0 ? void 0 : this.declarations.get(id);
|
|
476
|
+
}
|
|
477
|
+
declarations;
|
|
478
|
+
/** The source `node` was written as, read straight out of the file. */
|
|
479
|
+
writtenSource(node) {
|
|
480
|
+
const source = this.options.source;
|
|
481
|
+
if (source === void 0) return "";
|
|
482
|
+
const lines = source.split(/\r?\n/);
|
|
483
|
+
if (node.line.start === node.line.end) {
|
|
484
|
+
return (lines[node.line.start - 1] ?? "").slice(node.column.start - 1, node.column.end - 1);
|
|
485
|
+
}
|
|
486
|
+
const first = (lines[node.line.start - 1] ?? "").slice(node.column.start - 1);
|
|
487
|
+
const middle = lines.slice(node.line.start, node.line.end - 1);
|
|
488
|
+
const last = (lines[node.line.end - 1] ?? "").slice(0, node.column.end - 1);
|
|
489
|
+
const indent = /^\s*/.exec(lines[node.line.start - 1] ?? "")[0];
|
|
490
|
+
const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
|
|
491
|
+
return [first, ...middle.map(trim), trim(last)].join("\n");
|
|
492
|
+
}
|
|
493
|
+
loweredMethodCall(node) {
|
|
494
|
+
const lowerings = this.options.lowerings;
|
|
495
|
+
if (!lowerings?.length) return void 0;
|
|
496
|
+
const receiver = this.options.types?.typeOf.get(node.object);
|
|
497
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
498
|
+
const answer = lowerings[i].plugin.methodCall?.({
|
|
499
|
+
method: node.method.name,
|
|
500
|
+
receiver,
|
|
501
|
+
argumentCount: node.arguments.length,
|
|
502
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
503
|
+
});
|
|
504
|
+
if (answer) return { callee: answer.callee, passReceiver: answer.passReceiver !== false };
|
|
505
|
+
}
|
|
506
|
+
return void 0;
|
|
507
|
+
}
|
|
508
|
+
helperDefinitions() {
|
|
509
|
+
const out2 = [];
|
|
510
|
+
const assign2 = this.helpers.get("assign");
|
|
511
|
+
if (assign2) {
|
|
512
|
+
out2.push(localFunction(assign2, functionBody(["target"], [
|
|
513
|
+
numericFor("i", number(1), selectCount(this.builtin("select")), [
|
|
514
|
+
local(["source"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
|
|
515
|
+
ifThen(binary("~=", identifier("source"), nil()), [
|
|
516
|
+
genericFor(["key", "value"], [call(this.builtin("pairs"), [identifier("source")])], [
|
|
517
|
+
assign([index(identifier("target"), identifier("key"))], [identifier("value")])
|
|
518
|
+
])
|
|
519
|
+
])
|
|
520
|
+
]),
|
|
521
|
+
returns([identifier("target")])
|
|
522
|
+
], true)));
|
|
523
|
+
}
|
|
524
|
+
for (const { index: index2, runtime: runtime2, name } of this.loweringUsed) {
|
|
525
|
+
const { plugin, from } = this.options.lowerings[index2];
|
|
526
|
+
const source = plugin.runtime?.[runtime2];
|
|
527
|
+
if (source === void 0) {
|
|
528
|
+
this.diagnostics.push({
|
|
529
|
+
message: `'${from}' asked for a runtime it does not have: '${runtime2}'`,
|
|
530
|
+
line: 1,
|
|
531
|
+
column: 1
|
|
532
|
+
});
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
out2.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name)).body.statements);
|
|
536
|
+
}
|
|
537
|
+
const console2 = this.helpers.get("console");
|
|
538
|
+
if (console2) {
|
|
539
|
+
const lines = this.options.module?.lines ?? "nil";
|
|
540
|
+
out2.push(...(0, import_luau_parser.parse)(CONSOLE_RUNTIME.replace(/__NAME__/g, console2).replace(/__LINES__/g, lines)).body.statements);
|
|
541
|
+
}
|
|
542
|
+
const concat = this.helpers.get("concat");
|
|
543
|
+
if (concat) {
|
|
544
|
+
out2.push(localFunction(concat, functionBody([], [
|
|
545
|
+
local(["result"], [table([])]),
|
|
546
|
+
numericFor("i", number(1), selectCount(this.builtin("select")), [
|
|
547
|
+
local(["part"], [call(this.builtin("select"), [identifier("i"), vararg()])]),
|
|
548
|
+
callStatement(call(member(this.builtin("table"), "move"), [
|
|
549
|
+
identifier("part"),
|
|
550
|
+
number(1),
|
|
551
|
+
unary("#", identifier("part")),
|
|
552
|
+
binary("+", unary("#", identifier("result")), number(1)),
|
|
553
|
+
identifier("result")
|
|
554
|
+
]))
|
|
555
|
+
]),
|
|
556
|
+
returns([identifier("result")])
|
|
557
|
+
], true)));
|
|
558
|
+
}
|
|
559
|
+
return out2;
|
|
560
|
+
}
|
|
561
|
+
// --------------------------------------------------------
|
|
562
|
+
// Modules
|
|
563
|
+
// --------------------------------------------------------
|
|
564
|
+
info = { names: [], links: [], stars: [], exports: false };
|
|
565
|
+
/** The module body, in the order an ES module runs:
|
|
566
|
+
*
|
|
567
|
+
* local a, b, util -- every top-level name, declared first
|
|
568
|
+
* function exports.f() ... -- every top-level function, hoisted
|
|
569
|
+
* util = require("util") -- the imports
|
|
570
|
+
* ... -- the rest, in source order */
|
|
571
|
+
module(context) {
|
|
572
|
+
const statements = this.source.body.statements;
|
|
573
|
+
const exportsTable = () => identifier(this.exportsName);
|
|
574
|
+
const { names, links, stars } = this.info;
|
|
575
|
+
const locals = [];
|
|
576
|
+
const requires = [];
|
|
577
|
+
const moduleLocals = /* @__PURE__ */ new Map();
|
|
578
|
+
const importedFrom = /* @__PURE__ */ new Map();
|
|
579
|
+
const resolve3 = (source) => {
|
|
580
|
+
const key = context.resolve(source.value);
|
|
581
|
+
if (key === void 0) this.report(source, `Cannot find module '${source.value}'`);
|
|
582
|
+
return key;
|
|
583
|
+
};
|
|
584
|
+
const moduleLocal = (key) => {
|
|
585
|
+
let local2 = moduleLocals.get(key);
|
|
586
|
+
if (!local2) {
|
|
587
|
+
local2 = this.names.fresh(moduleName(key));
|
|
588
|
+
moduleLocals.set(key, local2);
|
|
589
|
+
locals.push(local2);
|
|
590
|
+
requires.push(assign([identifier(local2)], [call(context.require, [string(key)])]));
|
|
591
|
+
}
|
|
592
|
+
return local2;
|
|
593
|
+
};
|
|
594
|
+
const load = (key) => {
|
|
595
|
+
if (!moduleLocals.has(key)) requires.push(callStatement(call(context.require, [string(key)])));
|
|
596
|
+
};
|
|
597
|
+
const exportBinding = (node, exported) => {
|
|
598
|
+
const binding = node && this.bindingByDeclaration.get(node);
|
|
599
|
+
if (!binding) return;
|
|
600
|
+
const existing = this.rewrites.get(binding.id);
|
|
601
|
+
if (existing) {
|
|
602
|
+
const first = memberChain(existing());
|
|
603
|
+
links.push({ name: exported, module: context.name, imported: first?.[first.length - 1] });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
names.push(exported);
|
|
607
|
+
this.rewrites.set(binding.id, () => member(exportsTable(), exported));
|
|
608
|
+
};
|
|
609
|
+
for (const statement of statements) {
|
|
610
|
+
if (statement.type !== "ImportStatement" || statement.isTypeOnly) continue;
|
|
611
|
+
const specifiers = [
|
|
612
|
+
...statement.defaultImport ? [{ local: statement.defaultImport, imported: "default" }] : [],
|
|
613
|
+
...statement.namespaceImport ? [{ local: statement.namespaceImport, imported: void 0 }] : [],
|
|
614
|
+
...statement.specifiers.map((s) => ({ local: s.local, imported: s.imported.name }))
|
|
615
|
+
];
|
|
616
|
+
const used = specifiers.filter((s) => (this.bindingByDeclaration.get(s.local)?.references.length ?? 0) > 0);
|
|
617
|
+
if (!used.length) continue;
|
|
618
|
+
const key = resolve3(statement.source);
|
|
619
|
+
if (key === void 0) continue;
|
|
620
|
+
const local2 = moduleLocal(key);
|
|
621
|
+
for (const s of used) {
|
|
622
|
+
const binding = this.bindingByDeclaration.get(s.local);
|
|
623
|
+
importedFrom.set(binding.id, { module: key, imported: s.imported });
|
|
624
|
+
const imported = s.imported;
|
|
625
|
+
this.rewrites.set(binding.id, imported === void 0 ? () => identifier(local2) : () => member(identifier(local2), imported));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const statement of statements) {
|
|
629
|
+
switch (statement.type) {
|
|
630
|
+
case "ExportStatement": {
|
|
631
|
+
const declaration = statement.declaration;
|
|
632
|
+
if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
|
|
633
|
+
exportBinding(declaration.name, declaration.name.name);
|
|
634
|
+
} else for (const pattern of declaration.names.flatMap(identifierPatterns)) exportBinding(pattern, pattern.name);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "ExportDefaultStatement":
|
|
638
|
+
names.push("default");
|
|
639
|
+
break;
|
|
640
|
+
case "ExportNamedStatement": {
|
|
641
|
+
if (statement.source) {
|
|
642
|
+
const key = resolve3(statement.source);
|
|
643
|
+
if (key === void 0) break;
|
|
644
|
+
load(key);
|
|
645
|
+
for (const s of statement.specifiers) links.push({ name: s.exported.name, module: key, imported: s.local.name });
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
for (const s of statement.specifiers) {
|
|
649
|
+
const declaration = topLevelDeclaration(statements, s.local.name);
|
|
650
|
+
const binding = declaration && this.bindingByDeclaration.get(declaration.node);
|
|
651
|
+
const from = binding && importedFrom.get(binding.id);
|
|
652
|
+
if (from) links.push({ name: s.exported.name, ...from });
|
|
653
|
+
else if (declaration?.type === "Declaration") exportBinding(declaration.node, s.exported.name);
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "ExportAllStatement": {
|
|
658
|
+
const key = resolve3(statement.source);
|
|
659
|
+
if (key === void 0) break;
|
|
660
|
+
load(key);
|
|
661
|
+
stars.push(key);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
this.info.exports = names.length > 0 || links.length > 0 || stars.length > 0;
|
|
667
|
+
for (const statement of statements) {
|
|
668
|
+
const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
|
|
669
|
+
if (declaration.type === "VariableDeclaration") {
|
|
670
|
+
for (const pattern of declaration.names.flatMap(identifierPatterns)) {
|
|
671
|
+
if (!this.isRewritten(pattern)) locals.push(this.name(pattern.name));
|
|
672
|
+
}
|
|
673
|
+
} else if ((declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") && !this.isRewritten(declaration.name)) {
|
|
674
|
+
locals.push(this.name(declaration.name.name));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const hoisted = [];
|
|
678
|
+
const body = [];
|
|
679
|
+
for (const statement of statements) {
|
|
680
|
+
const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
|
|
681
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
682
|
+
hoisted.push(this.from(this.functionStatement(
|
|
683
|
+
this.reference(declaration.name),
|
|
684
|
+
declaration,
|
|
685
|
+
this.functionBody(declaration.func),
|
|
686
|
+
false
|
|
687
|
+
), declaration));
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (declaration.type === "ClassDeclaration") {
|
|
691
|
+
body.push(...this.classDeclaration(declaration, "assign"));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
switch (declaration.type) {
|
|
695
|
+
case "VariableDeclaration":
|
|
696
|
+
body.push(...this.variableDeclaration(declaration, "assign"));
|
|
697
|
+
break;
|
|
698
|
+
case "ExportDefaultStatement": {
|
|
699
|
+
const exported = declaration.declaration;
|
|
700
|
+
if (exported.type === "ClassDeclaration") {
|
|
701
|
+
body.push(...this.classDeclaration(exported, "assign"));
|
|
702
|
+
body.push(assign([member(exportsTable(), "default")], [this.reference(exported.name)]));
|
|
703
|
+
} else {
|
|
704
|
+
body.push(assign([member(exportsTable(), "default")], [this.expression(exported)]));
|
|
705
|
+
}
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "ReturnStatement":
|
|
709
|
+
if (declaration.arguments.length) this.report(declaration, "A module cannot return a value; export it instead");
|
|
710
|
+
body.push(returns([]));
|
|
711
|
+
break;
|
|
712
|
+
case "ImportStatement":
|
|
713
|
+
case "ExportNamedStatement":
|
|
714
|
+
case "ExportAllStatement":
|
|
715
|
+
break;
|
|
716
|
+
default:
|
|
717
|
+
body.push(...this.statement(declaration));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const declarations = [];
|
|
721
|
+
for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
|
|
722
|
+
return [...declarations, ...hoisted, ...requires, ...body];
|
|
723
|
+
}
|
|
724
|
+
/** Is `expression` a table iterated directly — not an iterator function
|
|
725
|
+
* such as `pairs(t)` or `string.gmatch(s, p)`? Known from its type. */
|
|
726
|
+
iteratesTable(expression) {
|
|
727
|
+
const types = this.options.types;
|
|
728
|
+
const type = types?.typeOf.get(expression);
|
|
729
|
+
if (!types || !type) return false;
|
|
730
|
+
const seen = /* @__PURE__ */ new Set();
|
|
731
|
+
const table2 = (t) => {
|
|
732
|
+
if (seen.has(t)) return false;
|
|
733
|
+
seen.add(t);
|
|
734
|
+
switch (t.kind) {
|
|
735
|
+
case "array":
|
|
736
|
+
case "tuple":
|
|
737
|
+
return true;
|
|
738
|
+
case "object":
|
|
739
|
+
return !t.class;
|
|
740
|
+
case "union":
|
|
741
|
+
return t.types.every((m) => m.kind === "primitive" && m.name === "nil" || table2(m));
|
|
742
|
+
case "intersection":
|
|
743
|
+
return t.types.some(table2);
|
|
744
|
+
case "genericRef": {
|
|
745
|
+
const alias = types.aliases.get(t.name);
|
|
746
|
+
return alias !== void 0 && table2(alias);
|
|
747
|
+
}
|
|
748
|
+
default:
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
return table2(type);
|
|
753
|
+
}
|
|
754
|
+
isRewritten(node) {
|
|
755
|
+
const binding = this.bindingByDeclaration.get(node);
|
|
756
|
+
return binding !== void 0 && this.rewrites.has(binding.id);
|
|
757
|
+
}
|
|
758
|
+
// --------------------------------------------------------
|
|
759
|
+
// Statements
|
|
760
|
+
// --------------------------------------------------------
|
|
761
|
+
block(block2, prelude = []) {
|
|
762
|
+
return block([...prelude, ...this.blockStatements(block2.statements)]);
|
|
763
|
+
}
|
|
764
|
+
// --------------------------------------------------------
|
|
765
|
+
// Hoisting
|
|
766
|
+
// --------------------------------------------------------
|
|
767
|
+
//
|
|
768
|
+
// A function declaration is visible to its whole block. Luau's
|
|
769
|
+
// `local function f` is not, so a function something above it refers to is
|
|
770
|
+
// declared as a local at the top of the block and assigned where it is
|
|
771
|
+
// written: another function's body can call it once the block has run
|
|
772
|
+
// that far. (Calling it straight away, above its declaration, is an error
|
|
773
|
+
// scope analysis reports.) A bundle's module and a file's top level hoist
|
|
774
|
+
// the whole function instead.
|
|
775
|
+
/** Declarations in `statements` that something before them reads: a
|
|
776
|
+
* function called above it, or a value a closure in its own initializer
|
|
777
|
+
* reads (`local t = { f = function() return t end }`). Lua's `local`
|
|
778
|
+
* starts after its statement, so those names are declared at the top of
|
|
779
|
+
* the block and assigned where they are written. */
|
|
780
|
+
referencedAhead(statements) {
|
|
781
|
+
const functions = /* @__PURE__ */ new Set();
|
|
782
|
+
const classes = /* @__PURE__ */ new Set();
|
|
783
|
+
const variables = /* @__PURE__ */ new Set();
|
|
784
|
+
const names = [];
|
|
785
|
+
const before = (node, line, column) => line < node.line.start || line === node.line.start && column < node.column.start;
|
|
786
|
+
for (const statement of statements) {
|
|
787
|
+
if (statement.type === "FunctionDeclaration" || statement.type === "ClassDeclaration") {
|
|
788
|
+
const binding = this.bindingByDeclaration.get(statement.name);
|
|
789
|
+
if (binding?.references.some((r) => before(statement.name, r.line.start, r.column.start))) {
|
|
790
|
+
if (statement.type === "ClassDeclaration") classes.add(statement);
|
|
791
|
+
else functions.add(statement);
|
|
792
|
+
names.push(this.name(statement.name.name));
|
|
793
|
+
}
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
797
|
+
const patterns = statement.names.flatMap(identifierPatterns);
|
|
798
|
+
const end = { line: { start: statement.line.end }, column: { start: statement.column.end } };
|
|
799
|
+
const needed = patterns.some((pattern) => {
|
|
800
|
+
const binding = this.bindingByDeclaration.get(pattern);
|
|
801
|
+
return binding?.references.some((r) => before(end, r.line.start, r.column.start));
|
|
802
|
+
});
|
|
803
|
+
if (!needed) continue;
|
|
804
|
+
variables.add(statement);
|
|
805
|
+
for (const pattern of patterns) names.push(this.name(pattern.name));
|
|
806
|
+
}
|
|
807
|
+
return { functions, classes, variables, names };
|
|
808
|
+
}
|
|
809
|
+
blockStatements(statements) {
|
|
810
|
+
const ahead = this.referencedAhead(statements);
|
|
811
|
+
if (!ahead.names.length) return statements.flatMap((s) => this.statement(s));
|
|
812
|
+
return [
|
|
813
|
+
local(ahead.names, []),
|
|
814
|
+
...statements.flatMap((s) => {
|
|
815
|
+
if (s.type === "FunctionDeclaration" && ahead.functions.has(s)) {
|
|
816
|
+
return [this.functionStatement(this.reference(s.name), s, this.functionBody(s.func), false)];
|
|
817
|
+
}
|
|
818
|
+
if (s.type === "VariableDeclaration" && ahead.variables.has(s)) {
|
|
819
|
+
return this.variableDeclaration(s, "assign");
|
|
820
|
+
}
|
|
821
|
+
if (s.type === "ClassDeclaration" && ahead.classes.has(s)) {
|
|
822
|
+
return this.classDeclaration(s, "assign");
|
|
823
|
+
}
|
|
824
|
+
return this.statement(s);
|
|
825
|
+
})
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
/** A file's statements outside a bundle. A function used above its
|
|
829
|
+
* declaration is hoisted whole, as in a module: every top-level name is
|
|
830
|
+
* declared first, then the functions are defined, then the rest runs. */
|
|
831
|
+
topLevel(statements) {
|
|
832
|
+
if (!this.referencedAhead(statements).names.length) return statements.flatMap((s) => this.statement(s));
|
|
833
|
+
const locals = [];
|
|
834
|
+
const hoisted = [];
|
|
835
|
+
const body = [];
|
|
836
|
+
for (const statement of statements) {
|
|
837
|
+
if (statement.type === "VariableDeclaration") {
|
|
838
|
+
for (const pattern of statement.names.flatMap(identifierPatterns)) locals.push(this.name(pattern.name));
|
|
839
|
+
body.push(...this.variableDeclaration(statement, "assign"));
|
|
840
|
+
} else if (statement.type === "FunctionDeclaration") {
|
|
841
|
+
locals.push(this.name(statement.name.name));
|
|
842
|
+
hoisted.push(this.from(this.functionStatement(
|
|
843
|
+
this.reference(statement.name),
|
|
844
|
+
statement,
|
|
845
|
+
this.functionBody(statement.func),
|
|
846
|
+
false
|
|
847
|
+
), statement));
|
|
848
|
+
} else if (statement.type === "ClassDeclaration") {
|
|
849
|
+
locals.push(this.name(statement.name.name));
|
|
850
|
+
body.push(...this.classDeclaration(statement, "assign"));
|
|
851
|
+
} else {
|
|
852
|
+
body.push(...this.statement(statement));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const declarations = [];
|
|
856
|
+
for (let i = 0; i < locals.length; i += 100) declarations.push(local(locals.slice(i, i + 100), []));
|
|
857
|
+
return [...declarations, ...hoisted, ...body];
|
|
858
|
+
}
|
|
859
|
+
/** Where each emitted statement came from. A bundle is one Luau file, so
|
|
860
|
+
* a line in it says nothing about which of the project's files wrote it;
|
|
861
|
+
* this is what lets a traceback be read back into the source. Statements
|
|
862
|
+
* are the unit because lines are: an expression shares its statement's.
|
|
863
|
+
*
|
|
864
|
+
* Only statements built without a span of their own are stamped — the few
|
|
865
|
+
* that carry one already kept a more exact position. */
|
|
866
|
+
statement(node) {
|
|
867
|
+
const emitted = this.statementOf(node);
|
|
868
|
+
for (const s of emitted) {
|
|
869
|
+
if (!s.line.start) {
|
|
870
|
+
s.line = { ...node.line };
|
|
871
|
+
s.column = { ...node.column };
|
|
872
|
+
}
|
|
873
|
+
if (!this.origins.has(s)) this.origins.set(s, node.line.start);
|
|
874
|
+
}
|
|
875
|
+
return emitted;
|
|
876
|
+
}
|
|
877
|
+
/** Stamps a statement built outside `statement` — a hoisted function, an
|
|
878
|
+
* assignment the module wrapper makes — with where it was written. */
|
|
879
|
+
from(statement, node) {
|
|
880
|
+
if (!statement.line.start) {
|
|
881
|
+
statement.line = { ...node.line };
|
|
882
|
+
statement.column = { ...node.column };
|
|
883
|
+
}
|
|
884
|
+
if (!this.origins.has(statement)) this.origins.set(statement, node.line.start);
|
|
885
|
+
return statement;
|
|
886
|
+
}
|
|
887
|
+
statementOf(node) {
|
|
888
|
+
switch (node.type) {
|
|
889
|
+
// Types, and what exists only for them.
|
|
890
|
+
case "TypeAliasStatement":
|
|
891
|
+
case "ExportTypeAliasStatement":
|
|
892
|
+
case "DeclareStatement":
|
|
893
|
+
case "DeclareClassStatement":
|
|
894
|
+
case "ErrorStatement":
|
|
895
|
+
return [];
|
|
896
|
+
case "VariableDeclaration":
|
|
897
|
+
return this.variableDeclaration(node, "declare");
|
|
898
|
+
case "FunctionDeclaration":
|
|
899
|
+
return [{ ...localFunction(this.name(node.name.name), this.functionBody(node.func)), attributes: node.attributes }];
|
|
900
|
+
case "FunctionDeclarationStatement":
|
|
901
|
+
return [this.functionDeclarationStatement(node)];
|
|
902
|
+
case "ClassDeclaration":
|
|
903
|
+
return this.classDeclaration(node, "declare");
|
|
904
|
+
case "AssignmentStatement":
|
|
905
|
+
return this.assignment(node);
|
|
906
|
+
case "CompoundAssignmentStatement":
|
|
907
|
+
if (this.lua51) return this.compoundAssignment51(node);
|
|
908
|
+
return [{
|
|
909
|
+
type: "CompoundAssignmentStatement",
|
|
910
|
+
operator: node.operator,
|
|
911
|
+
target: this.expression(node.target),
|
|
912
|
+
value: this.expression(node.value),
|
|
913
|
+
...spanOf(node)
|
|
914
|
+
}];
|
|
915
|
+
case "CallStatement": {
|
|
916
|
+
const chain = optionalChain(node.expression);
|
|
917
|
+
if (chain) return [this.optionalCallStatement(chain)];
|
|
918
|
+
const expression = this.expression(node.expression);
|
|
919
|
+
if (expression.type !== "CallExpression" && expression.type !== "MethodCallExpression") {
|
|
920
|
+
this.report(node, "A statement must be a call");
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
return [callStatement(expression)];
|
|
924
|
+
}
|
|
925
|
+
// `value` on a line of its own: written to ask the editor about a
|
|
926
|
+
// name, and worth nothing once the file is compiled. It cannot be
|
|
927
|
+
// kept — Lua has no expression statement — and it cannot run
|
|
928
|
+
// anything, since a call is a `CallStatement` instead, so dropping
|
|
929
|
+
// it changes nothing but the noise.
|
|
930
|
+
case "ExpressionStatement":
|
|
931
|
+
return [];
|
|
932
|
+
case "DoStatement":
|
|
933
|
+
return [{ type: "DoStatement", body: this.block(node.body), ...spanOf(node) }];
|
|
934
|
+
case "WhileStatement":
|
|
935
|
+
return [{ type: "WhileStatement", condition: this.expression(node.condition), body: this.loopBody51(this.block(node.body)), ...spanOf(node) }];
|
|
936
|
+
case "RepeatStatement": {
|
|
937
|
+
const body = this.block(node.body);
|
|
938
|
+
const condition = this.expression(node.condition);
|
|
939
|
+
if (this.lua51 && hasOwnJump(body.statements, "ContinueStatement")) {
|
|
940
|
+
const hidden = declaredNames(body.statements).filter((name) => mentions(condition, name));
|
|
941
|
+
if (hidden.length) {
|
|
942
|
+
this.report(node, `'continue' in a 'repeat' whose 'until' reads ${hidden.map((n) => `'${n}'`).join(", ")} cannot be compiled for Lua 5.1, which has no 'continue' to lower it to. Move what the condition reads out of the loop body, or use a 'while' loop.`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return [{ type: "RepeatStatement", body: this.loopBody51(body), condition, ...spanOf(node) }];
|
|
946
|
+
}
|
|
947
|
+
case "IfStatement":
|
|
948
|
+
return [{
|
|
949
|
+
type: "IfStatement",
|
|
950
|
+
clauses: node.clauses.map((c) => ({
|
|
951
|
+
type: "IfClause",
|
|
952
|
+
condition: this.expression(c.condition),
|
|
953
|
+
body: this.block(c.body),
|
|
954
|
+
...spanOf(c)
|
|
955
|
+
})),
|
|
956
|
+
alternate: node.alternate && this.block(node.alternate),
|
|
957
|
+
...spanOf(node)
|
|
958
|
+
}];
|
|
959
|
+
case "NumericForStatement":
|
|
960
|
+
return [{
|
|
961
|
+
type: "NumericForStatement",
|
|
962
|
+
variable: { type: "TypedIdentifier", name: this.name(node.variable.name), ...spanOf(node.variable) },
|
|
963
|
+
start: this.expression(node.start),
|
|
964
|
+
end: this.expression(node.end),
|
|
965
|
+
step: node.step && this.expression(node.step),
|
|
966
|
+
body: this.loopBody51(this.block(node.body)),
|
|
967
|
+
...spanOf(node)
|
|
968
|
+
}];
|
|
969
|
+
case "GenericForStatement": {
|
|
970
|
+
const prelude = [];
|
|
971
|
+
const variables = node.variables.map((target) => {
|
|
972
|
+
if (target.type === "IdentifierPattern") return this.name(target.name);
|
|
973
|
+
const temp = this.names.fresh("item");
|
|
974
|
+
prelude.push(...this.destructure(target, identifier(temp), "declare"));
|
|
975
|
+
return temp;
|
|
976
|
+
});
|
|
977
|
+
if (variables.length === 1 && node.iterators.length === 1 && this.iteratesTable(node.iterators[0])) {
|
|
978
|
+
variables.unshift(this.names.fresh("_"));
|
|
979
|
+
}
|
|
980
|
+
return [genericFor(
|
|
981
|
+
variables,
|
|
982
|
+
node.iterators.map((e) => this.expression(e)),
|
|
983
|
+
this.loopBody51(this.block(node.body, prelude)).statements
|
|
984
|
+
)];
|
|
985
|
+
}
|
|
986
|
+
case "ReturnStatement":
|
|
987
|
+
return [returns(this.values(node.arguments))];
|
|
988
|
+
case "BreakStatement":
|
|
989
|
+
return [{ type: "BreakStatement", ...spanOf(node) }];
|
|
990
|
+
case "ContinueStatement":
|
|
991
|
+
return [{ type: "ContinueStatement", ...spanOf(node) }];
|
|
992
|
+
case "ImportStatement":
|
|
993
|
+
if (node.isTypeOnly) return [];
|
|
994
|
+
this.report(node, this.options.module ? "Imports and exports belong at the top level of a module" : "Imports and exports need a bundle: build the project with @tilua/compiler");
|
|
995
|
+
return [];
|
|
996
|
+
case "ExportStatement":
|
|
997
|
+
case "ExportDefaultStatement":
|
|
998
|
+
case "ExportNamedStatement":
|
|
999
|
+
case "ExportAllStatement":
|
|
1000
|
+
this.report(node, this.options.module ? "Imports and exports belong at the top level of a module" : "Imports and exports need a bundle: build the project with @tilua/compiler");
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
functionDeclarationStatement(node) {
|
|
1005
|
+
const statement = this.functionStatement(this.reference(node.target.base), node, this.functionBody(node.func), node.isMethod, node.target.path.map((p) => p.name));
|
|
1006
|
+
if (!node.target.method) return statement;
|
|
1007
|
+
return { ...statement, target: { ...statement.target, method: identifier(node.target.method.name) } };
|
|
1008
|
+
}
|
|
1009
|
+
/** `function a.b.c()`, for a target written as an expression. */
|
|
1010
|
+
// ============================================================
|
|
1011
|
+
// `class ... end`
|
|
1012
|
+
// ------------------------------------------------------------
|
|
1013
|
+
// The shape is Lua's own, and the memory model the language
|
|
1014
|
+
// promises: one table per class holding its methods and its
|
|
1015
|
+
// statics, and one table per instance whose metatable points at
|
|
1016
|
+
// it. An instance reaches its class directly — nothing is copied
|
|
1017
|
+
// per instance, and there is no prototype chain of its own.
|
|
1018
|
+
//
|
|
1019
|
+
// local Dog = __class(Animal)
|
|
1020
|
+
// function Dog.__init(this, name)
|
|
1021
|
+
// Animal.__init(this, name) -- super(name)
|
|
1022
|
+
// this.breed = "corgi" -- a field initializer
|
|
1023
|
+
// end
|
|
1024
|
+
// function Dog.new(...)
|
|
1025
|
+
// local this = setmetatable({}, Dog)
|
|
1026
|
+
// Dog.__init(this, ...)
|
|
1027
|
+
// return this
|
|
1028
|
+
// end
|
|
1029
|
+
// function Dog.speak(this) ... end
|
|
1030
|
+
//
|
|
1031
|
+
// `new Dog(x)` is `Dog.new(x)`, `dog:speak()` passes the instance
|
|
1032
|
+
// as `this`, and `super.speak()` is `Animal.speak(this)` — the
|
|
1033
|
+
// base's own function, run on this instance.
|
|
1034
|
+
// ============================================================
|
|
1035
|
+
/** The locals the file's classes are built with, named once. */
|
|
1036
|
+
classHelpers;
|
|
1037
|
+
classRuntime() {
|
|
1038
|
+
return this.classHelpers ??= {
|
|
1039
|
+
build: this.names.fresh("tilua_class"),
|
|
1040
|
+
accessors: this.names.fresh("tilua_accessors")
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/** The helpers, as Luau — emitted only in a file that declares a class.
|
|
1044
|
+
* `__getters`/`__setters` chain to the base's, so a derived class sees
|
|
1045
|
+
* the accessors it inherits; `__index` only becomes a function for a
|
|
1046
|
+
* class that has some, which leaves every other class on the plain
|
|
1047
|
+
* `__index = class` fast path.
|
|
1048
|
+
*
|
|
1049
|
+
* `ClassObject` and `ParentClass` are the links the memory model
|
|
1050
|
+
* promises: an instance reads `ClassObject` through its metatable and
|
|
1051
|
+
* lands on its own class, and a class reads `ParentClass` and lands on
|
|
1052
|
+
* the one it extends. Both live on the class table, so an instance
|
|
1053
|
+
* carries neither. */
|
|
1054
|
+
classRuntimeStatements() {
|
|
1055
|
+
if (!this.classHelpers) return [];
|
|
1056
|
+
const { build, accessors } = this.classHelpers;
|
|
1057
|
+
return (0, import_luau_parser.parse)(`
|
|
1058
|
+
local function ${build}(base)
|
|
1059
|
+
local class = { __getters = {}, __setters = {} }
|
|
1060
|
+
class.__index = class
|
|
1061
|
+
class.ClassObject = class
|
|
1062
|
+
class.ParentClass = base
|
|
1063
|
+
if base ~= nil then
|
|
1064
|
+
setmetatable(class, { __index = base })
|
|
1065
|
+
setmetatable(class.__getters, { __index = base.__getters })
|
|
1066
|
+
setmetatable(class.__setters, { __index = base.__setters })
|
|
1067
|
+
end
|
|
1068
|
+
return class
|
|
1069
|
+
end
|
|
1070
|
+
local function ${accessors}(class)
|
|
1071
|
+
if not class.__dynamic and next(class.__getters) == nil and next(class.__setters) == nil then
|
|
1072
|
+
return
|
|
1073
|
+
end
|
|
1074
|
+
class.__dynamic = true
|
|
1075
|
+
class.__index = function(this, key)
|
|
1076
|
+
local getter = class.__getters[key]
|
|
1077
|
+
if getter ~= nil then
|
|
1078
|
+
return getter(this)
|
|
1079
|
+
end
|
|
1080
|
+
return class[key]
|
|
1081
|
+
end
|
|
1082
|
+
class.__newindex = function(this, key, value)
|
|
1083
|
+
local setter = class.__setters[key]
|
|
1084
|
+
if setter ~= nil then
|
|
1085
|
+
setter(this, value)
|
|
1086
|
+
return
|
|
1087
|
+
end
|
|
1088
|
+
rawset(this, key, value)
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
`).body.statements;
|
|
1092
|
+
}
|
|
1093
|
+
/** The class being lowered, so `super` knows what to name. */
|
|
1094
|
+
classContext;
|
|
1095
|
+
/** A class written where a value goes. Building it takes statements, and
|
|
1096
|
+
* an expression has no room for them, so they go in a function that runs
|
|
1097
|
+
* where the class is written and hands the class table back. */
|
|
1098
|
+
classExpression(node) {
|
|
1099
|
+
const local2 = this.names.fresh(node.name ? this.name(node.name.name) : "class");
|
|
1100
|
+
const statements = this.classStatements(node, identifier(local2), local2);
|
|
1101
|
+
return call(parenthesized(functionExpression(
|
|
1102
|
+
functionBody([], [...statements, returns([identifier(local2)])])
|
|
1103
|
+
)), []);
|
|
1104
|
+
}
|
|
1105
|
+
classDeclaration(node, mode) {
|
|
1106
|
+
const name = this.name(node.name.name);
|
|
1107
|
+
return mode === "declare" ? this.classStatements(node, identifier(name), name) : this.classStatements(node, this.reference(node.name));
|
|
1108
|
+
}
|
|
1109
|
+
/** `declare` names the local the class table goes in; without it the
|
|
1110
|
+
* table is assigned to `self`, which a module has already declared. */
|
|
1111
|
+
classStatements(node, self, declare) {
|
|
1112
|
+
const { build, accessors } = this.classRuntime();
|
|
1113
|
+
const create = call(identifier(build), [node.superclass ? this.reference(node.superclass) : nil()]);
|
|
1114
|
+
const out2 = [declare !== void 0 ? local([declare], [create]) : assign([self], [create])];
|
|
1115
|
+
const previous = this.classContext;
|
|
1116
|
+
this.classContext = node;
|
|
1117
|
+
try {
|
|
1118
|
+
for (const member2 of node.members) {
|
|
1119
|
+
switch (member2.type) {
|
|
1120
|
+
case "ClassField":
|
|
1121
|
+
if (member2.isStatic) {
|
|
1122
|
+
out2.push(assign(
|
|
1123
|
+
[member(self, member2.name.name)],
|
|
1124
|
+
[member2.init ? this.expression(member2.init) : nil()]
|
|
1125
|
+
));
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
case "ClassMethod":
|
|
1129
|
+
out2.push(this.functionStatement(
|
|
1130
|
+
member(self, member2.name.name),
|
|
1131
|
+
member2,
|
|
1132
|
+
this.classFunctionBody(member2.func),
|
|
1133
|
+
false
|
|
1134
|
+
));
|
|
1135
|
+
break;
|
|
1136
|
+
case "ClassAccessor":
|
|
1137
|
+
out2.push(assign(
|
|
1138
|
+
[index(
|
|
1139
|
+
member(self, member2.kind === "get" ? "__getters" : "__setters"),
|
|
1140
|
+
string(member2.name.name)
|
|
1141
|
+
)],
|
|
1142
|
+
[functionExpression(this.classFunctionBody(member2.func))]
|
|
1143
|
+
));
|
|
1144
|
+
break;
|
|
1145
|
+
case "ClassConstructor":
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
out2.push(this.classInit(node, self));
|
|
1150
|
+
out2.push(this.classNew(node, self));
|
|
1151
|
+
out2.push(callStatement(call(identifier(accessors), [self])));
|
|
1152
|
+
} finally {
|
|
1153
|
+
this.classContext = previous;
|
|
1154
|
+
}
|
|
1155
|
+
return out2;
|
|
1156
|
+
}
|
|
1157
|
+
/** `Class.__init(this, ...)` — what every instance of the class runs.
|
|
1158
|
+
* Kept apart from `new` so `super(...)` can run the base's on the
|
|
1159
|
+
* instance already being built rather than making a second one. */
|
|
1160
|
+
classInit(node, self) {
|
|
1161
|
+
const instance = this.thisName();
|
|
1162
|
+
const initializers = node.members.filter((m) => m.type === "ClassField" && !m.isStatic && m.init !== void 0).map((field2) => assign(
|
|
1163
|
+
[member(identifier(instance), field2.name.name)],
|
|
1164
|
+
[this.expression(field2.init)]
|
|
1165
|
+
));
|
|
1166
|
+
const constructor = node.members.find((m) => m.type === "ClassConstructor");
|
|
1167
|
+
if (!constructor) {
|
|
1168
|
+
const body2 = functionBody([instance], [
|
|
1169
|
+
...node.superclass ? [callStatement(call(
|
|
1170
|
+
member(this.reference(node.superclass), "__init"),
|
|
1171
|
+
[identifier(instance), vararg()]
|
|
1172
|
+
))] : [],
|
|
1173
|
+
...initializers
|
|
1174
|
+
], true);
|
|
1175
|
+
return this.functionStatement(member(self, "__init"), node, body2, false);
|
|
1176
|
+
}
|
|
1177
|
+
const body = this.classFunctionBody(constructor.func);
|
|
1178
|
+
const statements = body.body.statements;
|
|
1179
|
+
const afterSuper = node.superclass ? statements.findIndex(isSuperInit) + 1 : 0;
|
|
1180
|
+
statements.splice(Math.max(afterSuper, 0), 0, ...initializers);
|
|
1181
|
+
return this.functionStatement(member(self, "__init"), constructor, body, false);
|
|
1182
|
+
}
|
|
1183
|
+
/** `Class.new(...)`: the instance, its metatable, its constructor. */
|
|
1184
|
+
classNew(node, self) {
|
|
1185
|
+
const instance = this.thisName();
|
|
1186
|
+
const body = functionBody([], [
|
|
1187
|
+
local([instance], [call(this.builtin("setmetatable"), [table([]), self])]),
|
|
1188
|
+
callStatement(call(member(self, "__init"), [identifier(instance), vararg()])),
|
|
1189
|
+
returns([identifier(instance)])
|
|
1190
|
+
], true);
|
|
1191
|
+
return this.functionStatement(member(self, "new"), node, body, false);
|
|
1192
|
+
}
|
|
1193
|
+
/** What `this` is called in emitted code. */
|
|
1194
|
+
thisName() {
|
|
1195
|
+
return this.name("this");
|
|
1196
|
+
}
|
|
1197
|
+
/** A class method keeps its receiver as a written parameter: `:` supplies
|
|
1198
|
+
* it at the call site, but the declaration spells it out so `super` and
|
|
1199
|
+
* `new` can pass it by hand. */
|
|
1200
|
+
classFunctionBody(func) {
|
|
1201
|
+
return this.functionBody(func.isMethod ? { ...func, isMethod: false } : func);
|
|
1202
|
+
}
|
|
1203
|
+
/** `super(...)` / `super.m(...)` — the base class, by the name this file
|
|
1204
|
+
* knows it under. */
|
|
1205
|
+
superClassReference(node) {
|
|
1206
|
+
const superclass = this.classContext?.superclass;
|
|
1207
|
+
if (!superclass) {
|
|
1208
|
+
this.report(node, "'super' is only available inside a class that extends another");
|
|
1209
|
+
return nil();
|
|
1210
|
+
}
|
|
1211
|
+
return this.reference(superclass);
|
|
1212
|
+
}
|
|
1213
|
+
functionStatement(target, node, func, isMethod, path = []) {
|
|
1214
|
+
const chain = memberChain(target);
|
|
1215
|
+
if (!chain) this.report(node, "This function name cannot be written in Luau");
|
|
1216
|
+
const [root, ...prefix] = chain ?? ["_"];
|
|
1217
|
+
return {
|
|
1218
|
+
type: "FunctionDeclarationStatement",
|
|
1219
|
+
target: {
|
|
1220
|
+
type: "FunctionName",
|
|
1221
|
+
base: identifier(root),
|
|
1222
|
+
path: [...prefix, ...path].map(identifier),
|
|
1223
|
+
...spanOf(node)
|
|
1224
|
+
},
|
|
1225
|
+
isMethod,
|
|
1226
|
+
func,
|
|
1227
|
+
attributes: node.attributes,
|
|
1228
|
+
...spanOf(node)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/** `const a, { b } = x, y`. In "assign" mode — a module's top level, where
|
|
1232
|
+
* every name is declared up front — nothing is declared, only assigned. */
|
|
1233
|
+
variableDeclaration(node, mode) {
|
|
1234
|
+
if (mode === "assign" && !node.init.length) {
|
|
1235
|
+
const exported = node.names.flatMap(identifierPatterns).filter((p) => this.isRewritten(p));
|
|
1236
|
+
return exported.length ? [assign(exported.map((p) => this.reference(p)), exported.map(() => nil()))] : [];
|
|
1237
|
+
}
|
|
1238
|
+
const init = this.values(node.init);
|
|
1239
|
+
if (node.names.every((n) => n.type === "IdentifierPattern")) {
|
|
1240
|
+
const names2 = node.names;
|
|
1241
|
+
return mode === "declare" ? [local(names2.map((n) => this.name(n.name)), init)] : [assign(names2.map((n) => this.reference(n)), init)];
|
|
1242
|
+
}
|
|
1243
|
+
const only = node.names.length === 1 && node.init.length === 1 ? node.names[0] : void 0;
|
|
1244
|
+
if (only && only.type !== "IdentifierPattern") {
|
|
1245
|
+
if (init[0].type === "Identifier") return this.destructure(only, init[0], mode);
|
|
1246
|
+
const temp = this.names.fresh("ref");
|
|
1247
|
+
return [local([temp], init), ...this.destructure(only, identifier(temp), mode)];
|
|
1248
|
+
}
|
|
1249
|
+
const after = [];
|
|
1250
|
+
const names = [];
|
|
1251
|
+
const targets = [];
|
|
1252
|
+
for (const target of node.names) {
|
|
1253
|
+
if (target.type === "IdentifierPattern") {
|
|
1254
|
+
names.push(this.name(target.name));
|
|
1255
|
+
targets.push(this.reference(target));
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
const temp = this.names.fresh("ref");
|
|
1259
|
+
names.push(temp);
|
|
1260
|
+
targets.push(identifier(temp));
|
|
1261
|
+
after.push(...this.destructure(target, identifier(temp), mode));
|
|
1262
|
+
}
|
|
1263
|
+
if (mode === "declare") return [local(names, init), ...after];
|
|
1264
|
+
const temps = node.names.flatMap((t, i) => t.type === "IdentifierPattern" ? [] : [names[i]]);
|
|
1265
|
+
return [...temps.length ? [local(temps, [])] : [], assign(targets, init), ...after];
|
|
1266
|
+
}
|
|
1267
|
+
assignment(node) {
|
|
1268
|
+
if (node.targets.every((t) => t.type !== "ObjectPattern" && t.type !== "ArrayPattern")) {
|
|
1269
|
+
return [assign(node.targets.map((t) => this.expression(t)), this.values(node.values))];
|
|
1270
|
+
}
|
|
1271
|
+
const values = this.values(node.values);
|
|
1272
|
+
const only = node.targets.length === 1 && values.length === 1 ? node.targets[0] : void 0;
|
|
1273
|
+
if (only && (only.type === "ObjectPattern" || only.type === "ArrayPattern") && values[0].type === "Identifier") {
|
|
1274
|
+
const statements = this.destructure(only, values[0], "assign");
|
|
1275
|
+
return statements.some((s) => s.type === "LocalStatement") ? [doBlock(statements)] : statements;
|
|
1276
|
+
}
|
|
1277
|
+
const temps = node.targets.map(() => this.names.fresh("ref"));
|
|
1278
|
+
const body = [local(temps, values)];
|
|
1279
|
+
node.targets.forEach((target, i) => {
|
|
1280
|
+
const value = identifier(temps[i]);
|
|
1281
|
+
if (target.type === "ObjectPattern" || target.type === "ArrayPattern") {
|
|
1282
|
+
body.push(...this.destructure(target, value, "assign"));
|
|
1283
|
+
} else {
|
|
1284
|
+
body.push(assign([this.expression(target)], [value]));
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
return [doBlock(body)];
|
|
1288
|
+
}
|
|
1289
|
+
// --------------------------------------------------------
|
|
1290
|
+
// Destructuring
|
|
1291
|
+
// --------------------------------------------------------
|
|
1292
|
+
/** Bind (or assign) every name in `pattern` from `source`, which must be a
|
|
1293
|
+
* plain name so that reading it repeatedly has no side effects.
|
|
1294
|
+
*
|
|
1295
|
+
* The reads share one statement, in the pattern's order —
|
|
1296
|
+
* `local a, b = source.a, source.b` — with a temporary standing in for a
|
|
1297
|
+
* nested pattern. Defaults, nested patterns and rest follow it. */
|
|
1298
|
+
destructure(pattern, source, mode) {
|
|
1299
|
+
const keys = [];
|
|
1300
|
+
const reads = [];
|
|
1301
|
+
const after = [];
|
|
1302
|
+
const bind = (target, value, fallback) => {
|
|
1303
|
+
if (target.type === "IdentifierPattern") {
|
|
1304
|
+
const name2 = this.name(target.name);
|
|
1305
|
+
const reference = mode === "declare" ? identifier(name2) : this.reference(target);
|
|
1306
|
+
reads.push({ name: name2, target: reference, value, temp: false });
|
|
1307
|
+
if (fallback) after.push(this.defaultValue(reference, fallback));
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const name = this.names.fresh("ref");
|
|
1311
|
+
reads.push({ name, target: identifier(name), value, temp: true });
|
|
1312
|
+
if (fallback) after.push(this.defaultValue(identifier(name), fallback));
|
|
1313
|
+
after.push(...this.destructure(target, identifier(name), mode));
|
|
1314
|
+
};
|
|
1315
|
+
if (pattern.type === "ObjectPattern") {
|
|
1316
|
+
const taken = [];
|
|
1317
|
+
for (const property of pattern.properties) {
|
|
1318
|
+
let key;
|
|
1319
|
+
if (!property.computed && (property.key.type === "Identifier" || property.key.type === "StringLiteral")) {
|
|
1320
|
+
const name = property.key.type === "Identifier" ? property.key.name : property.key.value;
|
|
1321
|
+
key = string(name);
|
|
1322
|
+
bind(property.value, member(source, name), property.default);
|
|
1323
|
+
} else {
|
|
1324
|
+
const name = this.names.fresh("key");
|
|
1325
|
+
keys.push({ name, value: this.expression(property.key) });
|
|
1326
|
+
key = identifier(name);
|
|
1327
|
+
bind(property.value, index(source, key), property.default);
|
|
1328
|
+
}
|
|
1329
|
+
taken.push(key);
|
|
1330
|
+
}
|
|
1331
|
+
if (pattern.rest) {
|
|
1332
|
+
const rest = this.restTarget(pattern.rest, table([]), mode, after);
|
|
1333
|
+
const key = this.names.fresh("key");
|
|
1334
|
+
const value = this.names.fresh("value");
|
|
1335
|
+
const kept = taken.reduce((condition, k) => {
|
|
1336
|
+
const differs = binary("~=", identifier(key), k);
|
|
1337
|
+
return condition ? binary("and", condition, differs) : differs;
|
|
1338
|
+
}, void 0);
|
|
1339
|
+
const copy = assign([index(rest.target, identifier(key))], [identifier(value)]);
|
|
1340
|
+
after.push(genericFor([key, value], [call(this.builtin("pairs"), [source])], kept ? [ifThen(kept, [copy])] : [copy]));
|
|
1341
|
+
after.push(...rest.then);
|
|
1342
|
+
}
|
|
1343
|
+
} else {
|
|
1344
|
+
pattern.elements.forEach((element, i) => {
|
|
1345
|
+
if (element) bind(element.value, index(source, number(i + 1)), element.default);
|
|
1346
|
+
});
|
|
1347
|
+
if (pattern.rest) {
|
|
1348
|
+
const elements = call(member(this.builtin("table"), "move"), [
|
|
1349
|
+
source,
|
|
1350
|
+
number(pattern.elements.length + 1),
|
|
1351
|
+
unary("#", source),
|
|
1352
|
+
number(1),
|
|
1353
|
+
table([])
|
|
1354
|
+
]);
|
|
1355
|
+
const rest = this.restTarget(pattern.rest, elements, mode, after);
|
|
1356
|
+
after.push(...rest.then);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const out2 = [];
|
|
1360
|
+
if (keys.length) out2.push(local(keys.map((k) => k.name), keys.map((k) => k.value)));
|
|
1361
|
+
if (mode === "declare") {
|
|
1362
|
+
if (reads.length) out2.push(local(reads.map((r) => r.name), reads.map((r) => r.value)));
|
|
1363
|
+
} else {
|
|
1364
|
+
const temps = reads.filter((r) => r.temp);
|
|
1365
|
+
const targets = reads.filter((r) => !r.temp);
|
|
1366
|
+
if (temps.length) out2.push(local(temps.map((r) => r.name), temps.map((r) => r.value)));
|
|
1367
|
+
if (targets.length) out2.push(assign(targets.map((r) => r.target), targets.map((r) => r.value)));
|
|
1368
|
+
}
|
|
1369
|
+
return [...out2, ...after];
|
|
1370
|
+
}
|
|
1371
|
+
/** Where a rest element's new table goes: straight into its name, or into
|
|
1372
|
+
* a temporary that a nested pattern then destructures (`then`). The
|
|
1373
|
+
* statement creating it is pushed onto `after`. */
|
|
1374
|
+
restTarget(target, initial, mode, after) {
|
|
1375
|
+
if (target.type === "IdentifierPattern") {
|
|
1376
|
+
if (mode === "declare") {
|
|
1377
|
+
const name2 = this.name(target.name);
|
|
1378
|
+
after.push(local([name2], [initial]));
|
|
1379
|
+
return { target: identifier(name2), then: [] };
|
|
1380
|
+
}
|
|
1381
|
+
const reference = this.reference(target);
|
|
1382
|
+
after.push(assign([reference], [initial]));
|
|
1383
|
+
return { target: reference, then: [] };
|
|
1384
|
+
}
|
|
1385
|
+
const name = this.names.fresh("rest");
|
|
1386
|
+
after.push(local([name], [initial]));
|
|
1387
|
+
return { target: identifier(name), then: this.destructure(target, identifier(name), mode) };
|
|
1388
|
+
}
|
|
1389
|
+
/** `if target == nil then target = fallback end` */
|
|
1390
|
+
defaultValue(target, fallback) {
|
|
1391
|
+
return ifThen(binary("==", target, nil()), [assign([target], [this.expression(fallback)])]);
|
|
1392
|
+
}
|
|
1393
|
+
// --------------------------------------------------------
|
|
1394
|
+
// Functions
|
|
1395
|
+
// --------------------------------------------------------
|
|
1396
|
+
functionBody(func) {
|
|
1397
|
+
const params = func.isMethod ? func.params.slice(1) : func.params;
|
|
1398
|
+
const prelude = [];
|
|
1399
|
+
const names = [];
|
|
1400
|
+
for (const p of params) {
|
|
1401
|
+
if (p.rest) {
|
|
1402
|
+
prelude.push(local([this.name(p.name)], [table([{ type: "TableFieldPositional", value: vararg() }])]));
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const name = p.pattern ? this.names.fresh("arg") : this.name(p.name);
|
|
1406
|
+
if (p.default) prelude.push(this.defaultValue(identifier(name), p.default));
|
|
1407
|
+
if (p.pattern) prelude.push(...this.destructure(p.pattern, identifier(name), "declare"));
|
|
1408
|
+
names.push(name);
|
|
1409
|
+
}
|
|
1410
|
+
return { ...functionBody(names, [], func.hasVarargs), body: this.block(func.body, prelude) };
|
|
1411
|
+
}
|
|
1412
|
+
// --------------------------------------------------------
|
|
1413
|
+
// Expressions
|
|
1414
|
+
// --------------------------------------------------------
|
|
1415
|
+
expression(node) {
|
|
1416
|
+
if (node.type === "CallExpression") {
|
|
1417
|
+
const callee = node.callee;
|
|
1418
|
+
const through = callee.type === "SuperExpression" ? "__init" : callee.type === "MemberExpression" && callee.object.type === "SuperExpression" ? callee.property.name : void 0;
|
|
1419
|
+
if (through !== void 0) {
|
|
1420
|
+
return call(member(this.superClassReference(node), through), [
|
|
1421
|
+
identifier(this.thisName()),
|
|
1422
|
+
...this.values(node.arguments)
|
|
1423
|
+
]);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
switch (node.type) {
|
|
1427
|
+
case "Identifier":
|
|
1428
|
+
return this.reference(node);
|
|
1429
|
+
case "NilLiteral":
|
|
1430
|
+
return nil();
|
|
1431
|
+
case "BooleanLiteral":
|
|
1432
|
+
return boolean(node.value);
|
|
1433
|
+
case "NumberLiteral":
|
|
1434
|
+
return number(node.value, this.numberRaw(node));
|
|
1435
|
+
case "StringLiteral":
|
|
1436
|
+
return string(node.value);
|
|
1437
|
+
case "VarargExpression":
|
|
1438
|
+
return vararg();
|
|
1439
|
+
// Only a recovering parse makes one, and a syntax error stops the build.
|
|
1440
|
+
case "ErrorExpression":
|
|
1441
|
+
this.report(node, "Syntax error");
|
|
1442
|
+
return nil();
|
|
1443
|
+
case "InterpolatedStringExpression":
|
|
1444
|
+
return this.interpolatedString(node);
|
|
1445
|
+
case "FunctionExpression":
|
|
1446
|
+
return functionExpression(this.functionBody(node.func));
|
|
1447
|
+
case "TableExpression":
|
|
1448
|
+
return this.tableExpression(node);
|
|
1449
|
+
case "ArrayExpression":
|
|
1450
|
+
return this.arrayExpression(node);
|
|
1451
|
+
case "BinaryExpression": {
|
|
1452
|
+
const left = this.expression(node.left);
|
|
1453
|
+
const right = this.expression(node.right);
|
|
1454
|
+
if (this.lua51 && node.operator === "//") return this.floorDivide(left, right);
|
|
1455
|
+
return binary(node.operator, left, right);
|
|
1456
|
+
}
|
|
1457
|
+
case "UnaryExpression":
|
|
1458
|
+
return unary(node.operator, this.expression(node.argument));
|
|
1459
|
+
case "NewExpression":
|
|
1460
|
+
return call(
|
|
1461
|
+
member(this.expression(node.callee), "new"),
|
|
1462
|
+
this.values(node.arguments)
|
|
1463
|
+
);
|
|
1464
|
+
case "SuperExpression":
|
|
1465
|
+
return this.superClassReference(node);
|
|
1466
|
+
case "SpreadElement":
|
|
1467
|
+
this.report(node, "'...' can only spread into a call's arguments or an array");
|
|
1468
|
+
return nil();
|
|
1469
|
+
case "ClassExpression":
|
|
1470
|
+
return this.classExpression(node);
|
|
1471
|
+
case "MemberExpression":
|
|
1472
|
+
case "IndexExpression":
|
|
1473
|
+
case "CallExpression":
|
|
1474
|
+
case "MethodCallExpression": {
|
|
1475
|
+
const chain = optionalChain(node);
|
|
1476
|
+
if (chain) return this.optionalChainExpression(chain);
|
|
1477
|
+
return this.link(node, this.expression(linkObject(node)));
|
|
1478
|
+
}
|
|
1479
|
+
case "ParenthesizedExpression":
|
|
1480
|
+
return parenthesized(this.expression(node.expression));
|
|
1481
|
+
// Types only: the value is the expression itself.
|
|
1482
|
+
case "TypeAssertionExpression":
|
|
1483
|
+
case "SatisfiesExpression":
|
|
1484
|
+
case "AsConstExpression":
|
|
1485
|
+
return this.expression(node.expression);
|
|
1486
|
+
case "IfElseExpression":
|
|
1487
|
+
if (this.lua51) return this.ifElse51(node);
|
|
1488
|
+
return {
|
|
1489
|
+
type: "IfElseExpression",
|
|
1490
|
+
clauses: node.clauses.map((c) => ({ condition: this.expression(c.condition), body: this.expression(c.body) })),
|
|
1491
|
+
alternate: this.expression(node.alternate),
|
|
1492
|
+
...spanOf(node)
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
/** A list of values — a call's arguments, a `return`'s, a declaration's,
|
|
1497
|
+
* an assignment's. `f(a, ...xs)` is Lua's own last-value expansion,
|
|
1498
|
+
* `f(a, table.unpack(xs))`; a spread anywhere else cannot be, since only
|
|
1499
|
+
* the last value of a list expands, so the whole list is built as an
|
|
1500
|
+
* array first and that is what expands. */
|
|
1501
|
+
values(list) {
|
|
1502
|
+
const spreads = list.filter((a) => a.type === "SpreadElement");
|
|
1503
|
+
if (!spreads.length) return list.map((a) => this.expression(a));
|
|
1504
|
+
const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
|
|
1505
|
+
const last = list[list.length - 1];
|
|
1506
|
+
if (spreads.length === 1 && last.type === "SpreadElement") {
|
|
1507
|
+
return [...list.slice(0, -1).map((a) => this.expression(a)), unpack(this.expression(last.argument))];
|
|
1508
|
+
}
|
|
1509
|
+
return [unpack(this.arrayExpression({
|
|
1510
|
+
type: "ArrayExpression",
|
|
1511
|
+
elements: [...list],
|
|
1512
|
+
...spanOf(list[0])
|
|
1513
|
+
}))];
|
|
1514
|
+
}
|
|
1515
|
+
/** One link of an access chain, read from `object`. */
|
|
1516
|
+
link(node, object) {
|
|
1517
|
+
switch (node.type) {
|
|
1518
|
+
case "MemberExpression":
|
|
1519
|
+
return member(object, node.property.name);
|
|
1520
|
+
case "IndexExpression":
|
|
1521
|
+
return index(object, this.expression(node.index));
|
|
1522
|
+
case "CallExpression":
|
|
1523
|
+
return call(object, this.values(node.arguments));
|
|
1524
|
+
case "MethodCallExpression": {
|
|
1525
|
+
const consoleCall = this.consoleCall(node);
|
|
1526
|
+
if (consoleCall) return consoleCall;
|
|
1527
|
+
const args2 = this.values(node.arguments);
|
|
1528
|
+
const lowered = this.loweredMethodCall(node);
|
|
1529
|
+
if (lowered) {
|
|
1530
|
+
return call(calleePath(lowered.callee), lowered.passReceiver ? [object, ...args2] : args2);
|
|
1531
|
+
}
|
|
1532
|
+
if (!isLuauName(node.method.name)) {
|
|
1533
|
+
this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
|
|
1534
|
+
}
|
|
1535
|
+
return methodCall(object, node.method.name, args2);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// --------------------------------------------------------
|
|
1540
|
+
// Optional chains
|
|
1541
|
+
// --------------------------------------------------------
|
|
1542
|
+
//
|
|
1543
|
+
// `a?.b.c` is nil when `a` is, and then neither `.b` nor `.c` runs. Each
|
|
1544
|
+
// `?.` tests what the chain holds so far, once: the value is kept in a
|
|
1545
|
+
// local rather than read again.
|
|
1546
|
+
/** The links of a chain from `?.` onwards, split into segments that each
|
|
1547
|
+
* start at a `?.` / `?:`. */
|
|
1548
|
+
segments(chain) {
|
|
1549
|
+
const out2 = [];
|
|
1550
|
+
for (const link of chain.links) {
|
|
1551
|
+
if (link.optional || !out2.length) out2.push([link]);
|
|
1552
|
+
else out2[out2.length - 1].push(link);
|
|
1553
|
+
}
|
|
1554
|
+
return out2;
|
|
1555
|
+
}
|
|
1556
|
+
applySegment(segment, object) {
|
|
1557
|
+
return segment.reduce((value, link) => this.link(link, value), object);
|
|
1558
|
+
}
|
|
1559
|
+
/** `a?.b:m()` as a statement:
|
|
1560
|
+
* `do local ref = a if ref ~= nil then ref.b:m() end end` */
|
|
1561
|
+
optionalCallStatement(chain) {
|
|
1562
|
+
const segments = this.segments(chain);
|
|
1563
|
+
const base = this.expression(chain.base);
|
|
1564
|
+
const ref = base.type === "Identifier" && segments.length === 1 ? base.name : this.names.fresh("ref");
|
|
1565
|
+
const at = identifier(ref);
|
|
1566
|
+
const nest = (i) => {
|
|
1567
|
+
const value = this.applySegment(segments[i], at);
|
|
1568
|
+
if (i === segments.length - 1) {
|
|
1569
|
+
return [callStatement(value)];
|
|
1570
|
+
}
|
|
1571
|
+
return [assign([at], [value]), ifThen(binary("~=", at, nil()), nest(i + 1))];
|
|
1572
|
+
};
|
|
1573
|
+
const test = ifThen(binary("~=", at, nil()), nest(0));
|
|
1574
|
+
if (base.type === "Identifier" && base.name === ref) return test;
|
|
1575
|
+
return doBlock([local([ref], [base]), test]);
|
|
1576
|
+
}
|
|
1577
|
+
/** `a?.b` as a value. The one-test form on a name is an `if` expression;
|
|
1578
|
+
* anything else runs in a function, so every link is evaluated at most
|
|
1579
|
+
* once and in order. */
|
|
1580
|
+
optionalChainExpression(chain) {
|
|
1581
|
+
const segments = this.segments(chain);
|
|
1582
|
+
const base = this.expression(chain.base);
|
|
1583
|
+
const last = chain.links[chain.links.length - 1];
|
|
1584
|
+
if (base.type === "Identifier" && segments.length === 1 && last.type !== "CallExpression" && last.type !== "MethodCallExpression") {
|
|
1585
|
+
return {
|
|
1586
|
+
type: "IfElseExpression",
|
|
1587
|
+
clauses: [{ condition: binary("==", base, nil()), body: nil() }],
|
|
1588
|
+
alternate: this.applySegment(segments[0], base),
|
|
1589
|
+
line: base.line,
|
|
1590
|
+
column: base.column
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const ref = this.names.fresh("ref");
|
|
1594
|
+
const at = identifier(ref);
|
|
1595
|
+
const statements = [local([ref], [base])];
|
|
1596
|
+
segments.forEach((segment, i) => {
|
|
1597
|
+
statements.push(ifThen(binary("==", at, nil()), [returns([nil()])]));
|
|
1598
|
+
const value = this.applySegment(segment, at);
|
|
1599
|
+
statements.push(i === segments.length - 1 ? returns([value]) : assign([at], [value]));
|
|
1600
|
+
});
|
|
1601
|
+
const passesVarargs = statements.some(usesVararg);
|
|
1602
|
+
const func = functionExpression(functionBody([], statements, passesVarargs));
|
|
1603
|
+
return call(parenthesized(func), passesVarargs ? [vararg()] : []);
|
|
1604
|
+
}
|
|
1605
|
+
/** `` `${a} any` `` -> `("%s any"):format(tostring(a))` */
|
|
1606
|
+
interpolatedString(node) {
|
|
1607
|
+
let format = "";
|
|
1608
|
+
let text = "";
|
|
1609
|
+
const args2 = [];
|
|
1610
|
+
for (const part of node.parts) {
|
|
1611
|
+
if (part.kind === "string") {
|
|
1612
|
+
format += part.value.replace(/%/g, "%%");
|
|
1613
|
+
text += part.value;
|
|
1614
|
+
} else {
|
|
1615
|
+
format += "%s";
|
|
1616
|
+
args2.push(call(this.builtin("tostring"), [this.expression(part.expression)]));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (!args2.length) return string(text);
|
|
1620
|
+
return methodCall(parenthesized(string(format)), "format", args2);
|
|
1621
|
+
}
|
|
1622
|
+
/** `a += b` for Lua 5.1, which has no compound assignment: `a = a + b`.
|
|
1623
|
+
*
|
|
1624
|
+
* The target has to be read and written, and writing it out twice would
|
|
1625
|
+
* evaluate whatever addresses it twice — `t[next()] += 1` would advance
|
|
1626
|
+
* twice and add to the wrong slot. So anything but a plain name has its
|
|
1627
|
+
* object and key taken into locals first, and both halves use those. */
|
|
1628
|
+
compoundAssignment51(node) {
|
|
1629
|
+
const operator = node.operator.slice(0, -1);
|
|
1630
|
+
const before = [];
|
|
1631
|
+
let target;
|
|
1632
|
+
if (node.target.type === "MemberExpression" || node.target.type === "IndexExpression") {
|
|
1633
|
+
const object = this.expression(node.target.object);
|
|
1634
|
+
const objectName = this.names.fresh("target");
|
|
1635
|
+
before.push(local([objectName], [object]));
|
|
1636
|
+
if (node.target.type === "MemberExpression") {
|
|
1637
|
+
target = member(identifier(objectName), node.target.property.name);
|
|
1638
|
+
} else {
|
|
1639
|
+
const keyName = this.names.fresh("key");
|
|
1640
|
+
before.push(local([keyName], [this.expression(node.target.index)]));
|
|
1641
|
+
target = index(identifier(objectName), identifier(keyName));
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
target = this.expression(node.target);
|
|
1645
|
+
}
|
|
1646
|
+
const value = this.expression(node.value);
|
|
1647
|
+
const combined = operator === "//" ? this.floorDivide(target, value) : binary(operator, target, parenthesized(value));
|
|
1648
|
+
const statement = assign([target], [combined]);
|
|
1649
|
+
return before.length ? [doBlock([...before, statement])] : [statement];
|
|
1650
|
+
}
|
|
1651
|
+
/** A loop body for Lua 5.1, which has neither `continue` nor the `goto`
|
|
1652
|
+
* every other Lua lowers it to.
|
|
1653
|
+
*
|
|
1654
|
+
* `repeat ... until true` runs its body exactly once, so a `break` inside
|
|
1655
|
+
* it abandons the rest of the body and lands back in the enclosing loop —
|
|
1656
|
+
* which is what `continue` means. The catch is that a *real* `break` in
|
|
1657
|
+
* the same body would now leave only that inner `repeat`, so when the
|
|
1658
|
+
* body has one it sets a flag that the loop checks once the `repeat` is
|
|
1659
|
+
* done, and breaks for real there. A body with no `continue` is left
|
|
1660
|
+
* exactly as it was. */
|
|
1661
|
+
loopBody51(body) {
|
|
1662
|
+
if (!this.lua51 || !hasOwnJump(body.statements, "ContinueStatement")) return body;
|
|
1663
|
+
const flag = hasOwnJump(body.statements, "BreakStatement") ? this.names.fresh("broke") : void 0;
|
|
1664
|
+
const once = repeatUntil(rewriteOwnJumps(body.statements, flag), boolean(true));
|
|
1665
|
+
if (!flag) return block([once]);
|
|
1666
|
+
return block([
|
|
1667
|
+
local([flag], [boolean(false)]),
|
|
1668
|
+
once,
|
|
1669
|
+
ifThen(identifier(flag), [breakStatement()])
|
|
1670
|
+
]);
|
|
1671
|
+
}
|
|
1672
|
+
/** `if c then a else b` as an expression, for Lua 5.1, which has none.
|
|
1673
|
+
*
|
|
1674
|
+
* `c and a or b` is the usual stand-in, but it is wrong whenever `a` can
|
|
1675
|
+
* be `false` or `nil`: it then yields `b` even though `c` held. So it is
|
|
1676
|
+
* only used where every branch is written as a value that can be neither,
|
|
1677
|
+
* and anything else becomes a call — one closure, but always the branch
|
|
1678
|
+
* that was written. */
|
|
1679
|
+
ifElse51(node) {
|
|
1680
|
+
const clauses = node.clauses.map((c) => ({
|
|
1681
|
+
condition: this.expression(c.condition),
|
|
1682
|
+
body: this.expression(c.body)
|
|
1683
|
+
}));
|
|
1684
|
+
const alternate = this.expression(node.alternate);
|
|
1685
|
+
if (clauses.every((c) => alwaysTruthy(c.body))) {
|
|
1686
|
+
let result = alternate;
|
|
1687
|
+
for (let i = clauses.length - 1; i >= 0; i--) {
|
|
1688
|
+
const { condition, body } = clauses[i];
|
|
1689
|
+
result = binary("or", binary("and", condition, body), result);
|
|
1690
|
+
}
|
|
1691
|
+
return parenthesized(result);
|
|
1692
|
+
}
|
|
1693
|
+
const statement = {
|
|
1694
|
+
type: "IfStatement",
|
|
1695
|
+
clauses: clauses.map((c) => ({
|
|
1696
|
+
type: "IfClause",
|
|
1697
|
+
condition: c.condition,
|
|
1698
|
+
body: block([returns([c.body])]),
|
|
1699
|
+
...spanOf(node)
|
|
1700
|
+
})),
|
|
1701
|
+
alternate: block([returns([alternate])]),
|
|
1702
|
+
...spanOf(node)
|
|
1703
|
+
};
|
|
1704
|
+
return call(
|
|
1705
|
+
parenthesized(functionExpression(functionBody([], [statement]))),
|
|
1706
|
+
[]
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
/** How a number is written out. Luau takes what was written; Lua 5.1 has
|
|
1710
|
+
* neither digit separators nor binary literals, so `1_000_000` loses its
|
|
1711
|
+
* underscores and `0b1010` becomes the decimal it means. A hex literal
|
|
1712
|
+
* keeps its base — 5.1 reads those. */
|
|
1713
|
+
numberRaw(node) {
|
|
1714
|
+
if (!this.lua51) return node.raw;
|
|
1715
|
+
if (/^0[bB]/.test(node.raw)) return String(node.value);
|
|
1716
|
+
return node.raw.replace(/_/g, "");
|
|
1717
|
+
}
|
|
1718
|
+
/** `a // b` for Lua 5.1, which has no floor-division operator. */
|
|
1719
|
+
floorDivide(left, right) {
|
|
1720
|
+
return call(
|
|
1721
|
+
member(this.builtin("math"), "floor"),
|
|
1722
|
+
[binary("/", left, parenthesized(right))]
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
/** `{ a: 1, b, [k]: v }`. A spread splits the literal into parts that
|
|
1726
|
+
* `assign` copies into one table in order, so a later key still wins. */
|
|
1727
|
+
tableExpression(node) {
|
|
1728
|
+
const parts = [];
|
|
1729
|
+
let current = [];
|
|
1730
|
+
for (const f of node.fields) {
|
|
1731
|
+
switch (f.type) {
|
|
1732
|
+
case "TableFieldNamed": {
|
|
1733
|
+
const key = f.key.type === "Identifier" ? f.key.name : f.key.value;
|
|
1734
|
+
current.push(field(key, this.expression(f.value)));
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
case "TableFieldShorthand":
|
|
1738
|
+
current.push(field(f.name.name, this.reference(f.name)));
|
|
1739
|
+
break;
|
|
1740
|
+
case "TableFieldComputed":
|
|
1741
|
+
current.push({ type: "TableFieldComputed", key: this.expression(f.key), value: this.expression(f.value) });
|
|
1742
|
+
break;
|
|
1743
|
+
case "TableFieldSpread":
|
|
1744
|
+
if (current.length) parts.push(table(current));
|
|
1745
|
+
current = [];
|
|
1746
|
+
parts.push(this.expression(f.argument));
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (parts.length === 0) return table(current);
|
|
1751
|
+
if (current.length) parts.push(table(current));
|
|
1752
|
+
return call(this.helper("assign"), [table([]), ...parts]);
|
|
1753
|
+
}
|
|
1754
|
+
/** `[1, ...xs, 2]`. Without a spread it is a Luau sequence; with one, the
|
|
1755
|
+
* runs of plain elements and the spread arrays are joined by `concat`. */
|
|
1756
|
+
arrayExpression(node) {
|
|
1757
|
+
if (!node.elements.some((e) => e.type === "SpreadElement")) {
|
|
1758
|
+
return table(node.elements.map((e) => ({ type: "TableFieldPositional", value: this.expression(e) })));
|
|
1759
|
+
}
|
|
1760
|
+
const parts = [];
|
|
1761
|
+
let current = [];
|
|
1762
|
+
const flush = () => {
|
|
1763
|
+
if (!current.length) return;
|
|
1764
|
+
parts.push(table(current.map((e, i) => {
|
|
1765
|
+
const value = this.expression(e);
|
|
1766
|
+
const last = i === current.length - 1;
|
|
1767
|
+
return { type: "TableFieldPositional", value: last && expandsToMany(value) ? parenthesized(value) : value };
|
|
1768
|
+
})));
|
|
1769
|
+
current = [];
|
|
1770
|
+
};
|
|
1771
|
+
for (const element of node.elements) {
|
|
1772
|
+
if (element.type === "SpreadElement") {
|
|
1773
|
+
flush();
|
|
1774
|
+
parts.push(this.expression(element.argument));
|
|
1775
|
+
} else {
|
|
1776
|
+
current.push(element);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
flush();
|
|
1780
|
+
return call(this.helper("concat"), parts);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
function isLink(node) {
|
|
1784
|
+
return node.type === "MemberExpression" || node.type === "IndexExpression" || node.type === "CallExpression" || node.type === "MethodCallExpression";
|
|
1785
|
+
}
|
|
1786
|
+
function linkObject(node) {
|
|
1787
|
+
return node.type === "CallExpression" ? node.callee : node.object;
|
|
1788
|
+
}
|
|
1789
|
+
function optionalChain(node) {
|
|
1790
|
+
const spine = [];
|
|
1791
|
+
for (let e = node; isLink(e); e = linkObject(e)) spine.unshift(e);
|
|
1792
|
+
const first = spine.findIndex((link) => link.optional);
|
|
1793
|
+
if (first < 0) return void 0;
|
|
1794
|
+
return { base: linkObject(spine[first]), links: spine.slice(first) };
|
|
1795
|
+
}
|
|
1796
|
+
function usesVararg(node) {
|
|
1797
|
+
if (Array.isArray(node)) return node.some(usesVararg);
|
|
1798
|
+
if (!node || typeof node !== "object") return false;
|
|
1799
|
+
const record = node;
|
|
1800
|
+
if (record.type === "VarargExpression") return true;
|
|
1801
|
+
if (record.type === "FunctionExpression" || record.type === "LocalFunctionStatement" || record.type === "FunctionStatement") return false;
|
|
1802
|
+
return Object.values(record).some(usesVararg);
|
|
1803
|
+
}
|
|
1804
|
+
function calleePath(path) {
|
|
1805
|
+
const [head, ...rest] = path.split(".");
|
|
1806
|
+
return rest.reduce((value, name) => member(value, name), identifier(head));
|
|
1807
|
+
}
|
|
1808
|
+
function spanOf(node) {
|
|
1809
|
+
return { line: node.line, column: node.column };
|
|
1810
|
+
}
|
|
1811
|
+
function vararg() {
|
|
1812
|
+
return { type: "VarargExpression", line: { start: 0, end: 0 }, column: { start: 0, end: 0 } };
|
|
1813
|
+
}
|
|
1814
|
+
function selectCount(select) {
|
|
1815
|
+
return call(select, [string("#"), vararg()]);
|
|
1816
|
+
}
|
|
1817
|
+
function expandsToMany(e) {
|
|
1818
|
+
return e.type === "CallExpression" || e.type === "MethodCallExpression" || e.type === "VarargExpression";
|
|
1819
|
+
}
|
|
1820
|
+
function isSuperInit(statement) {
|
|
1821
|
+
return statement.type === "CallStatement" && statement.expression.type === "CallExpression" && statement.expression.callee.type === "MemberExpression" && statement.expression.callee.property.name === "__init";
|
|
1822
|
+
}
|
|
1823
|
+
function memberChain(e) {
|
|
1824
|
+
if (e.type === "Identifier") return [e.name];
|
|
1825
|
+
if (e.type === "MemberExpression") {
|
|
1826
|
+
const base = memberChain(e.object);
|
|
1827
|
+
return base && [...base, e.property.name];
|
|
1828
|
+
}
|
|
1829
|
+
return void 0;
|
|
1830
|
+
}
|
|
1831
|
+
function identifierPatterns(target) {
|
|
1832
|
+
switch (target.type) {
|
|
1833
|
+
case "IdentifierPattern":
|
|
1834
|
+
return [target];
|
|
1835
|
+
case "ObjectPattern":
|
|
1836
|
+
return [...target.properties.flatMap((p) => identifierPatterns(p.value)), ...target.rest ? identifierPatterns(target.rest) : []];
|
|
1837
|
+
case "ArrayPattern":
|
|
1838
|
+
return [
|
|
1839
|
+
...target.elements.flatMap((e) => e ? identifierPatterns(e.value) : []),
|
|
1840
|
+
...target.rest ? identifierPatterns(target.rest) : []
|
|
1841
|
+
];
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function topLevelDeclaration(statements, name) {
|
|
1845
|
+
for (const statement of statements) {
|
|
1846
|
+
const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
|
|
1847
|
+
if (declaration.type === "VariableDeclaration") {
|
|
1848
|
+
const pattern = declaration.names.flatMap(identifierPatterns).find((p) => p.name === name);
|
|
1849
|
+
if (pattern) return { type: "Declaration", node: pattern };
|
|
1850
|
+
} else if (declaration.type === "FunctionDeclaration" && declaration.name.name === name) {
|
|
1851
|
+
return { type: "Declaration", node: declaration.name };
|
|
1852
|
+
} else if (declaration.type === "ImportStatement") {
|
|
1853
|
+
const local2 = declaration.defaultImport?.name === name ? declaration.defaultImport : declaration.specifiers.find((s) => s.local.name === name)?.local;
|
|
1854
|
+
if (local2) return { type: "ImportStatement", node: local2 };
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return void 0;
|
|
1858
|
+
}
|
|
1859
|
+
function moduleName(key) {
|
|
1860
|
+
const last = key.split("/").filter(Boolean).pop() ?? "module";
|
|
1861
|
+
const cleaned = last.replace(/[^A-Za-z0-9_]/g, "_");
|
|
1862
|
+
return /^[A-Za-z_]/.test(cleaned) && !LUAU_KEYWORDS.has(cleaned) ? cleaned : `module_${cleaned}`;
|
|
1863
|
+
}
|
|
1864
|
+
function alwaysTruthy(value) {
|
|
1865
|
+
switch (value.type) {
|
|
1866
|
+
case "NumberLiteral":
|
|
1867
|
+
case "StringLiteral":
|
|
1868
|
+
case "InterpolatedStringExpression":
|
|
1869
|
+
case "TableExpression":
|
|
1870
|
+
case "FunctionExpression":
|
|
1871
|
+
return true;
|
|
1872
|
+
case "BooleanLiteral":
|
|
1873
|
+
return value.value;
|
|
1874
|
+
case "ParenthesizedExpression":
|
|
1875
|
+
return alwaysTruthy(value.expression);
|
|
1876
|
+
default:
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function ownBlocks(statement) {
|
|
1881
|
+
switch (statement.type) {
|
|
1882
|
+
case "DoStatement":
|
|
1883
|
+
return [statement.body];
|
|
1884
|
+
case "IfStatement":
|
|
1885
|
+
return [...statement.clauses.map((c) => c.body), ...statement.alternate ? [statement.alternate] : []];
|
|
1886
|
+
default:
|
|
1887
|
+
return [];
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
function hasOwnJump(statements, type) {
|
|
1891
|
+
return statements.some((statement) => statement.type === type || ownBlocks(statement).some((b) => hasOwnJump(b.statements, type)));
|
|
1892
|
+
}
|
|
1893
|
+
function rewriteOwnJumps(statements, flag) {
|
|
1894
|
+
return statements.flatMap((statement) => {
|
|
1895
|
+
if (statement.type === "ContinueStatement") return [breakStatement()];
|
|
1896
|
+
if (statement.type === "BreakStatement") {
|
|
1897
|
+
return flag ? [assign([identifier(flag)], [boolean(true)]), breakStatement()] : [statement];
|
|
1898
|
+
}
|
|
1899
|
+
if (statement.type === "DoStatement") {
|
|
1900
|
+
return [doBlock(rewriteOwnJumps(statement.body.statements, flag))];
|
|
1901
|
+
}
|
|
1902
|
+
if (statement.type === "IfStatement") {
|
|
1903
|
+
return [{
|
|
1904
|
+
...statement,
|
|
1905
|
+
clauses: statement.clauses.map((c) => ({
|
|
1906
|
+
...c,
|
|
1907
|
+
body: block(rewriteOwnJumps(c.body.statements, flag))
|
|
1908
|
+
})),
|
|
1909
|
+
alternate: statement.alternate && block(rewriteOwnJumps(statement.alternate.statements, flag))
|
|
1910
|
+
}];
|
|
1911
|
+
}
|
|
1912
|
+
return [statement];
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
function declaredNames(statements) {
|
|
1916
|
+
const names = [];
|
|
1917
|
+
for (const statement of statements) {
|
|
1918
|
+
if (statement.type === "LocalStatement") names.push(...statement.names.map((n) => n.name));
|
|
1919
|
+
else if (statement.type === "LocalFunctionStatement") names.push(statement.name.name);
|
|
1920
|
+
}
|
|
1921
|
+
return names;
|
|
1922
|
+
}
|
|
1923
|
+
function mentions(expression, name) {
|
|
1924
|
+
let found = false;
|
|
1925
|
+
const walk = (value) => {
|
|
1926
|
+
if (found || !value || typeof value !== "object") return;
|
|
1927
|
+
if (Array.isArray(value)) {
|
|
1928
|
+
for (const item of value) walk(item);
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const node = value;
|
|
1932
|
+
if (node.type === "Identifier" && node.name === name) {
|
|
1933
|
+
found = true;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1937
|
+
if (key !== "line" && key !== "column") walk(child);
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
walk(expression);
|
|
1941
|
+
return found;
|
|
1942
|
+
}
|
|
1943
|
+
function isFunctionType(type) {
|
|
1944
|
+
if (type.kind === "function") return true;
|
|
1945
|
+
return type.kind === "intersection" && type.types.some((t) => t.kind === "function");
|
|
1946
|
+
}
|
|
1947
|
+
function allNodes(root) {
|
|
1948
|
+
const out2 = [];
|
|
1949
|
+
const visit = (value) => {
|
|
1950
|
+
if (!value || typeof value !== "object") return;
|
|
1951
|
+
if (Array.isArray(value)) return void value.forEach(visit);
|
|
1952
|
+
const record = value;
|
|
1953
|
+
if (typeof record.type === "string") out2.push(value);
|
|
1954
|
+
for (const [key, child] of Object.entries(record)) {
|
|
1955
|
+
if (key !== "line" && key !== "column") visit(child);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
visit(root);
|
|
1959
|
+
return out2;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// src/linemap.ts
|
|
1963
|
+
function statementsOf(node) {
|
|
1964
|
+
const out2 = [];
|
|
1965
|
+
const visit = (value) => {
|
|
1966
|
+
if (!value || typeof value !== "object") return;
|
|
1967
|
+
if (Array.isArray(value)) return void value.forEach(visit);
|
|
1968
|
+
const record = value;
|
|
1969
|
+
if (typeof record.type === "string" && record.type.endsWith("Statement")) {
|
|
1970
|
+
out2.push(value);
|
|
1971
|
+
}
|
|
1972
|
+
for (const [key, child] of Object.entries(record)) {
|
|
1973
|
+
if (key !== "line" && key !== "column") visit(child);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
visit(node);
|
|
1977
|
+
return out2;
|
|
1978
|
+
}
|
|
1979
|
+
function buildLineMap(emitted, printed, origins) {
|
|
1980
|
+
const before = statementsOf(emitted);
|
|
1981
|
+
const after = statementsOf(printed);
|
|
1982
|
+
if (before.length !== after.length) return /* @__PURE__ */ new Map();
|
|
1983
|
+
const map = /* @__PURE__ */ new Map();
|
|
1984
|
+
for (let i = 0; i < before.length; i++) {
|
|
1985
|
+
if (before[i].type !== after[i].type) return /* @__PURE__ */ new Map();
|
|
1986
|
+
const origin = origins.get(before[i]);
|
|
1987
|
+
if (!origin) continue;
|
|
1988
|
+
const line = after[i].line.start;
|
|
1989
|
+
if (!map.has(line)) map.set(line, origin);
|
|
1990
|
+
}
|
|
1991
|
+
return map;
|
|
1992
|
+
}
|
|
1993
|
+
function lineMapSource(map) {
|
|
1994
|
+
const entries = [...map.entries()].sort((a, b) => a[0] - b[0]);
|
|
1995
|
+
const quoted = (s) => JSON.stringify(s.replace(/\\/g, "/"));
|
|
1996
|
+
return `{${entries.map(([line, o]) => `[${line}]={${quoted(o.file)},${o.line}}`).join(",")}}`;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/lowering.ts
|
|
2000
|
+
var import_node_url = require("url");
|
|
2001
|
+
async function loadLowerings(modules) {
|
|
2002
|
+
const lowerings = [];
|
|
2003
|
+
const problems = [];
|
|
2004
|
+
for (const module2 of modules) {
|
|
2005
|
+
try {
|
|
2006
|
+
const loaded = await import((0, import_node_url.pathToFileURL)(module2.file).href);
|
|
2007
|
+
const plugin = loaded.default;
|
|
2008
|
+
if (!plugin || typeof plugin !== "object") {
|
|
2009
|
+
problems.push({
|
|
2010
|
+
file: module2.file,
|
|
2011
|
+
message: `'${module2.from}' lowering module has no default export`
|
|
2012
|
+
});
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
lowerings.push({ plugin, from: module2.from });
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
problems.push({
|
|
2018
|
+
file: module2.file,
|
|
2019
|
+
message: `'${module2.from}' lowering module failed to load: ${error.message}`
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return { lowerings, problems };
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/bundle.ts
|
|
2027
|
+
async function bundle(options) {
|
|
2028
|
+
const entry2 = (0, import_node_path2.resolve)(options.entry);
|
|
2029
|
+
const diagnostics = [];
|
|
2030
|
+
const inline = options.config !== void 0 && typeof options.config !== "string";
|
|
2031
|
+
const { config: config2, problems } = resolveConfig(options.config, entry2, options.root ?? (inline ? (0, import_node_path2.dirname)(entry2) : process.cwd()));
|
|
2032
|
+
for (const p of problems) diagnostics.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
|
|
2033
|
+
const root = (0, import_node_path2.resolve)(options.root ?? config2?.directory ?? (0, import_node_path2.dirname)(entry2));
|
|
2034
|
+
const nameOf = (file) => (0, import_node_path2.relative)(root, file).replace(/\\/g, "/").replace(/\.tilua$/, "");
|
|
2035
|
+
const sources = /* @__PURE__ */ new Map();
|
|
2036
|
+
const directives = /* @__PURE__ */ new Map();
|
|
2037
|
+
const queue = [entry2];
|
|
2038
|
+
while (queue.length) {
|
|
2039
|
+
const file = queue.shift();
|
|
2040
|
+
if (sources.has(file)) continue;
|
|
2041
|
+
const parsed = parseFile(file, diagnostics, directives);
|
|
2042
|
+
if (!parsed) return { modules: [], diagnostics };
|
|
2043
|
+
const { program: program2, text } = parsed;
|
|
2044
|
+
const scopes = (0, import_parser3.analyzeScopes)(program2);
|
|
2045
|
+
for (const d of scopes.diagnostics) {
|
|
2046
|
+
diagnostics.push({ file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "scope" });
|
|
2047
|
+
}
|
|
2048
|
+
sources.set(file, { file, name: nameOf(file), text, program: program2, scopes });
|
|
2049
|
+
for (const specifier of importedSpecifiers(program2)) {
|
|
2050
|
+
const target = (0, import_parser3.resolveModulePath)(file, specifier, config2);
|
|
2051
|
+
if (target && !target.endsWith(".d.tilua")) queue.push(target);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const analysis = analyzeModules([...sources.values()], config2);
|
|
2055
|
+
const { lowerings, problems: loweringProblems } = await loadLowerings(analysis.lowerings);
|
|
2056
|
+
for (const p of loweringProblems) {
|
|
2057
|
+
diagnostics.push({ file: p.file, message: p.message, line: 1, column: 1, category: "config" });
|
|
2058
|
+
}
|
|
2059
|
+
if (options.typeCheck !== false) diagnostics.push(...analysis.diagnostics);
|
|
2060
|
+
const names = Names.from(...[...sources.values()].map((s) => s.program));
|
|
2061
|
+
const G = names.fresh("G");
|
|
2062
|
+
const requireExpression = member(identifier(G), "require");
|
|
2063
|
+
const modules = /* @__PURE__ */ new Map();
|
|
2064
|
+
const origins = /* @__PURE__ */ new WeakMap();
|
|
2065
|
+
const pending = [entry2];
|
|
2066
|
+
while (pending.length) {
|
|
2067
|
+
const file = pending.shift();
|
|
2068
|
+
if (modules.has(file)) continue;
|
|
2069
|
+
const source = sources.get(file);
|
|
2070
|
+
const lowered = lower(source.program, source.scopes, {
|
|
2071
|
+
names,
|
|
2072
|
+
types: analysis.types.get(file),
|
|
2073
|
+
lowerings,
|
|
2074
|
+
target: options.target ?? config2?.target,
|
|
2075
|
+
source: source.text,
|
|
2076
|
+
module: {
|
|
2077
|
+
name: source.name,
|
|
2078
|
+
require: requireExpression,
|
|
2079
|
+
lines: `${G}.lines`,
|
|
2080
|
+
resolve: (specifier) => {
|
|
2081
|
+
const target = (0, import_parser3.resolveModulePath)(file, specifier, config2);
|
|
2082
|
+
if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
|
|
2083
|
+
pending.push(target);
|
|
2084
|
+
return sources.get(target).name;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
|
|
2089
|
+
const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
|
|
2090
|
+
for (const statement of allStatements(lowered.statements)) {
|
|
2091
|
+
const line = lowered.origins.get(statement);
|
|
2092
|
+
if (line !== void 0) origins.set(statement, { file: shown, line });
|
|
2093
|
+
}
|
|
2094
|
+
modules.set(file, {
|
|
2095
|
+
name: source.name,
|
|
2096
|
+
statements: lowered.statements,
|
|
2097
|
+
exportsName: lowered.exportsName,
|
|
2098
|
+
info: lowered.module
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
for (const [file, fileDirectives] of directives) {
|
|
2102
|
+
const semantic = diagnostics.filter((d) => d.file === file && (d.category === "scope" || d.category === "type"));
|
|
2103
|
+
const { kept, unusedExpectErrors } = (0, import_parser3.applyDirectives)(fileDirectives, semantic, (d) => d.line);
|
|
2104
|
+
const suppressed = new Set(semantic.filter((d) => !kept.includes(d)));
|
|
2105
|
+
const remaining = diagnostics.filter((d) => !suppressed.has(d));
|
|
2106
|
+
diagnostics.length = 0;
|
|
2107
|
+
diagnostics.push(...remaining);
|
|
2108
|
+
if (options.typeCheck === false) continue;
|
|
2109
|
+
for (const d of unusedExpectErrors) {
|
|
2110
|
+
diagnostics.push({ file, message: import_parser3.UNUSED_EXPECT_ERROR, line: d.line, column: d.column, category: "type" });
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
const moduleProblems = new Set(diagnostics.filter((d) => d.category === "module").map((d) => `${d.file}:${d.line}:${d.column}:${d.message}`));
|
|
2114
|
+
const reported = diagnostics.filter((d) => d.category !== "type" || !moduleProblems.has(`${d.file}:${d.line}:${d.column}:${d.message}`));
|
|
2115
|
+
diagnostics.length = 0;
|
|
2116
|
+
diagnostics.push(...reported);
|
|
2117
|
+
const moduleNames = [...modules.values()].map((m) => m.name);
|
|
2118
|
+
if (diagnostics.some((d) => d.category === "syntax" || d.category === "scope" || d.category === "module")) {
|
|
2119
|
+
return { modules: moduleNames, diagnostics };
|
|
2120
|
+
}
|
|
2121
|
+
const program = (0, import_luau_parser2.parse)(runtime(G));
|
|
2122
|
+
const assignment = program.body.statements[1];
|
|
2123
|
+
const modulesTable = assignment.type === "AssignmentStatement" && assignment.values[0].type === "TableExpression" ? assignment.values[0].fields.find((f) => f.type === "TableFieldNamed" && f.name.name === "modules")?.value : void 0;
|
|
2124
|
+
if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
|
|
2125
|
+
for (const module2 of modules.values()) {
|
|
2126
|
+
const { names: exported, links, stars } = module2.info;
|
|
2127
|
+
const fields = [];
|
|
2128
|
+
if (exported.length) fields.push(field("names", table(exported.map((n) => field(n, boolean(true))))));
|
|
2129
|
+
if (links.length) {
|
|
2130
|
+
fields.push(field("links", table(links.map((link) => field(link.name, table([
|
|
2131
|
+
{ type: "TableFieldPositional", value: string(link.module) },
|
|
2132
|
+
...link.imported === void 0 ? [] : [{ type: "TableFieldPositional", value: string(link.imported) }]
|
|
2133
|
+
]))))));
|
|
2134
|
+
}
|
|
2135
|
+
if (stars.length) fields.push(field("stars", table(stars.map((star) => ({ type: "TableFieldPositional", value: string(star) })))));
|
|
2136
|
+
fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
|
|
2137
|
+
modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
|
|
2138
|
+
}
|
|
2139
|
+
const start = call(requireExpression, [string(sources.get(entry2).name)]);
|
|
2140
|
+
program.body.statements.push(modules.get(entry2).info.exports ? returns([start]) : callStatement(start));
|
|
2141
|
+
const map = buildLineMap(program, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program)), origins);
|
|
2142
|
+
if (map.size) {
|
|
2143
|
+
program.body.statements.splice(program.body.statements.length - 1, 0, assign(
|
|
2144
|
+
[member(identifier(G), "lines")],
|
|
2145
|
+
[(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
|
|
2146
|
+
));
|
|
2147
|
+
}
|
|
2148
|
+
return { code: (0, import_luau_parser2.print)(program) + "\n", modules: moduleNames, diagnostics };
|
|
2149
|
+
}
|
|
2150
|
+
function runtime(G) {
|
|
2151
|
+
return `
|
|
2152
|
+
local ${G}
|
|
2153
|
+
${G} = {
|
|
2154
|
+
modules = {},
|
|
2155
|
+
records = {},
|
|
2156
|
+
require = function(name)
|
|
2157
|
+
local record = ${G}.records[name]
|
|
2158
|
+
if record ~= nil then
|
|
2159
|
+
return record.exports
|
|
2160
|
+
end
|
|
2161
|
+
local module = ${G}.modules[name]
|
|
2162
|
+
local initialized = {}
|
|
2163
|
+
local exports = setmetatable({}, {
|
|
2164
|
+
__index = function(_, key)
|
|
2165
|
+
if initialized[key] then
|
|
2166
|
+
return nil
|
|
2167
|
+
end
|
|
2168
|
+
if module.names ~= nil and module.names[key] then
|
|
2169
|
+
error(("Cannot access '%s' before initialization: '%s' has not reached it yet"):format(tostring(key), name), 2)
|
|
2170
|
+
end
|
|
2171
|
+
local link = module.links ~= nil and module.links[key]
|
|
2172
|
+
if link then
|
|
2173
|
+
local source = ${G}.require(link[1])
|
|
2174
|
+
if link[2] == nil then
|
|
2175
|
+
return source
|
|
2176
|
+
end
|
|
2177
|
+
return source[link[2]]
|
|
2178
|
+
end
|
|
2179
|
+
if module.stars ~= nil and key ~= "default" then
|
|
2180
|
+
for _, star in module.stars do
|
|
2181
|
+
local value = ${G}.require(star)[key]
|
|
2182
|
+
if value ~= nil then
|
|
2183
|
+
return value
|
|
2184
|
+
end
|
|
2185
|
+
end
|
|
2186
|
+
end
|
|
2187
|
+
return nil
|
|
2188
|
+
end,
|
|
2189
|
+
__newindex = function(target, key, value)
|
|
2190
|
+
initialized[key] = true
|
|
2191
|
+
rawset(target, key, value)
|
|
2192
|
+
end,
|
|
2193
|
+
})
|
|
2194
|
+
record = { loading = true, exports = exports }
|
|
2195
|
+
${G}.records[name] = record
|
|
2196
|
+
module.load(exports)
|
|
2197
|
+
record.loading = false
|
|
2198
|
+
return exports
|
|
2199
|
+
end,
|
|
2200
|
+
}
|
|
2201
|
+
`;
|
|
2202
|
+
}
|
|
2203
|
+
function parseFile(file, diagnostics, directives) {
|
|
2204
|
+
let text;
|
|
2205
|
+
try {
|
|
2206
|
+
text = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
2207
|
+
} catch {
|
|
2208
|
+
diagnostics.push({ file, message: "Cannot read file", line: 1, column: 1, category: "module" });
|
|
2209
|
+
return void 0;
|
|
2210
|
+
}
|
|
2211
|
+
try {
|
|
2212
|
+
const program = (0, import_parser3.parse)(text);
|
|
2213
|
+
directives.set(file, (0, import_parser3.directivesOf)(text));
|
|
2214
|
+
return { program, text };
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
if (error instanceof import_parser3.ParseError || error instanceof import_parser3.LexError) {
|
|
2217
|
+
const { line, column } = error;
|
|
2218
|
+
diagnostics.push({ file, message: error.message, line, column, category: "syntax" });
|
|
2219
|
+
return void 0;
|
|
2220
|
+
}
|
|
2221
|
+
throw error;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function importedSpecifiers(program) {
|
|
2225
|
+
return program.body.statements.flatMap((s) => s.type === "ImportStatement" || s.type === "ExportAllStatement" || s.type === "ExportNamedStatement" && s.source ? [s.source.value] : []);
|
|
2226
|
+
}
|
|
2227
|
+
function analyzeModules(modules, config2) {
|
|
2228
|
+
const out2 = [];
|
|
2229
|
+
const analyses = /* @__PURE__ */ new Map();
|
|
2230
|
+
const libraries = config2 ? (0, import_parser3.resolveTypeLibraries)(config2) : { files: [], lowerings: [], problems: [] };
|
|
2231
|
+
for (const p of libraries.problems) out2.push({ file: p.file, message: p.message, line: p.line ?? 1, column: p.column ?? 1, category: "config" });
|
|
2232
|
+
const libs = libraries.files.map((file) => (0, import_parser3.parse)((0, import_node_fs.readFileSync)(file, "utf8")));
|
|
2233
|
+
const globals = libs.flatMap((lib) => lib.body.statements.flatMap((s) => s.type === "DeclareStatement" ? [s.name] : []));
|
|
2234
|
+
const byFile = new Map(modules.map((m) => [m.file, m]));
|
|
2235
|
+
const exportsCache = /* @__PURE__ */ new Map();
|
|
2236
|
+
const inProgress = /* @__PURE__ */ new Set();
|
|
2237
|
+
const resolverFor = (file) => (specifier) => {
|
|
2238
|
+
const target = (0, import_parser3.resolveModulePath)(file, specifier, config2);
|
|
2239
|
+
if (!target) return void 0;
|
|
2240
|
+
if (inProgress.has(target)) return { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
|
|
2241
|
+
const cached = exportsCache.get(target);
|
|
2242
|
+
if (cached) return cached;
|
|
2243
|
+
let program = byFile.get(target)?.program;
|
|
2244
|
+
if (!program) {
|
|
2245
|
+
try {
|
|
2246
|
+
program = (0, import_parser3.parse)((0, import_node_fs.readFileSync)(target, "utf8"));
|
|
2247
|
+
} catch {
|
|
2248
|
+
return void 0;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
inProgress.add(target);
|
|
2252
|
+
try {
|
|
2253
|
+
const scopes = (0, import_parser3.analyzeScopes)(program, { builtinGlobals: globals });
|
|
2254
|
+
const types = (0, import_parser3.analyzeTypes)(program, scopes, { libs, resolveModule: resolverFor(target), diagnostics: false });
|
|
2255
|
+
const exports2 = (0, import_parser3.moduleExports)(program, scopes, types, resolverFor(target));
|
|
2256
|
+
exportsCache.set(target, exports2);
|
|
2257
|
+
return exports2;
|
|
2258
|
+
} finally {
|
|
2259
|
+
inProgress.delete(target);
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
for (const module2 of modules) {
|
|
2263
|
+
const scopes = (0, import_parser3.analyzeScopes)(module2.program, { builtinGlobals: globals, reportUndeclared: libs.length > 0 });
|
|
2264
|
+
for (const d of scopes.diagnostics) {
|
|
2265
|
+
if (d.kind !== "undeclared") continue;
|
|
2266
|
+
out2.push({ file: module2.file, message: d.message, line: d.node.line.start, column: d.node.column.start, category: "type" });
|
|
2267
|
+
}
|
|
2268
|
+
const types = (0, import_parser3.analyzeTypes)(module2.program, scopes, {
|
|
2269
|
+
libs,
|
|
2270
|
+
resolveModule: resolverFor(module2.file),
|
|
2271
|
+
reportUnknownTypes: libs.length > 0
|
|
2272
|
+
});
|
|
2273
|
+
analyses.set(module2.file, types);
|
|
2274
|
+
for (const d of types.diagnostics) {
|
|
2275
|
+
const at = d.node;
|
|
2276
|
+
out2.push({ file: module2.file, message: d.message, line: at.line.start, column: at.column.start, category: "type" });
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings] };
|
|
2280
|
+
}
|
|
2281
|
+
function allStatements(statements) {
|
|
2282
|
+
const out2 = [];
|
|
2283
|
+
const visit = (value) => {
|
|
2284
|
+
if (!value || typeof value !== "object") return;
|
|
2285
|
+
if (Array.isArray(value)) return void value.forEach(visit);
|
|
2286
|
+
const record = value;
|
|
2287
|
+
if (typeof record.type === "string" && record.type.endsWith("Statement")) {
|
|
2288
|
+
out2.push(value);
|
|
2289
|
+
}
|
|
2290
|
+
for (const [key, child] of Object.entries(record)) {
|
|
2291
|
+
if (key !== "line" && key !== "column") visit(child);
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
visit(statements);
|
|
2295
|
+
return out2;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/cli.ts
|
|
2299
|
+
var usage = "usage: tilua <entry> [--out <file>] [--config <tilua.config.json>] [--noCheck] [--target luau|lua51]";
|
|
2300
|
+
var args = process.argv.slice(2);
|
|
2301
|
+
var entry;
|
|
2302
|
+
var out;
|
|
2303
|
+
var config;
|
|
2304
|
+
var typeCheck = true;
|
|
2305
|
+
var buildTarget;
|
|
2306
|
+
for (let i = 0; i < args.length; i++) {
|
|
2307
|
+
const arg = args[i];
|
|
2308
|
+
if (arg === "--out") out = args[++i];
|
|
2309
|
+
else if (arg === "--config") config = args[++i];
|
|
2310
|
+
else if (arg === "--noCheck") typeCheck = false;
|
|
2311
|
+
else if (arg === "--target") {
|
|
2312
|
+
const value = args[++i];
|
|
2313
|
+
if (value !== "luau" && value !== "lua51") {
|
|
2314
|
+
console.error(`--target must be 'luau' or 'lua51', not '${value}'`);
|
|
2315
|
+
process.exit(2);
|
|
2316
|
+
}
|
|
2317
|
+
buildTarget = value;
|
|
2318
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
2319
|
+
console.log(usage);
|
|
2320
|
+
process.exit(0);
|
|
2321
|
+
} else entry = arg;
|
|
2322
|
+
}
|
|
2323
|
+
if (!entry) {
|
|
2324
|
+
console.error(usage);
|
|
2325
|
+
process.exit(2);
|
|
2326
|
+
}
|
|
2327
|
+
async function main(from) {
|
|
2328
|
+
const result = await bundle({ entry: from, config, typeCheck, target: buildTarget });
|
|
2329
|
+
for (const d of result.diagnostics) {
|
|
2330
|
+
console.error(`${(0, import_node_path3.relative)(process.cwd(), d.file)}:${d.line}:${d.column} ${d.message}`);
|
|
2331
|
+
}
|
|
2332
|
+
if (result.code === void 0) {
|
|
2333
|
+
console.error("no bundle written");
|
|
2334
|
+
process.exit(1);
|
|
2335
|
+
}
|
|
2336
|
+
const target = out ?? from.replace(/\.tilua$/, "") + ".luau";
|
|
2337
|
+
(0, import_node_fs2.writeFileSync)(target, result.code);
|
|
2338
|
+
console.log(`${target}: ${result.modules.length} module${result.modules.length === 1 ? "" : "s"}`);
|
|
2339
|
+
process.exit(result.diagnostics.length ? 1 : 0);
|
|
2340
|
+
}
|
|
2341
|
+
main(entry).catch((error) => {
|
|
2342
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2343
|
+
process.exit(1);
|
|
2344
|
+
});
|