@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/cli.cjs
CHANGED
|
@@ -113,144 +113,6 @@ function isLuauName(name) {
|
|
|
113
113
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
|
|
114
114
|
}
|
|
115
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
116
|
// src/names.ts
|
|
255
117
|
var Names = class _Names {
|
|
256
118
|
constructor(taken) {
|
|
@@ -330,14 +192,18 @@ var Lowerer = class {
|
|
|
330
192
|
const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
|
|
331
193
|
const helpers = this.helperDefinitions();
|
|
332
194
|
const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
|
|
195
|
+
const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
|
|
333
196
|
return {
|
|
334
|
-
statements: [...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
197
|
+
statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
335
198
|
exportsName: this.exportsName,
|
|
336
199
|
module: this.info,
|
|
337
200
|
diagnostics: this.diagnostics,
|
|
338
|
-
origins: this.origins
|
|
201
|
+
origins: this.origins,
|
|
202
|
+
usesScriptArgs: this.usesScriptArgs
|
|
339
203
|
};
|
|
340
204
|
}
|
|
205
|
+
/** Whether the file reads `scriptArgs`, the language's own global. */
|
|
206
|
+
usesScriptArgs = false;
|
|
341
207
|
report(node, message) {
|
|
342
208
|
this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
|
|
343
209
|
}
|
|
@@ -398,60 +264,9 @@ var Lowerer = class {
|
|
|
398
264
|
}
|
|
399
265
|
loweringNames = /* @__PURE__ */ new Map();
|
|
400
266
|
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
267
|
/** What a name was declared as: the value of `const f = ...`, or the whole
|
|
453
|
-
* `function f() { ... }`. Built once, on the first
|
|
454
|
-
*
|
|
268
|
+
* `function f() { ... }`. Built once, on the first call a library asks
|
|
269
|
+
* about — most files have none. */
|
|
455
270
|
declarationOf(argument) {
|
|
456
271
|
if (argument.type !== "Identifier") return void 0;
|
|
457
272
|
if (!this.declarations) {
|
|
@@ -490,21 +305,131 @@ var Lowerer = class {
|
|
|
490
305
|
const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
|
|
491
306
|
return [first, ...middle.map(trim), trim(last)].join("\n");
|
|
492
307
|
}
|
|
308
|
+
/** Where `node` was written, as a library is told it. */
|
|
309
|
+
callSite(node) {
|
|
310
|
+
return { file: this.options.file, line: node.line.start, column: node.column.start };
|
|
311
|
+
}
|
|
312
|
+
/** What the compiler knows about each argument and the runtime cannot:
|
|
313
|
+
* its type, and the code it — or what it names — was written as. Built
|
|
314
|
+
* only if a library reads it. */
|
|
315
|
+
argumentInfo(args2) {
|
|
316
|
+
return args2.map((argument) => {
|
|
317
|
+
const declaration = this.declarationOf(argument);
|
|
318
|
+
const type = this.options.types?.typeOf.get(argument);
|
|
319
|
+
return {
|
|
320
|
+
type,
|
|
321
|
+
typeText: type ? (0, import_parser2.formatType)(type) : void 0,
|
|
322
|
+
source: this.writtenSource(argument),
|
|
323
|
+
declaration: declaration ? this.writtenSource(declaration) : void 0,
|
|
324
|
+
spread: argument.type === "SpreadElement"
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/** The name a global is read by at `node`, or `undefined` when `node` is
|
|
329
|
+
* not a global — a local of the same name is the author's. */
|
|
330
|
+
globalName(node) {
|
|
331
|
+
if (node.type === "MemberExpression" && !node.optional) {
|
|
332
|
+
const base = this.globalName(node.object);
|
|
333
|
+
return base === void 0 ? void 0 : `${base}.${node.property.name}`;
|
|
334
|
+
}
|
|
335
|
+
if (node.type !== "Identifier") return void 0;
|
|
336
|
+
const id = this.scopes.bindingOf.get(node);
|
|
337
|
+
if (id === void 0) return node.name;
|
|
338
|
+
return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
|
|
339
|
+
}
|
|
340
|
+
/** What a library says `receiver:method(...)` is. The last library
|
|
341
|
+
* loaded is asked first, so a project's own library can answer for a
|
|
342
|
+
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
343
|
+
* method call, which is what a value that answers to the method itself
|
|
344
|
+
* wants (`text:upper()`). */
|
|
493
345
|
loweredMethodCall(node) {
|
|
494
346
|
const lowerings = this.options.lowerings;
|
|
495
|
-
if (!lowerings?.
|
|
347
|
+
if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
|
|
496
348
|
const receiver = this.options.types?.typeOf.get(node.object);
|
|
349
|
+
const receiverGlobal = this.globalName(node.object);
|
|
350
|
+
const info = this.lazyArguments(node.arguments);
|
|
497
351
|
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
498
|
-
const
|
|
352
|
+
const plugin = lowerings[i].plugin;
|
|
353
|
+
if (!plugin.methodCall) continue;
|
|
354
|
+
const answer = plugin.methodCall({
|
|
499
355
|
method: node.method.name,
|
|
500
356
|
receiver,
|
|
357
|
+
receiverGlobal,
|
|
501
358
|
argumentCount: node.arguments.length,
|
|
359
|
+
at: this.callSite(node.method),
|
|
360
|
+
get arguments() {
|
|
361
|
+
return info();
|
|
362
|
+
},
|
|
363
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
364
|
+
});
|
|
365
|
+
if (answer) {
|
|
366
|
+
return {
|
|
367
|
+
callee: answer.callee,
|
|
368
|
+
prepend: this.prepended(answer.prepend, i, node),
|
|
369
|
+
passReceiver: answer.passReceiver !== false
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
375
|
+
/** What a library says a call to a global — `print(x)` — is. */
|
|
376
|
+
loweredGlobalCall(node) {
|
|
377
|
+
const lowerings = this.options.lowerings;
|
|
378
|
+
if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
|
|
379
|
+
const name = this.globalName(node.callee);
|
|
380
|
+
if (name === void 0) return void 0;
|
|
381
|
+
const info = this.lazyArguments(node.arguments);
|
|
382
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
383
|
+
const plugin = lowerings[i].plugin;
|
|
384
|
+
if (!plugin.globalCall) continue;
|
|
385
|
+
const answer = plugin.globalCall({
|
|
386
|
+
name,
|
|
387
|
+
at: this.callSite(node.callee),
|
|
388
|
+
get arguments() {
|
|
389
|
+
return info();
|
|
390
|
+
},
|
|
391
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
392
|
+
});
|
|
393
|
+
if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
|
|
394
|
+
}
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
/** What a library says a global read as a value — `local p = print` — is. */
|
|
398
|
+
loweredGlobalValue(node) {
|
|
399
|
+
const lowerings = this.options.lowerings;
|
|
400
|
+
if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
|
|
401
|
+
const name = this.globalName(node);
|
|
402
|
+
if (name === void 0) return void 0;
|
|
403
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
404
|
+
const answer = lowerings[i].plugin.globalValue?.({
|
|
405
|
+
name,
|
|
406
|
+
at: this.callSite(node),
|
|
502
407
|
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
503
408
|
});
|
|
504
|
-
if (answer) return
|
|
409
|
+
if (answer !== void 0) return this.libraryExpression(answer, i, node);
|
|
505
410
|
}
|
|
506
411
|
return void 0;
|
|
507
412
|
}
|
|
413
|
+
lazyArguments(args2) {
|
|
414
|
+
let info;
|
|
415
|
+
return () => info ??= this.argumentInfo(args2);
|
|
416
|
+
}
|
|
417
|
+
prepended(sources, index2, node) {
|
|
418
|
+
return (sources ?? []).flatMap((source) => {
|
|
419
|
+
const expression = this.libraryExpression(source, index2, node);
|
|
420
|
+
return expression ? [expression] : [];
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/** Luau a library wrote as an expression. One that does not parse is the
|
|
424
|
+
* library's mistake, reported at the call it was written for. */
|
|
425
|
+
libraryExpression(source, index2, node) {
|
|
426
|
+
try {
|
|
427
|
+
return (0, import_luau_parser.parseExpressionFromSource)(source);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
508
433
|
helperDefinitions() {
|
|
509
434
|
const out2 = [];
|
|
510
435
|
const assign2 = this.helpers.get("assign");
|
|
@@ -532,12 +457,9 @@ var Lowerer = class {
|
|
|
532
457
|
});
|
|
533
458
|
continue;
|
|
534
459
|
}
|
|
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
460
|
const lines = this.options.module?.lines ?? "nil";
|
|
540
|
-
|
|
461
|
+
const fail = this.options.module?.fail ?? "nil";
|
|
462
|
+
out2.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
|
|
541
463
|
}
|
|
542
464
|
const concat = this.helpers.get("concat");
|
|
543
465
|
if (concat) {
|
|
@@ -706,7 +628,7 @@ var Lowerer = class {
|
|
|
706
628
|
break;
|
|
707
629
|
}
|
|
708
630
|
case "ReturnStatement":
|
|
709
|
-
if (declaration.
|
|
631
|
+
if (declaration.argument) this.report(declaration, "A module cannot return a value; export it instead");
|
|
710
632
|
body.push(returns([]));
|
|
711
633
|
break;
|
|
712
634
|
case "ImportStatement":
|
|
@@ -885,7 +807,7 @@ var Lowerer = class {
|
|
|
885
807
|
case "CallStatement": {
|
|
886
808
|
const chain = optionalChain(node.expression);
|
|
887
809
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
888
|
-
const expression = this.
|
|
810
|
+
const expression = this.callValues(node.expression);
|
|
889
811
|
if (expression.type !== "CallExpression" && expression.type !== "MethodCallExpression") {
|
|
890
812
|
this.report(node, "A statement must be a call");
|
|
891
813
|
return [];
|
|
@@ -938,23 +860,26 @@ var Lowerer = class {
|
|
|
938
860
|
}];
|
|
939
861
|
case "GenericForStatement": {
|
|
940
862
|
const prelude = [];
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (variables.length === 1 && this.options.types?.iteratesValues.has(node)) {
|
|
948
|
-
variables.unshift(this.names.fresh("_"));
|
|
863
|
+
let item;
|
|
864
|
+
if (node.variable.type === "IdentifierPattern") {
|
|
865
|
+
item = this.name(node.variable.name);
|
|
866
|
+
} else {
|
|
867
|
+
item = this.names.fresh("item");
|
|
868
|
+
prelude.push(...this.destructure(node.variable, identifier(item), "declare"));
|
|
949
869
|
}
|
|
870
|
+
const form = this.options.types?.loops.get(node) ?? { walks: "values", viaIter: false };
|
|
871
|
+
let source = this.expression(node.iterator);
|
|
872
|
+
if (form.viaIter) source = methodCall(source, "__iter", []);
|
|
873
|
+
const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
|
|
874
|
+
const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
|
|
950
875
|
return [genericFor(
|
|
951
876
|
variables,
|
|
952
|
-
|
|
877
|
+
iterators,
|
|
953
878
|
this.loopBody51(this.block(node.body, prelude)).statements
|
|
954
879
|
)];
|
|
955
880
|
}
|
|
956
881
|
case "ReturnStatement":
|
|
957
|
-
return [returns(this.
|
|
882
|
+
return [returns(node.argument ? [this.expression(node.argument)] : [])];
|
|
958
883
|
case "BreakStatement":
|
|
959
884
|
return [{ type: "BreakStatement", ...spanOf(node) }];
|
|
960
885
|
case "ContinueStatement":
|
|
@@ -998,7 +923,7 @@ var Lowerer = class {
|
|
|
998
923
|
// end
|
|
999
924
|
// function Dog.speak(this) ... end
|
|
1000
925
|
//
|
|
1001
|
-
// `new
|
|
926
|
+
// `Dog.new(x)` builds one, `dog:speak()` passes the instance
|
|
1002
927
|
// as `this`, and `super.speak()` is `Animal.speak(this)` — the
|
|
1003
928
|
// base's own function, run on this instance.
|
|
1004
929
|
// ============================================================
|
|
@@ -1024,6 +949,7 @@ var Lowerer = class {
|
|
|
1024
949
|
classRuntimeStatements() {
|
|
1025
950
|
if (!this.classHelpers) return [];
|
|
1026
951
|
const { build, accessors } = this.classHelpers;
|
|
952
|
+
const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
|
|
1027
953
|
return (0, import_luau_parser.parse)(`
|
|
1028
954
|
local function ${build}(base)
|
|
1029
955
|
local class = { __getters = {}, __setters = {} }
|
|
@@ -1034,6 +960,9 @@ local function ${build}(base)
|
|
|
1034
960
|
setmetatable(class, { __index = base })
|
|
1035
961
|
setmetatable(class.__getters, { __index = base.__getters })
|
|
1036
962
|
setmetatable(class.__setters, { __index = base.__setters })
|
|
963
|
+
for _, key in ipairs(${metamethods}) do
|
|
964
|
+
class[key] = base[key]
|
|
965
|
+
end
|
|
1037
966
|
end
|
|
1038
967
|
return class
|
|
1039
968
|
end
|
|
@@ -1096,6 +1025,7 @@ end
|
|
|
1096
1025
|
}
|
|
1097
1026
|
break;
|
|
1098
1027
|
case "ClassMethod":
|
|
1028
|
+
if (member2.isAbstract) break;
|
|
1099
1029
|
out2.push(this.functionStatement(
|
|
1100
1030
|
member(self, member2.name.name),
|
|
1101
1031
|
member2,
|
|
@@ -1117,7 +1047,7 @@ end
|
|
|
1117
1047
|
}
|
|
1118
1048
|
}
|
|
1119
1049
|
out2.push(this.classInit(node, self));
|
|
1120
|
-
out2.push(this.classNew(node, self));
|
|
1050
|
+
if (!node.isAbstract) out2.push(this.classNew(node, self));
|
|
1121
1051
|
out2.push(callStatement(call(identifier(accessors), [self])));
|
|
1122
1052
|
} finally {
|
|
1123
1053
|
this.classContext = previous;
|
|
@@ -1395,7 +1325,11 @@ end
|
|
|
1395
1325
|
}
|
|
1396
1326
|
switch (node.type) {
|
|
1397
1327
|
case "Identifier":
|
|
1398
|
-
|
|
1328
|
+
if (node.name === "scriptArgs" && this.isLanguageGlobal(node)) {
|
|
1329
|
+
this.usesScriptArgs = true;
|
|
1330
|
+
return identifier("scriptArgs");
|
|
1331
|
+
}
|
|
1332
|
+
return this.loweredGlobalValue(node) ?? this.reference(node);
|
|
1399
1333
|
case "NilLiteral":
|
|
1400
1334
|
return nil();
|
|
1401
1335
|
case "BooleanLiteral":
|
|
@@ -1404,8 +1338,6 @@ end
|
|
|
1404
1338
|
return number(node.value, this.numberRaw(node));
|
|
1405
1339
|
case "StringLiteral":
|
|
1406
1340
|
return string(node.value);
|
|
1407
|
-
case "VarargExpression":
|
|
1408
|
-
return vararg();
|
|
1409
1341
|
// Only a recovering parse makes one, and a syntax error stops the build.
|
|
1410
1342
|
case "ErrorExpression":
|
|
1411
1343
|
this.report(node, "Syntax error");
|
|
@@ -1426,11 +1358,6 @@ end
|
|
|
1426
1358
|
}
|
|
1427
1359
|
case "UnaryExpression":
|
|
1428
1360
|
return unary(node.operator, this.expression(node.argument));
|
|
1429
|
-
case "NewExpression":
|
|
1430
|
-
return call(
|
|
1431
|
-
member(this.expression(node.callee), "new"),
|
|
1432
|
-
this.values(node.arguments)
|
|
1433
|
-
);
|
|
1434
1361
|
case "SuperExpression":
|
|
1435
1362
|
return this.superClassReference(node);
|
|
1436
1363
|
case "SpreadElement":
|
|
@@ -1438,12 +1365,15 @@ end
|
|
|
1438
1365
|
return nil();
|
|
1439
1366
|
case "ClassExpression":
|
|
1440
1367
|
return this.classExpression(node);
|
|
1441
|
-
case "MemberExpression":
|
|
1442
|
-
case "IndexExpression":
|
|
1443
1368
|
case "CallExpression":
|
|
1444
|
-
case "MethodCallExpression":
|
|
1369
|
+
case "MethodCallExpression":
|
|
1370
|
+
return this.callValues(node);
|
|
1371
|
+
case "MemberExpression":
|
|
1372
|
+
case "IndexExpression": {
|
|
1445
1373
|
const chain = optionalChain(node);
|
|
1446
1374
|
if (chain) return this.optionalChainExpression(chain);
|
|
1375
|
+
const global = node.type === "MemberExpression" ? this.loweredGlobalValue(node) : void 0;
|
|
1376
|
+
if (global) return global;
|
|
1447
1377
|
return this.link(node, this.expression(linkObject(node)));
|
|
1448
1378
|
}
|
|
1449
1379
|
case "ParenthesizedExpression":
|
|
@@ -1463,14 +1393,47 @@ end
|
|
|
1463
1393
|
};
|
|
1464
1394
|
}
|
|
1465
1395
|
}
|
|
1396
|
+
/** A call. */
|
|
1397
|
+
callValues(node) {
|
|
1398
|
+
const chain = optionalChain(node);
|
|
1399
|
+
if (chain) return this.optionalChainExpression(chain);
|
|
1400
|
+
const object = linkObject(node);
|
|
1401
|
+
if (node.type === "CallExpression" && object.type === "Identifier") {
|
|
1402
|
+
return this.link(node, this.reference(object));
|
|
1403
|
+
}
|
|
1404
|
+
return this.link(node, this.expression(object));
|
|
1405
|
+
}
|
|
1406
|
+
/** A call is one value in tilua, and Luau would hand on every value one
|
|
1407
|
+
* in the last place of an argument list or an array returns. Types say
|
|
1408
|
+
* how many a call returns; where a type says nothing — `any`, `unknown`
|
|
1409
|
+
* — the call is kept to one: `f((g()))`. */
|
|
1410
|
+
keptToOne(nodes, values) {
|
|
1411
|
+
const node = nodes[nodes.length - 1];
|
|
1412
|
+
const last = values[values.length - 1];
|
|
1413
|
+
if (!node || !last || !expandsToMany(last)) return values;
|
|
1414
|
+
const type = this.options.types?.typeOf.get(node);
|
|
1415
|
+
if (type && type.kind !== "any" && type.kind !== "unknown") return values;
|
|
1416
|
+
return [...values.slice(0, -1), parenthesized(last)];
|
|
1417
|
+
}
|
|
1418
|
+
/** Is this `scriptArgs` the language's own, not a name the file declared? */
|
|
1419
|
+
isLanguageGlobal(node) {
|
|
1420
|
+
const id = this.bindingIdOf(node);
|
|
1421
|
+
return id === void 0 || this.scopes.bindings.get(id)?.kind === "global";
|
|
1422
|
+
}
|
|
1466
1423
|
/** A list of values — a call's arguments, a `return`'s, a declaration's,
|
|
1467
1424
|
* an assignment's. `f(a, ...xs)` is Lua's own last-value expansion,
|
|
1468
1425
|
* `f(a, table.unpack(xs))`; a spread anywhere else cannot be, since only
|
|
1469
1426
|
* the last value of a list expands, so the whole list is built as an
|
|
1470
|
-
* array first and that is what expands.
|
|
1471
|
-
|
|
1427
|
+
* array first and that is what expands.
|
|
1428
|
+
*
|
|
1429
|
+
* `arguments` is set for a call's arguments, where Luau hands on every
|
|
1430
|
+
* value the last one returns — see `keptToOne`. */
|
|
1431
|
+
values(list, asArguments = false) {
|
|
1472
1432
|
const spreads = list.filter((a) => a.type === "SpreadElement");
|
|
1473
|
-
if (!spreads.length)
|
|
1433
|
+
if (!spreads.length) {
|
|
1434
|
+
const lowered = list.map((a) => this.expression(a));
|
|
1435
|
+
return asArguments ? this.keptToOne(list, lowered) : lowered;
|
|
1436
|
+
}
|
|
1474
1437
|
const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
|
|
1475
1438
|
const last = list[list.length - 1];
|
|
1476
1439
|
if (spreads.length === 1 && last.type === "SpreadElement") {
|
|
@@ -1489,15 +1452,21 @@ end
|
|
|
1489
1452
|
return member(object, node.property.name);
|
|
1490
1453
|
case "IndexExpression":
|
|
1491
1454
|
return index(object, this.expression(node.index));
|
|
1492
|
-
case "CallExpression":
|
|
1493
|
-
|
|
1455
|
+
case "CallExpression": {
|
|
1456
|
+
const lowered = this.loweredGlobalCall(node);
|
|
1457
|
+
const args2 = this.values(node.arguments, true);
|
|
1458
|
+
if (!lowered) return call(object, args2);
|
|
1459
|
+
return call(calleePath(lowered.callee), [...lowered.prepend, ...args2]);
|
|
1460
|
+
}
|
|
1494
1461
|
case "MethodCallExpression": {
|
|
1495
|
-
const consoleCall = this.consoleCall(node);
|
|
1496
|
-
if (consoleCall) return consoleCall;
|
|
1497
|
-
const args2 = this.values(node.arguments);
|
|
1498
1462
|
const lowered = this.loweredMethodCall(node);
|
|
1463
|
+
const args2 = this.values(node.arguments, true);
|
|
1499
1464
|
if (lowered) {
|
|
1500
|
-
return call(calleePath(lowered.callee),
|
|
1465
|
+
return call(calleePath(lowered.callee), [
|
|
1466
|
+
...lowered.prepend,
|
|
1467
|
+
...lowered.passReceiver ? [object] : [],
|
|
1468
|
+
...args2
|
|
1469
|
+
]);
|
|
1501
1470
|
}
|
|
1502
1471
|
if (!isLuauName(node.method.name)) {
|
|
1503
1472
|
this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
|
|
@@ -1725,7 +1694,8 @@ end
|
|
|
1725
1694
|
* runs of plain elements and the spread arrays are joined by `concat`. */
|
|
1726
1695
|
arrayExpression(node) {
|
|
1727
1696
|
if (!node.elements.some((e) => e.type === "SpreadElement")) {
|
|
1728
|
-
|
|
1697
|
+
const elements = node.elements;
|
|
1698
|
+
return table(this.keptToOne(elements, elements.map((e) => this.expression(e))).map((value) => ({ type: "TableFieldPositional", value })));
|
|
1729
1699
|
}
|
|
1730
1700
|
const parts = [];
|
|
1731
1701
|
let current = [];
|
|
@@ -1910,10 +1880,6 @@ function mentions(expression, name) {
|
|
|
1910
1880
|
walk(expression);
|
|
1911
1881
|
return found;
|
|
1912
1882
|
}
|
|
1913
|
-
function isFunctionType(type) {
|
|
1914
|
-
if (type.kind === "function") return true;
|
|
1915
|
-
return type.kind === "intersection" && type.types.some((t) => t.kind === "function");
|
|
1916
|
-
}
|
|
1917
1883
|
function allNodes(root) {
|
|
1918
1884
|
const out2 = [];
|
|
1919
1885
|
const visit = (value) => {
|
|
@@ -1928,6 +1894,24 @@ function allNodes(root) {
|
|
|
1928
1894
|
visit(root);
|
|
1929
1895
|
return out2;
|
|
1930
1896
|
}
|
|
1897
|
+
var CLASS_METAMETHODS = [
|
|
1898
|
+
"__add",
|
|
1899
|
+
"__sub",
|
|
1900
|
+
"__mul",
|
|
1901
|
+
"__div",
|
|
1902
|
+
"__idiv",
|
|
1903
|
+
"__mod",
|
|
1904
|
+
"__pow",
|
|
1905
|
+
"__unm",
|
|
1906
|
+
"__concat",
|
|
1907
|
+
"__len",
|
|
1908
|
+
"__eq",
|
|
1909
|
+
"__lt",
|
|
1910
|
+
"__le",
|
|
1911
|
+
"__call",
|
|
1912
|
+
"__tostring",
|
|
1913
|
+
"__iter"
|
|
1914
|
+
];
|
|
1931
1915
|
|
|
1932
1916
|
// src/linemap.ts
|
|
1933
1917
|
function statementsOf(node) {
|
|
@@ -2037,16 +2021,19 @@ async function bundle(options) {
|
|
|
2037
2021
|
const file = pending.shift();
|
|
2038
2022
|
if (modules.has(file)) continue;
|
|
2039
2023
|
const source = sources.get(file);
|
|
2024
|
+
const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
|
|
2040
2025
|
const lowered = lower(source.program, source.scopes, {
|
|
2041
2026
|
names,
|
|
2042
2027
|
types: analysis.types.get(file),
|
|
2043
2028
|
lowerings,
|
|
2044
2029
|
target: options.target ?? config2?.target,
|
|
2045
2030
|
source: source.text,
|
|
2031
|
+
file: shown,
|
|
2046
2032
|
module: {
|
|
2047
2033
|
name: source.name,
|
|
2048
2034
|
require: requireExpression,
|
|
2049
2035
|
lines: `${G}.lines`,
|
|
2036
|
+
fail: `${G}.fail`,
|
|
2050
2037
|
resolve: (specifier) => {
|
|
2051
2038
|
const target = (0, import_parser3.resolveModulePath)(file, specifier, config2);
|
|
2052
2039
|
if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
|
|
@@ -2056,7 +2043,6 @@ async function bundle(options) {
|
|
|
2056
2043
|
}
|
|
2057
2044
|
});
|
|
2058
2045
|
for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
|
|
2059
|
-
const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
|
|
2060
2046
|
for (const statement of allStatements(lowered.statements)) {
|
|
2061
2047
|
const line = lowered.origins.get(statement);
|
|
2062
2048
|
if (line !== void 0) origins.set(statement, { file: shown, line });
|
|
@@ -2065,7 +2051,8 @@ async function bundle(options) {
|
|
|
2065
2051
|
name: source.name,
|
|
2066
2052
|
statements: lowered.statements,
|
|
2067
2053
|
exportsName: lowered.exportsName,
|
|
2068
|
-
info: lowered.module
|
|
2054
|
+
info: lowered.module,
|
|
2055
|
+
usesScriptArgs: lowered.usesScriptArgs
|
|
2069
2056
|
});
|
|
2070
2057
|
}
|
|
2071
2058
|
for (const [file, fileDirectives] of directives) {
|
|
@@ -2090,6 +2077,9 @@ async function bundle(options) {
|
|
|
2090
2077
|
}
|
|
2091
2078
|
const program = (0, import_luau_parser2.parse)(runtime(G));
|
|
2092
2079
|
const assignment = program.body.statements[1];
|
|
2080
|
+
if ([...modules.values()].some((m) => m.usesScriptArgs)) {
|
|
2081
|
+
program.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
|
|
2082
|
+
}
|
|
2093
2083
|
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;
|
|
2094
2084
|
if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
|
|
2095
2085
|
for (const module2 of modules.values()) {
|
|
@@ -2106,11 +2096,21 @@ async function bundle(options) {
|
|
|
2106
2096
|
fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
|
|
2107
2097
|
modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
|
|
2108
2098
|
}
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2099
|
+
const ok = names.fresh("ok");
|
|
2100
|
+
const result = names.fresh("result");
|
|
2101
|
+
const entryStatements = (0, import_luau_parser2.parse)(`
|
|
2102
|
+
local ${ok}, ${result} = xpcall(function()
|
|
2103
|
+
return ${G}.require(${JSON.stringify(sources.get(entry2).name)})
|
|
2104
|
+
end, ${G}.fail)
|
|
2105
|
+
if not ${ok} then
|
|
2106
|
+
error(${result}, 0)
|
|
2107
|
+
end
|
|
2108
|
+
${modules.get(entry2).info.exports ? `return ${result}` : ""}
|
|
2109
|
+
`).body.statements;
|
|
2110
|
+
program.body.statements.push(...entryStatements);
|
|
2111
2111
|
const map = buildLineMap(program, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program)), origins);
|
|
2112
2112
|
if (map.size) {
|
|
2113
|
-
program.body.statements.splice(program.body.statements.length -
|
|
2113
|
+
program.body.statements.splice(program.body.statements.length - entryStatements.length, 0, assign(
|
|
2114
2114
|
[member(identifier(G), "lines")],
|
|
2115
2115
|
[(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
|
|
2116
2116
|
));
|
|
@@ -2122,6 +2122,52 @@ function runtime(G) {
|
|
|
2122
2122
|
local ${G}
|
|
2123
2123
|
${G} = {
|
|
2124
2124
|
modules = {},
|
|
2125
|
+
-- Filled in just before the entry runs: bundle line -> { file, line }.
|
|
2126
|
+
lines = nil,
|
|
2127
|
+
-- What an error nothing caught says, in the project's files: the bundle's
|
|
2128
|
+
-- own positions in the message are replaced by the places they came from,
|
|
2129
|
+
-- and a traceback of those places follows. A message that is not a string
|
|
2130
|
+
-- is left as it is, and a position that is not this bundle's (a library
|
|
2131
|
+
-- that raised its error with one already mapped) is left alone.
|
|
2132
|
+
fail = function(message)
|
|
2133
|
+
if type(message) ~= "string" then
|
|
2134
|
+
return message
|
|
2135
|
+
end
|
|
2136
|
+
local lines = ${G}.lines
|
|
2137
|
+
if lines == nil then
|
|
2138
|
+
return debug.traceback(message, 2)
|
|
2139
|
+
end
|
|
2140
|
+
local chunk
|
|
2141
|
+
if debug.info ~= nil then
|
|
2142
|
+
chunk = debug.info(1, "s")
|
|
2143
|
+
else
|
|
2144
|
+
chunk = debug.getinfo(1, "S").short_src
|
|
2145
|
+
end
|
|
2146
|
+
local prefix = chunk:gsub("%p", "%%%0")
|
|
2147
|
+
local function place(line)
|
|
2148
|
+
local origin = lines[tonumber(line)]
|
|
2149
|
+
if origin == nil then
|
|
2150
|
+
return nil
|
|
2151
|
+
end
|
|
2152
|
+
return origin[1] .. ":" .. tostring(origin[2])
|
|
2153
|
+
end
|
|
2154
|
+
local mapped = message:gsub(prefix .. ":(%d+)", function(line)
|
|
2155
|
+
return place(line)
|
|
2156
|
+
end)
|
|
2157
|
+
local frames = {}
|
|
2158
|
+
for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
|
|
2159
|
+
local line = frame:match(prefix .. ":(%d+)")
|
|
2160
|
+
local at = line ~= nil and place(line) or nil
|
|
2161
|
+
if at ~= nil then
|
|
2162
|
+
local name = frame:match("function ([%w_.:]+)")
|
|
2163
|
+
frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
2164
|
+
end
|
|
2165
|
+
end
|
|
2166
|
+
if #frames == 0 then
|
|
2167
|
+
return mapped
|
|
2168
|
+
end
|
|
2169
|
+
return mapped .. "\\n" .. table.concat(frames, "\\n")
|
|
2170
|
+
end,
|
|
2125
2171
|
records = {},
|
|
2126
2172
|
require = function(name)
|
|
2127
2173
|
local record = ${G}.records[name]
|