@tilua/compiler 1.0.2 → 2.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 +44 -5
- package/dist/{chunk-QSRDDIU4.js → chunk-KG6W5XKA.js} +294 -248
- package/dist/cli.cjs +291 -245
- package/dist/cli.js +1 -1
- package/dist/index.cjs +291 -245
- package/dist/index.d.cts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -137,144 +137,6 @@ function isLuauName(name) {
|
|
|
137
137
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// src/console.ts
|
|
141
|
-
var CONSOLE_RUNTIME = `
|
|
142
|
-
local __NAME__ = (function()
|
|
143
|
-
local console = {}
|
|
144
|
-
local MAX_DEPTH = 4
|
|
145
|
-
local ESCAPES = { ["\\n"] = "\\\\n", ["\\r"] = "\\\\r", ["\\t"] = "\\\\t", ["\\""] = "\\\\\\"", ["\\\\"] = "\\\\\\\\" }
|
|
146
|
-
|
|
147
|
-
local function quote(text)
|
|
148
|
-
return '"' .. text:gsub('[%c"\\\\]', function(c) return ESCAPES[c] or ("\\\\%d"):format(c:byte()) end) .. '"'
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
-- A table with 1..n and nothing else prints as a list, as it was written.
|
|
152
|
-
local function isArray(value)
|
|
153
|
-
local count = 0
|
|
154
|
-
for key in pairs(value) do
|
|
155
|
-
if type(key) ~= "number" then return false end
|
|
156
|
-
count = count + 1
|
|
157
|
-
end
|
|
158
|
-
return count == #value
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
local function isIdentifier(key)
|
|
162
|
-
return type(key) == "string" and key:match("^[A-Za-z_][A-Za-z0-9_]*$") ~= nil
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
local render
|
|
166
|
-
|
|
167
|
-
local function renderTable(value, meta, seen, depth, indent)
|
|
168
|
-
if seen[value] then return "<cycle>" end
|
|
169
|
-
if depth > MAX_DEPTH then return "..." end
|
|
170
|
-
seen[value] = true
|
|
171
|
-
local inner = indent .. " "
|
|
172
|
-
local parts = {}
|
|
173
|
-
if isArray(value) then
|
|
174
|
-
for i = 1, #value do
|
|
175
|
-
parts[#parts + 1] = inner .. render(value[i], nil, seen, depth + 1, inner)
|
|
176
|
-
end
|
|
177
|
-
else
|
|
178
|
-
local keys = {}
|
|
179
|
-
for key in pairs(value) do keys[#keys + 1] = key end
|
|
180
|
-
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
|
181
|
-
for _, key in ipairs(keys) do
|
|
182
|
-
local name = isIdentifier(key) and key or ("[" .. render(key, nil, seen, depth + 1, inner) .. "]")
|
|
183
|
-
parts[#parts + 1] = inner .. name .. " = " .. render(value[key], nil, seen, depth + 1, inner)
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
seen[value] = nil
|
|
187
|
-
if #parts == 0 then return "{}" end
|
|
188
|
-
local open = isArray(value) and "[" or "{"
|
|
189
|
-
local close = isArray(value) and "]" or "}"
|
|
190
|
-
return open .. "\\n" .. table.concat(parts, ",\\n") .. "\\n" .. indent .. close
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
-- \`meta\` is what the compiler knew about this value and the runtime cannot:
|
|
194
|
-
-- a function's type, and the code it was written as.
|
|
195
|
-
render = function(value, meta, seen, depth, indent)
|
|
196
|
-
local kind = type(value)
|
|
197
|
-
if kind == "string" then return quote(value) end
|
|
198
|
-
if kind == "number" or kind == "boolean" or kind == "nil" then return tostring(value) end
|
|
199
|
-
if kind == "function" then
|
|
200
|
-
if meta == nil then return "function" end
|
|
201
|
-
if meta[2] == nil or meta[2] == "" then return meta[1] end
|
|
202
|
-
return meta[1] .. " " .. meta[2]
|
|
203
|
-
end
|
|
204
|
-
if kind == "table" then
|
|
205
|
-
local shown = rawget(value, "__tostring") == nil and getmetatable(value)
|
|
206
|
-
-- A value with its own \`__tostring\` (a Roblox Vector3, a class
|
|
207
|
-
-- instance that says how it reads) is trusted to describe itself.
|
|
208
|
-
if shown ~= nil and rawget(shown, "__tostring") ~= nil then return tostring(value) end
|
|
209
|
-
return renderTable(value, meta, seen, depth, indent)
|
|
210
|
-
end
|
|
211
|
-
return tostring(value)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
local function describe(meta, index, value)
|
|
215
|
-
return render(value, meta ~= nil and meta[index] or nil, {}, 1, "")
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
local function join(meta, count, ...)
|
|
219
|
-
local parts = {}
|
|
220
|
-
for i = 1, count do
|
|
221
|
-
parts[#parts + 1] = describe(meta, i, (select(i, ...)))
|
|
222
|
-
end
|
|
223
|
-
return table.concat(parts, " ")
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
-- A line of the bundle, as the place in the project that wrote it.
|
|
227
|
-
local lines = __LINES__
|
|
228
|
-
local function place(line)
|
|
229
|
-
local origin = lines ~= nil and lines[tonumber(line)]
|
|
230
|
-
if origin == nil then return nil end
|
|
231
|
-
return origin[1] .. ":" .. tostring(origin[2])
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
-- Luau names the bundle in every frame; the project's files are what the
|
|
235
|
-
-- reader knows. Frames the map has nothing for are dropped: they are this
|
|
236
|
-
-- runtime's own, and the bundle's.
|
|
237
|
-
local function trace(level)
|
|
238
|
-
local raw = debug.traceback(nil, level)
|
|
239
|
-
local out = {}
|
|
240
|
-
for frame in raw:gmatch("[^\\n]+") do
|
|
241
|
-
local line = frame:match(":(%d+)")
|
|
242
|
-
local at = line ~= nil and place(line) or nil
|
|
243
|
-
if at ~= nil then
|
|
244
|
-
local name = frame:match("function ([%w_.:]+)")
|
|
245
|
-
out[#out + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
return table.concat(out, "\\n")
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
-- Never on the caller's thread: reporting a problem must not become one.
|
|
252
|
-
local detach = (task ~= nil and task.spawn) or function(f) return f() end
|
|
253
|
-
|
|
254
|
-
local function raise(prefix, meta, count, ...)
|
|
255
|
-
local message = prefix .. join(meta, count, ...)
|
|
256
|
-
local where = trace(3)
|
|
257
|
-
detach(function()
|
|
258
|
-
error(where ~= "" and (message .. "\\n" .. where) or message, 0)
|
|
259
|
-
end)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
function console.log(meta, ...)
|
|
263
|
-
print(join(meta, select("#", ...), ...))
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
function console.warn(meta, ...)
|
|
267
|
-
raise("[warn] ", meta, select("#", ...), ...)
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
function console.error(meta, ...)
|
|
271
|
-
raise("", meta, select("#", ...), ...)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
return console
|
|
275
|
-
end)()
|
|
276
|
-
`;
|
|
277
|
-
|
|
278
140
|
// src/names.ts
|
|
279
141
|
var Names = class _Names {
|
|
280
142
|
constructor(taken) {
|
|
@@ -354,14 +216,18 @@ var Lowerer = class {
|
|
|
354
216
|
const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
|
|
355
217
|
const helpers = this.helperDefinitions();
|
|
356
218
|
const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
|
|
219
|
+
const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
|
|
357
220
|
return {
|
|
358
|
-
statements: [...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
221
|
+
statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
359
222
|
exportsName: this.exportsName,
|
|
360
223
|
module: this.info,
|
|
361
224
|
diagnostics: this.diagnostics,
|
|
362
|
-
origins: this.origins
|
|
225
|
+
origins: this.origins,
|
|
226
|
+
usesScriptArgs: this.usesScriptArgs
|
|
363
227
|
};
|
|
364
228
|
}
|
|
229
|
+
/** Whether the file reads `scriptArgs`, the language's own global. */
|
|
230
|
+
usesScriptArgs = false;
|
|
365
231
|
report(node, message) {
|
|
366
232
|
this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
|
|
367
233
|
}
|
|
@@ -422,60 +288,9 @@ var Lowerer = class {
|
|
|
422
288
|
}
|
|
423
289
|
loweringNames = /* @__PURE__ */ new Map();
|
|
424
290
|
loweringUsed = [];
|
|
425
|
-
/** What a library says `receiver:method(...)` is. The last library
|
|
426
|
-
* loaded is asked first, so a project's own library can answer for a
|
|
427
|
-
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
428
|
-
* method call, which is what a value that answers to the method itself
|
|
429
|
-
* wants (`text:upper()`). */
|
|
430
|
-
/** `console:log(a, f)` and its two siblings.
|
|
431
|
-
*
|
|
432
|
-
* Handled here rather than through a library's lowering because what they
|
|
433
|
-
* need is not a different callee but *more than was written*: a function
|
|
434
|
-
* prints as its type and its code, and neither survives to runtime. The
|
|
435
|
-
* compiler is the only thing that has both, so it passes them along:
|
|
436
|
-
*
|
|
437
|
-
* console:log(a, double)
|
|
438
|
-
* -- becomes
|
|
439
|
-
* tilua_console.log({ [2] = { "(x: number) => number", "(x: number) => x * 2" } }, a, double)
|
|
440
|
-
*
|
|
441
|
-
* The meta table is `nil` when nothing was worth saying, which is most
|
|
442
|
-
* calls. `console` itself is never passed: it holds nothing. */
|
|
443
|
-
consoleCall(node) {
|
|
444
|
-
if (node.object.type !== "Identifier" || node.object.name !== "console") return void 0;
|
|
445
|
-
const method = node.method.name;
|
|
446
|
-
if (method !== "log" && method !== "warn" && method !== "error") return void 0;
|
|
447
|
-
const binding = this.scopes.bindingOf.get(node.object);
|
|
448
|
-
if (binding !== void 0 && this.scopes.bindings.get(binding)?.kind !== "global") return void 0;
|
|
449
|
-
const meta = this.consoleMeta(node.arguments);
|
|
450
|
-
return call(
|
|
451
|
-
member(this.helper("console"), method),
|
|
452
|
-
[meta ?? nil(), ...this.values(node.arguments)]
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
/** What the compiler knows about the arguments and the runtime cannot:
|
|
456
|
-
* for each function argument, the type it was inferred as and the code it
|
|
457
|
-
* was written as. Keyed by position, so an argument that needs nothing
|
|
458
|
-
* costs nothing. */
|
|
459
|
-
consoleMeta(args) {
|
|
460
|
-
const fields = [];
|
|
461
|
-
args.forEach((argument, index2) => {
|
|
462
|
-
const type = this.options.types?.typeOf.get(argument);
|
|
463
|
-
if (!type || !isFunctionType(type)) return;
|
|
464
|
-
const written = this.writtenSource(this.declarationOf(argument) ?? argument);
|
|
465
|
-
fields.push({
|
|
466
|
-
type: "TableFieldComputed",
|
|
467
|
-
key: number(index2 + 1),
|
|
468
|
-
value: table([
|
|
469
|
-
{ type: "TableFieldPositional", value: string((0, import_parser2.formatType)(type)) },
|
|
470
|
-
{ type: "TableFieldPositional", value: string(written) }
|
|
471
|
-
])
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
return fields.length ? table(fields) : void 0;
|
|
475
|
-
}
|
|
476
291
|
/** What a name was declared as: the value of `const f = ...`, or the whole
|
|
477
|
-
* `function f() { ... }`. Built once, on the first
|
|
478
|
-
*
|
|
292
|
+
* `function f() { ... }`. Built once, on the first call a library asks
|
|
293
|
+
* about — most files have none. */
|
|
479
294
|
declarationOf(argument) {
|
|
480
295
|
if (argument.type !== "Identifier") return void 0;
|
|
481
296
|
if (!this.declarations) {
|
|
@@ -514,21 +329,131 @@ var Lowerer = class {
|
|
|
514
329
|
const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
|
|
515
330
|
return [first, ...middle.map(trim), trim(last)].join("\n");
|
|
516
331
|
}
|
|
332
|
+
/** Where `node` was written, as a library is told it. */
|
|
333
|
+
callSite(node) {
|
|
334
|
+
return { file: this.options.file, line: node.line.start, column: node.column.start };
|
|
335
|
+
}
|
|
336
|
+
/** What the compiler knows about each argument and the runtime cannot:
|
|
337
|
+
* its type, and the code it — or what it names — was written as. Built
|
|
338
|
+
* only if a library reads it. */
|
|
339
|
+
argumentInfo(args) {
|
|
340
|
+
return args.map((argument) => {
|
|
341
|
+
const declaration = this.declarationOf(argument);
|
|
342
|
+
const type = this.options.types?.typeOf.get(argument);
|
|
343
|
+
return {
|
|
344
|
+
type,
|
|
345
|
+
typeText: type ? (0, import_parser2.formatType)(type) : void 0,
|
|
346
|
+
source: this.writtenSource(argument),
|
|
347
|
+
declaration: declaration ? this.writtenSource(declaration) : void 0,
|
|
348
|
+
spread: argument.type === "SpreadElement"
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** The name a global is read by at `node`, or `undefined` when `node` is
|
|
353
|
+
* not a global — a local of the same name is the author's. */
|
|
354
|
+
globalName(node) {
|
|
355
|
+
if (node.type === "MemberExpression" && !node.optional) {
|
|
356
|
+
const base = this.globalName(node.object);
|
|
357
|
+
return base === void 0 ? void 0 : `${base}.${node.property.name}`;
|
|
358
|
+
}
|
|
359
|
+
if (node.type !== "Identifier") return void 0;
|
|
360
|
+
const id = this.scopes.bindingOf.get(node);
|
|
361
|
+
if (id === void 0) return node.name;
|
|
362
|
+
return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
|
|
363
|
+
}
|
|
364
|
+
/** What a library says `receiver:method(...)` is. The last library
|
|
365
|
+
* loaded is asked first, so a project's own library can answer for a
|
|
366
|
+
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
367
|
+
* method call, which is what a value that answers to the method itself
|
|
368
|
+
* wants (`text:upper()`). */
|
|
517
369
|
loweredMethodCall(node) {
|
|
518
370
|
const lowerings = this.options.lowerings;
|
|
519
|
-
if (!lowerings?.
|
|
371
|
+
if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
|
|
520
372
|
const receiver = this.options.types?.typeOf.get(node.object);
|
|
373
|
+
const receiverGlobal = this.globalName(node.object);
|
|
374
|
+
const info = this.lazyArguments(node.arguments);
|
|
521
375
|
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
522
|
-
const
|
|
376
|
+
const plugin = lowerings[i].plugin;
|
|
377
|
+
if (!plugin.methodCall) continue;
|
|
378
|
+
const answer = plugin.methodCall({
|
|
523
379
|
method: node.method.name,
|
|
524
380
|
receiver,
|
|
381
|
+
receiverGlobal,
|
|
525
382
|
argumentCount: node.arguments.length,
|
|
383
|
+
at: this.callSite(node.method),
|
|
384
|
+
get arguments() {
|
|
385
|
+
return info();
|
|
386
|
+
},
|
|
387
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
388
|
+
});
|
|
389
|
+
if (answer) {
|
|
390
|
+
return {
|
|
391
|
+
callee: answer.callee,
|
|
392
|
+
prepend: this.prepended(answer.prepend, i, node),
|
|
393
|
+
passReceiver: answer.passReceiver !== false
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
/** What a library says a call to a global — `print(x)` — is. */
|
|
400
|
+
loweredGlobalCall(node) {
|
|
401
|
+
const lowerings = this.options.lowerings;
|
|
402
|
+
if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
|
|
403
|
+
const name = this.globalName(node.callee);
|
|
404
|
+
if (name === void 0) return void 0;
|
|
405
|
+
const info = this.lazyArguments(node.arguments);
|
|
406
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
407
|
+
const plugin = lowerings[i].plugin;
|
|
408
|
+
if (!plugin.globalCall) continue;
|
|
409
|
+
const answer = plugin.globalCall({
|
|
410
|
+
name,
|
|
411
|
+
at: this.callSite(node.callee),
|
|
412
|
+
get arguments() {
|
|
413
|
+
return info();
|
|
414
|
+
},
|
|
415
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
416
|
+
});
|
|
417
|
+
if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
|
|
418
|
+
}
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
/** What a library says a global read as a value — `local p = print` — is. */
|
|
422
|
+
loweredGlobalValue(node) {
|
|
423
|
+
const lowerings = this.options.lowerings;
|
|
424
|
+
if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
|
|
425
|
+
const name = this.globalName(node);
|
|
426
|
+
if (name === void 0) return void 0;
|
|
427
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
428
|
+
const answer = lowerings[i].plugin.globalValue?.({
|
|
429
|
+
name,
|
|
430
|
+
at: this.callSite(node),
|
|
526
431
|
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
527
432
|
});
|
|
528
|
-
if (answer) return
|
|
433
|
+
if (answer !== void 0) return this.libraryExpression(answer, i, node);
|
|
529
434
|
}
|
|
530
435
|
return void 0;
|
|
531
436
|
}
|
|
437
|
+
lazyArguments(args) {
|
|
438
|
+
let info;
|
|
439
|
+
return () => info ??= this.argumentInfo(args);
|
|
440
|
+
}
|
|
441
|
+
prepended(sources, index2, node) {
|
|
442
|
+
return (sources ?? []).flatMap((source) => {
|
|
443
|
+
const expression = this.libraryExpression(source, index2, node);
|
|
444
|
+
return expression ? [expression] : [];
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** Luau a library wrote as an expression. One that does not parse is the
|
|
448
|
+
* library's mistake, reported at the call it was written for. */
|
|
449
|
+
libraryExpression(source, index2, node) {
|
|
450
|
+
try {
|
|
451
|
+
return (0, import_luau_parser.parseExpressionFromSource)(source);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
532
457
|
helperDefinitions() {
|
|
533
458
|
const out = [];
|
|
534
459
|
const assign2 = this.helpers.get("assign");
|
|
@@ -556,12 +481,9 @@ var Lowerer = class {
|
|
|
556
481
|
});
|
|
557
482
|
continue;
|
|
558
483
|
}
|
|
559
|
-
out.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name)).body.statements);
|
|
560
|
-
}
|
|
561
|
-
const console = this.helpers.get("console");
|
|
562
|
-
if (console) {
|
|
563
484
|
const lines = this.options.module?.lines ?? "nil";
|
|
564
|
-
|
|
485
|
+
const fail = this.options.module?.fail ?? "nil";
|
|
486
|
+
out.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
|
|
565
487
|
}
|
|
566
488
|
const concat = this.helpers.get("concat");
|
|
567
489
|
if (concat) {
|
|
@@ -730,7 +652,7 @@ var Lowerer = class {
|
|
|
730
652
|
break;
|
|
731
653
|
}
|
|
732
654
|
case "ReturnStatement":
|
|
733
|
-
if (declaration.
|
|
655
|
+
if (declaration.argument) this.report(declaration, "A module cannot return a value; export it instead");
|
|
734
656
|
body.push(returns([]));
|
|
735
657
|
break;
|
|
736
658
|
case "ImportStatement":
|
|
@@ -909,7 +831,7 @@ var Lowerer = class {
|
|
|
909
831
|
case "CallStatement": {
|
|
910
832
|
const chain = optionalChain(node.expression);
|
|
911
833
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
912
|
-
const expression = this.
|
|
834
|
+
const expression = this.callValues(node.expression);
|
|
913
835
|
if (expression.type !== "CallExpression" && expression.type !== "MethodCallExpression") {
|
|
914
836
|
this.report(node, "A statement must be a call");
|
|
915
837
|
return [];
|
|
@@ -962,23 +884,26 @@ var Lowerer = class {
|
|
|
962
884
|
}];
|
|
963
885
|
case "GenericForStatement": {
|
|
964
886
|
const prelude = [];
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (variables.length === 1 && this.options.types?.iteratesValues.has(node)) {
|
|
972
|
-
variables.unshift(this.names.fresh("_"));
|
|
887
|
+
let item;
|
|
888
|
+
if (node.variable.type === "IdentifierPattern") {
|
|
889
|
+
item = this.name(node.variable.name);
|
|
890
|
+
} else {
|
|
891
|
+
item = this.names.fresh("item");
|
|
892
|
+
prelude.push(...this.destructure(node.variable, identifier(item), "declare"));
|
|
973
893
|
}
|
|
894
|
+
const form = this.options.types?.loops.get(node) ?? { walks: "values", viaIter: false };
|
|
895
|
+
let source = this.expression(node.iterator);
|
|
896
|
+
if (form.viaIter) source = methodCall(source, "__iter", []);
|
|
897
|
+
const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
|
|
898
|
+
const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
|
|
974
899
|
return [genericFor(
|
|
975
900
|
variables,
|
|
976
|
-
|
|
901
|
+
iterators,
|
|
977
902
|
this.loopBody51(this.block(node.body, prelude)).statements
|
|
978
903
|
)];
|
|
979
904
|
}
|
|
980
905
|
case "ReturnStatement":
|
|
981
|
-
return [returns(this.
|
|
906
|
+
return [returns(node.argument ? [this.expression(node.argument)] : [])];
|
|
982
907
|
case "BreakStatement":
|
|
983
908
|
return [{ type: "BreakStatement", ...spanOf(node) }];
|
|
984
909
|
case "ContinueStatement":
|
|
@@ -1022,7 +947,7 @@ var Lowerer = class {
|
|
|
1022
947
|
// end
|
|
1023
948
|
// function Dog.speak(this) ... end
|
|
1024
949
|
//
|
|
1025
|
-
// `new
|
|
950
|
+
// `Dog.new(x)` builds one, `dog:speak()` passes the instance
|
|
1026
951
|
// as `this`, and `super.speak()` is `Animal.speak(this)` — the
|
|
1027
952
|
// base's own function, run on this instance.
|
|
1028
953
|
// ============================================================
|
|
@@ -1048,6 +973,7 @@ var Lowerer = class {
|
|
|
1048
973
|
classRuntimeStatements() {
|
|
1049
974
|
if (!this.classHelpers) return [];
|
|
1050
975
|
const { build, accessors } = this.classHelpers;
|
|
976
|
+
const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
|
|
1051
977
|
return (0, import_luau_parser.parse)(`
|
|
1052
978
|
local function ${build}(base)
|
|
1053
979
|
local class = { __getters = {}, __setters = {} }
|
|
@@ -1058,6 +984,9 @@ local function ${build}(base)
|
|
|
1058
984
|
setmetatable(class, { __index = base })
|
|
1059
985
|
setmetatable(class.__getters, { __index = base.__getters })
|
|
1060
986
|
setmetatable(class.__setters, { __index = base.__setters })
|
|
987
|
+
for _, key in ipairs(${metamethods}) do
|
|
988
|
+
class[key] = base[key]
|
|
989
|
+
end
|
|
1061
990
|
end
|
|
1062
991
|
return class
|
|
1063
992
|
end
|
|
@@ -1120,6 +1049,7 @@ end
|
|
|
1120
1049
|
}
|
|
1121
1050
|
break;
|
|
1122
1051
|
case "ClassMethod":
|
|
1052
|
+
if (member2.isAbstract) break;
|
|
1123
1053
|
out.push(this.functionStatement(
|
|
1124
1054
|
member(self, member2.name.name),
|
|
1125
1055
|
member2,
|
|
@@ -1141,7 +1071,7 @@ end
|
|
|
1141
1071
|
}
|
|
1142
1072
|
}
|
|
1143
1073
|
out.push(this.classInit(node, self));
|
|
1144
|
-
out.push(this.classNew(node, self));
|
|
1074
|
+
if (!node.isAbstract) out.push(this.classNew(node, self));
|
|
1145
1075
|
out.push(callStatement(call(identifier(accessors), [self])));
|
|
1146
1076
|
} finally {
|
|
1147
1077
|
this.classContext = previous;
|
|
@@ -1419,7 +1349,11 @@ end
|
|
|
1419
1349
|
}
|
|
1420
1350
|
switch (node.type) {
|
|
1421
1351
|
case "Identifier":
|
|
1422
|
-
|
|
1352
|
+
if (node.name === "scriptArgs" && this.isLanguageGlobal(node)) {
|
|
1353
|
+
this.usesScriptArgs = true;
|
|
1354
|
+
return identifier("scriptArgs");
|
|
1355
|
+
}
|
|
1356
|
+
return this.loweredGlobalValue(node) ?? this.reference(node);
|
|
1423
1357
|
case "NilLiteral":
|
|
1424
1358
|
return nil();
|
|
1425
1359
|
case "BooleanLiteral":
|
|
@@ -1428,8 +1362,6 @@ end
|
|
|
1428
1362
|
return number(node.value, this.numberRaw(node));
|
|
1429
1363
|
case "StringLiteral":
|
|
1430
1364
|
return string(node.value);
|
|
1431
|
-
case "VarargExpression":
|
|
1432
|
-
return vararg();
|
|
1433
1365
|
// Only a recovering parse makes one, and a syntax error stops the build.
|
|
1434
1366
|
case "ErrorExpression":
|
|
1435
1367
|
this.report(node, "Syntax error");
|
|
@@ -1450,11 +1382,6 @@ end
|
|
|
1450
1382
|
}
|
|
1451
1383
|
case "UnaryExpression":
|
|
1452
1384
|
return unary(node.operator, this.expression(node.argument));
|
|
1453
|
-
case "NewExpression":
|
|
1454
|
-
return call(
|
|
1455
|
-
member(this.expression(node.callee), "new"),
|
|
1456
|
-
this.values(node.arguments)
|
|
1457
|
-
);
|
|
1458
1385
|
case "SuperExpression":
|
|
1459
1386
|
return this.superClassReference(node);
|
|
1460
1387
|
case "SpreadElement":
|
|
@@ -1462,12 +1389,15 @@ end
|
|
|
1462
1389
|
return nil();
|
|
1463
1390
|
case "ClassExpression":
|
|
1464
1391
|
return this.classExpression(node);
|
|
1465
|
-
case "MemberExpression":
|
|
1466
|
-
case "IndexExpression":
|
|
1467
1392
|
case "CallExpression":
|
|
1468
|
-
case "MethodCallExpression":
|
|
1393
|
+
case "MethodCallExpression":
|
|
1394
|
+
return this.callValues(node);
|
|
1395
|
+
case "MemberExpression":
|
|
1396
|
+
case "IndexExpression": {
|
|
1469
1397
|
const chain = optionalChain(node);
|
|
1470
1398
|
if (chain) return this.optionalChainExpression(chain);
|
|
1399
|
+
const global = node.type === "MemberExpression" ? this.loweredGlobalValue(node) : void 0;
|
|
1400
|
+
if (global) return global;
|
|
1471
1401
|
return this.link(node, this.expression(linkObject(node)));
|
|
1472
1402
|
}
|
|
1473
1403
|
case "ParenthesizedExpression":
|
|
@@ -1487,14 +1417,47 @@ end
|
|
|
1487
1417
|
};
|
|
1488
1418
|
}
|
|
1489
1419
|
}
|
|
1420
|
+
/** A call. */
|
|
1421
|
+
callValues(node) {
|
|
1422
|
+
const chain = optionalChain(node);
|
|
1423
|
+
if (chain) return this.optionalChainExpression(chain);
|
|
1424
|
+
const object = linkObject(node);
|
|
1425
|
+
if (node.type === "CallExpression" && object.type === "Identifier") {
|
|
1426
|
+
return this.link(node, this.reference(object));
|
|
1427
|
+
}
|
|
1428
|
+
return this.link(node, this.expression(object));
|
|
1429
|
+
}
|
|
1430
|
+
/** A call is one value in tilua, and Luau would hand on every value one
|
|
1431
|
+
* in the last place of an argument list or an array returns. Types say
|
|
1432
|
+
* how many a call returns; where a type says nothing — `any`, `unknown`
|
|
1433
|
+
* — the call is kept to one: `f((g()))`. */
|
|
1434
|
+
keptToOne(nodes, values) {
|
|
1435
|
+
const node = nodes[nodes.length - 1];
|
|
1436
|
+
const last = values[values.length - 1];
|
|
1437
|
+
if (!node || !last || !expandsToMany(last)) return values;
|
|
1438
|
+
const type = this.options.types?.typeOf.get(node);
|
|
1439
|
+
if (type && type.kind !== "any" && type.kind !== "unknown") return values;
|
|
1440
|
+
return [...values.slice(0, -1), parenthesized(last)];
|
|
1441
|
+
}
|
|
1442
|
+
/** Is this `scriptArgs` the language's own, not a name the file declared? */
|
|
1443
|
+
isLanguageGlobal(node) {
|
|
1444
|
+
const id = this.bindingIdOf(node);
|
|
1445
|
+
return id === void 0 || this.scopes.bindings.get(id)?.kind === "global";
|
|
1446
|
+
}
|
|
1490
1447
|
/** A list of values — a call's arguments, a `return`'s, a declaration's,
|
|
1491
1448
|
* an assignment's. `f(a, ...xs)` is Lua's own last-value expansion,
|
|
1492
1449
|
* `f(a, table.unpack(xs))`; a spread anywhere else cannot be, since only
|
|
1493
1450
|
* the last value of a list expands, so the whole list is built as an
|
|
1494
|
-
* array first and that is what expands.
|
|
1495
|
-
|
|
1451
|
+
* array first and that is what expands.
|
|
1452
|
+
*
|
|
1453
|
+
* `arguments` is set for a call's arguments, where Luau hands on every
|
|
1454
|
+
* value the last one returns — see `keptToOne`. */
|
|
1455
|
+
values(list, asArguments = false) {
|
|
1496
1456
|
const spreads = list.filter((a) => a.type === "SpreadElement");
|
|
1497
|
-
if (!spreads.length)
|
|
1457
|
+
if (!spreads.length) {
|
|
1458
|
+
const lowered = list.map((a) => this.expression(a));
|
|
1459
|
+
return asArguments ? this.keptToOne(list, lowered) : lowered;
|
|
1460
|
+
}
|
|
1498
1461
|
const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
|
|
1499
1462
|
const last = list[list.length - 1];
|
|
1500
1463
|
if (spreads.length === 1 && last.type === "SpreadElement") {
|
|
@@ -1513,15 +1476,21 @@ end
|
|
|
1513
1476
|
return member(object, node.property.name);
|
|
1514
1477
|
case "IndexExpression":
|
|
1515
1478
|
return index(object, this.expression(node.index));
|
|
1516
|
-
case "CallExpression":
|
|
1517
|
-
|
|
1479
|
+
case "CallExpression": {
|
|
1480
|
+
const lowered = this.loweredGlobalCall(node);
|
|
1481
|
+
const args = this.values(node.arguments, true);
|
|
1482
|
+
if (!lowered) return call(object, args);
|
|
1483
|
+
return call(calleePath(lowered.callee), [...lowered.prepend, ...args]);
|
|
1484
|
+
}
|
|
1518
1485
|
case "MethodCallExpression": {
|
|
1519
|
-
const consoleCall = this.consoleCall(node);
|
|
1520
|
-
if (consoleCall) return consoleCall;
|
|
1521
|
-
const args = this.values(node.arguments);
|
|
1522
1486
|
const lowered = this.loweredMethodCall(node);
|
|
1487
|
+
const args = this.values(node.arguments, true);
|
|
1523
1488
|
if (lowered) {
|
|
1524
|
-
return call(calleePath(lowered.callee),
|
|
1489
|
+
return call(calleePath(lowered.callee), [
|
|
1490
|
+
...lowered.prepend,
|
|
1491
|
+
...lowered.passReceiver ? [object] : [],
|
|
1492
|
+
...args
|
|
1493
|
+
]);
|
|
1525
1494
|
}
|
|
1526
1495
|
if (!isLuauName(node.method.name)) {
|
|
1527
1496
|
this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
|
|
@@ -1749,7 +1718,8 @@ end
|
|
|
1749
1718
|
* runs of plain elements and the spread arrays are joined by `concat`. */
|
|
1750
1719
|
arrayExpression(node) {
|
|
1751
1720
|
if (!node.elements.some((e) => e.type === "SpreadElement")) {
|
|
1752
|
-
|
|
1721
|
+
const elements = node.elements;
|
|
1722
|
+
return table(this.keptToOne(elements, elements.map((e) => this.expression(e))).map((value) => ({ type: "TableFieldPositional", value })));
|
|
1753
1723
|
}
|
|
1754
1724
|
const parts = [];
|
|
1755
1725
|
let current = [];
|
|
@@ -1934,10 +1904,6 @@ function mentions(expression, name) {
|
|
|
1934
1904
|
walk(expression);
|
|
1935
1905
|
return found;
|
|
1936
1906
|
}
|
|
1937
|
-
function isFunctionType(type) {
|
|
1938
|
-
if (type.kind === "function") return true;
|
|
1939
|
-
return type.kind === "intersection" && type.types.some((t) => t.kind === "function");
|
|
1940
|
-
}
|
|
1941
1907
|
function allNodes(root) {
|
|
1942
1908
|
const out = [];
|
|
1943
1909
|
const visit = (value) => {
|
|
@@ -1952,6 +1918,24 @@ function allNodes(root) {
|
|
|
1952
1918
|
visit(root);
|
|
1953
1919
|
return out;
|
|
1954
1920
|
}
|
|
1921
|
+
var CLASS_METAMETHODS = [
|
|
1922
|
+
"__add",
|
|
1923
|
+
"__sub",
|
|
1924
|
+
"__mul",
|
|
1925
|
+
"__div",
|
|
1926
|
+
"__idiv",
|
|
1927
|
+
"__mod",
|
|
1928
|
+
"__pow",
|
|
1929
|
+
"__unm",
|
|
1930
|
+
"__concat",
|
|
1931
|
+
"__len",
|
|
1932
|
+
"__eq",
|
|
1933
|
+
"__lt",
|
|
1934
|
+
"__le",
|
|
1935
|
+
"__call",
|
|
1936
|
+
"__tostring",
|
|
1937
|
+
"__iter"
|
|
1938
|
+
];
|
|
1955
1939
|
|
|
1956
1940
|
// src/linemap.ts
|
|
1957
1941
|
function statementsOf(node) {
|
|
@@ -2061,16 +2045,19 @@ async function bundle(options) {
|
|
|
2061
2045
|
const file = pending.shift();
|
|
2062
2046
|
if (modules.has(file)) continue;
|
|
2063
2047
|
const source = sources.get(file);
|
|
2048
|
+
const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
|
|
2064
2049
|
const lowered = lower(source.program, source.scopes, {
|
|
2065
2050
|
names,
|
|
2066
2051
|
types: analysis.types.get(file),
|
|
2067
2052
|
lowerings,
|
|
2068
2053
|
target: options.target ?? config?.target,
|
|
2069
2054
|
source: source.text,
|
|
2055
|
+
file: shown,
|
|
2070
2056
|
module: {
|
|
2071
2057
|
name: source.name,
|
|
2072
2058
|
require: requireExpression,
|
|
2073
2059
|
lines: `${G}.lines`,
|
|
2060
|
+
fail: `${G}.fail`,
|
|
2074
2061
|
resolve: (specifier) => {
|
|
2075
2062
|
const target = (0, import_parser3.resolveModulePath)(file, specifier, config);
|
|
2076
2063
|
if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
|
|
@@ -2080,7 +2067,6 @@ async function bundle(options) {
|
|
|
2080
2067
|
}
|
|
2081
2068
|
});
|
|
2082
2069
|
for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
|
|
2083
|
-
const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
|
|
2084
2070
|
for (const statement of allStatements(lowered.statements)) {
|
|
2085
2071
|
const line = lowered.origins.get(statement);
|
|
2086
2072
|
if (line !== void 0) origins.set(statement, { file: shown, line });
|
|
@@ -2089,7 +2075,8 @@ async function bundle(options) {
|
|
|
2089
2075
|
name: source.name,
|
|
2090
2076
|
statements: lowered.statements,
|
|
2091
2077
|
exportsName: lowered.exportsName,
|
|
2092
|
-
info: lowered.module
|
|
2078
|
+
info: lowered.module,
|
|
2079
|
+
usesScriptArgs: lowered.usesScriptArgs
|
|
2093
2080
|
});
|
|
2094
2081
|
}
|
|
2095
2082
|
for (const [file, fileDirectives] of directives) {
|
|
@@ -2114,6 +2101,9 @@ async function bundle(options) {
|
|
|
2114
2101
|
}
|
|
2115
2102
|
const program2 = (0, import_luau_parser2.parse)(runtime(G));
|
|
2116
2103
|
const assignment = program2.body.statements[1];
|
|
2104
|
+
if ([...modules.values()].some((m) => m.usesScriptArgs)) {
|
|
2105
|
+
program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
|
|
2106
|
+
}
|
|
2117
2107
|
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;
|
|
2118
2108
|
if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
|
|
2119
2109
|
for (const module2 of modules.values()) {
|
|
@@ -2130,11 +2120,21 @@ async function bundle(options) {
|
|
|
2130
2120
|
fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
|
|
2131
2121
|
modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
|
|
2132
2122
|
}
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2123
|
+
const ok = names.fresh("ok");
|
|
2124
|
+
const result = names.fresh("result");
|
|
2125
|
+
const entryStatements = (0, import_luau_parser2.parse)(`
|
|
2126
|
+
local ${ok}, ${result} = xpcall(function()
|
|
2127
|
+
return ${G}.require(${JSON.stringify(sources.get(entry).name)})
|
|
2128
|
+
end, ${G}.fail)
|
|
2129
|
+
if not ${ok} then
|
|
2130
|
+
error(${result}, 0)
|
|
2131
|
+
end
|
|
2132
|
+
${modules.get(entry).info.exports ? `return ${result}` : ""}
|
|
2133
|
+
`).body.statements;
|
|
2134
|
+
program2.body.statements.push(...entryStatements);
|
|
2135
2135
|
const map = buildLineMap(program2, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program2)), origins);
|
|
2136
2136
|
if (map.size) {
|
|
2137
|
-
program2.body.statements.splice(program2.body.statements.length -
|
|
2137
|
+
program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
|
|
2138
2138
|
[member(identifier(G), "lines")],
|
|
2139
2139
|
[(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
|
|
2140
2140
|
));
|
|
@@ -2146,6 +2146,52 @@ function runtime(G) {
|
|
|
2146
2146
|
local ${G}
|
|
2147
2147
|
${G} = {
|
|
2148
2148
|
modules = {},
|
|
2149
|
+
-- Filled in just before the entry runs: bundle line -> { file, line }.
|
|
2150
|
+
lines = nil,
|
|
2151
|
+
-- What an error nothing caught says, in the project's files: the bundle's
|
|
2152
|
+
-- own positions in the message are replaced by the places they came from,
|
|
2153
|
+
-- and a traceback of those places follows. A message that is not a string
|
|
2154
|
+
-- is left as it is, and a position that is not this bundle's (a library
|
|
2155
|
+
-- that raised its error with one already mapped) is left alone.
|
|
2156
|
+
fail = function(message)
|
|
2157
|
+
if type(message) ~= "string" then
|
|
2158
|
+
return message
|
|
2159
|
+
end
|
|
2160
|
+
local lines = ${G}.lines
|
|
2161
|
+
if lines == nil then
|
|
2162
|
+
return debug.traceback(message, 2)
|
|
2163
|
+
end
|
|
2164
|
+
local chunk
|
|
2165
|
+
if debug.info ~= nil then
|
|
2166
|
+
chunk = debug.info(1, "s")
|
|
2167
|
+
else
|
|
2168
|
+
chunk = debug.getinfo(1, "S").short_src
|
|
2169
|
+
end
|
|
2170
|
+
local prefix = chunk:gsub("%p", "%%%0")
|
|
2171
|
+
local function place(line)
|
|
2172
|
+
local origin = lines[tonumber(line)]
|
|
2173
|
+
if origin == nil then
|
|
2174
|
+
return nil
|
|
2175
|
+
end
|
|
2176
|
+
return origin[1] .. ":" .. tostring(origin[2])
|
|
2177
|
+
end
|
|
2178
|
+
local mapped = message:gsub(prefix .. ":(%d+)", function(line)
|
|
2179
|
+
return place(line)
|
|
2180
|
+
end)
|
|
2181
|
+
local frames = {}
|
|
2182
|
+
for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
|
|
2183
|
+
local line = frame:match(prefix .. ":(%d+)")
|
|
2184
|
+
local at = line ~= nil and place(line) or nil
|
|
2185
|
+
if at ~= nil then
|
|
2186
|
+
local name = frame:match("function ([%w_.:]+)")
|
|
2187
|
+
frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
2188
|
+
end
|
|
2189
|
+
end
|
|
2190
|
+
if #frames == 0 then
|
|
2191
|
+
return mapped
|
|
2192
|
+
end
|
|
2193
|
+
return mapped .. "\\n" .. table.concat(frames, "\\n")
|
|
2194
|
+
end,
|
|
2149
2195
|
records = {},
|
|
2150
2196
|
require = function(name)
|
|
2151
2197
|
local record = ${G}.records[name]
|