@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
|
@@ -11,7 +11,7 @@ function resolveConfig(input, entry, cwd = process.cwd()) {
|
|
|
11
11
|
|
|
12
12
|
// src/lower.ts
|
|
13
13
|
import { formatType } from "@tilua/parser";
|
|
14
|
-
import { parse as parseLuau } from "luau-parser";
|
|
14
|
+
import { parse as parseLuau, parseExpressionFromSource } from "luau-parser";
|
|
15
15
|
|
|
16
16
|
// src/luau.ts
|
|
17
17
|
var NO_SPAN = { line: { start: 0, end: 0 }, column: { start: 0, end: 0 } };
|
|
@@ -101,144 +101,6 @@ function isLuauName(name) {
|
|
|
101
101
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !LUAU_KEYWORDS.has(name);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// src/console.ts
|
|
105
|
-
var CONSOLE_RUNTIME = `
|
|
106
|
-
local __NAME__ = (function()
|
|
107
|
-
local console = {}
|
|
108
|
-
local MAX_DEPTH = 4
|
|
109
|
-
local ESCAPES = { ["\\n"] = "\\\\n", ["\\r"] = "\\\\r", ["\\t"] = "\\\\t", ["\\""] = "\\\\\\"", ["\\\\"] = "\\\\\\\\" }
|
|
110
|
-
|
|
111
|
-
local function quote(text)
|
|
112
|
-
return '"' .. text:gsub('[%c"\\\\]', function(c) return ESCAPES[c] or ("\\\\%d"):format(c:byte()) end) .. '"'
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
-- A table with 1..n and nothing else prints as a list, as it was written.
|
|
116
|
-
local function isArray(value)
|
|
117
|
-
local count = 0
|
|
118
|
-
for key in pairs(value) do
|
|
119
|
-
if type(key) ~= "number" then return false end
|
|
120
|
-
count = count + 1
|
|
121
|
-
end
|
|
122
|
-
return count == #value
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
local function isIdentifier(key)
|
|
126
|
-
return type(key) == "string" and key:match("^[A-Za-z_][A-Za-z0-9_]*$") ~= nil
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
local render
|
|
130
|
-
|
|
131
|
-
local function renderTable(value, meta, seen, depth, indent)
|
|
132
|
-
if seen[value] then return "<cycle>" end
|
|
133
|
-
if depth > MAX_DEPTH then return "..." end
|
|
134
|
-
seen[value] = true
|
|
135
|
-
local inner = indent .. " "
|
|
136
|
-
local parts = {}
|
|
137
|
-
if isArray(value) then
|
|
138
|
-
for i = 1, #value do
|
|
139
|
-
parts[#parts + 1] = inner .. render(value[i], nil, seen, depth + 1, inner)
|
|
140
|
-
end
|
|
141
|
-
else
|
|
142
|
-
local keys = {}
|
|
143
|
-
for key in pairs(value) do keys[#keys + 1] = key end
|
|
144
|
-
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
|
145
|
-
for _, key in ipairs(keys) do
|
|
146
|
-
local name = isIdentifier(key) and key or ("[" .. render(key, nil, seen, depth + 1, inner) .. "]")
|
|
147
|
-
parts[#parts + 1] = inner .. name .. " = " .. render(value[key], nil, seen, depth + 1, inner)
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
seen[value] = nil
|
|
151
|
-
if #parts == 0 then return "{}" end
|
|
152
|
-
local open = isArray(value) and "[" or "{"
|
|
153
|
-
local close = isArray(value) and "]" or "}"
|
|
154
|
-
return open .. "\\n" .. table.concat(parts, ",\\n") .. "\\n" .. indent .. close
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
-- \`meta\` is what the compiler knew about this value and the runtime cannot:
|
|
158
|
-
-- a function's type, and the code it was written as.
|
|
159
|
-
render = function(value, meta, seen, depth, indent)
|
|
160
|
-
local kind = type(value)
|
|
161
|
-
if kind == "string" then return quote(value) end
|
|
162
|
-
if kind == "number" or kind == "boolean" or kind == "nil" then return tostring(value) end
|
|
163
|
-
if kind == "function" then
|
|
164
|
-
if meta == nil then return "function" end
|
|
165
|
-
if meta[2] == nil or meta[2] == "" then return meta[1] end
|
|
166
|
-
return meta[1] .. " " .. meta[2]
|
|
167
|
-
end
|
|
168
|
-
if kind == "table" then
|
|
169
|
-
local shown = rawget(value, "__tostring") == nil and getmetatable(value)
|
|
170
|
-
-- A value with its own \`__tostring\` (a Roblox Vector3, a class
|
|
171
|
-
-- instance that says how it reads) is trusted to describe itself.
|
|
172
|
-
if shown ~= nil and rawget(shown, "__tostring") ~= nil then return tostring(value) end
|
|
173
|
-
return renderTable(value, meta, seen, depth, indent)
|
|
174
|
-
end
|
|
175
|
-
return tostring(value)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
local function describe(meta, index, value)
|
|
179
|
-
return render(value, meta ~= nil and meta[index] or nil, {}, 1, "")
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
local function join(meta, count, ...)
|
|
183
|
-
local parts = {}
|
|
184
|
-
for i = 1, count do
|
|
185
|
-
parts[#parts + 1] = describe(meta, i, (select(i, ...)))
|
|
186
|
-
end
|
|
187
|
-
return table.concat(parts, " ")
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
-- A line of the bundle, as the place in the project that wrote it.
|
|
191
|
-
local lines = __LINES__
|
|
192
|
-
local function place(line)
|
|
193
|
-
local origin = lines ~= nil and lines[tonumber(line)]
|
|
194
|
-
if origin == nil then return nil end
|
|
195
|
-
return origin[1] .. ":" .. tostring(origin[2])
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
-- Luau names the bundle in every frame; the project's files are what the
|
|
199
|
-
-- reader knows. Frames the map has nothing for are dropped: they are this
|
|
200
|
-
-- runtime's own, and the bundle's.
|
|
201
|
-
local function trace(level)
|
|
202
|
-
local raw = debug.traceback(nil, level)
|
|
203
|
-
local out = {}
|
|
204
|
-
for frame in raw:gmatch("[^\\n]+") do
|
|
205
|
-
local line = frame:match(":(%d+)")
|
|
206
|
-
local at = line ~= nil and place(line) or nil
|
|
207
|
-
if at ~= nil then
|
|
208
|
-
local name = frame:match("function ([%w_.:]+)")
|
|
209
|
-
out[#out + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
return table.concat(out, "\\n")
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
-- Never on the caller's thread: reporting a problem must not become one.
|
|
216
|
-
local detach = (task ~= nil and task.spawn) or function(f) return f() end
|
|
217
|
-
|
|
218
|
-
local function raise(prefix, meta, count, ...)
|
|
219
|
-
local message = prefix .. join(meta, count, ...)
|
|
220
|
-
local where = trace(3)
|
|
221
|
-
detach(function()
|
|
222
|
-
error(where ~= "" and (message .. "\\n" .. where) or message, 0)
|
|
223
|
-
end)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
function console.log(meta, ...)
|
|
227
|
-
print(join(meta, select("#", ...), ...))
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
function console.warn(meta, ...)
|
|
231
|
-
raise("[warn] ", meta, select("#", ...), ...)
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
function console.error(meta, ...)
|
|
235
|
-
raise("", meta, select("#", ...), ...)
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
return console
|
|
239
|
-
end)()
|
|
240
|
-
`;
|
|
241
|
-
|
|
242
104
|
// src/names.ts
|
|
243
105
|
var Names = class _Names {
|
|
244
106
|
constructor(taken) {
|
|
@@ -318,14 +180,18 @@ var Lowerer = class {
|
|
|
318
180
|
const body = this.options.module ? this.module(this.options.module) : this.topLevel(this.source.body.statements);
|
|
319
181
|
const helpers = this.helperDefinitions();
|
|
320
182
|
const captured = [...this.builtins].map(([global, local2]) => local([local2], [identifier(global)]));
|
|
183
|
+
const scriptArgs = this.usesScriptArgs && !this.options.module ? [local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: vararg() }])])] : [];
|
|
321
184
|
return {
|
|
322
|
-
statements: [...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
185
|
+
statements: [...scriptArgs, ...captured, ...helpers, ...this.classRuntimeStatements(), ...body],
|
|
323
186
|
exportsName: this.exportsName,
|
|
324
187
|
module: this.info,
|
|
325
188
|
diagnostics: this.diagnostics,
|
|
326
|
-
origins: this.origins
|
|
189
|
+
origins: this.origins,
|
|
190
|
+
usesScriptArgs: this.usesScriptArgs
|
|
327
191
|
};
|
|
328
192
|
}
|
|
193
|
+
/** Whether the file reads `scriptArgs`, the language's own global. */
|
|
194
|
+
usesScriptArgs = false;
|
|
329
195
|
report(node, message) {
|
|
330
196
|
this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
|
|
331
197
|
}
|
|
@@ -386,60 +252,9 @@ var Lowerer = class {
|
|
|
386
252
|
}
|
|
387
253
|
loweringNames = /* @__PURE__ */ new Map();
|
|
388
254
|
loweringUsed = [];
|
|
389
|
-
/** What a library says `receiver:method(...)` is. The last library
|
|
390
|
-
* loaded is asked first, so a project's own library can answer for a
|
|
391
|
-
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
392
|
-
* method call, which is what a value that answers to the method itself
|
|
393
|
-
* wants (`text:upper()`). */
|
|
394
|
-
/** `console:log(a, f)` and its two siblings.
|
|
395
|
-
*
|
|
396
|
-
* Handled here rather than through a library's lowering because what they
|
|
397
|
-
* need is not a different callee but *more than was written*: a function
|
|
398
|
-
* prints as its type and its code, and neither survives to runtime. The
|
|
399
|
-
* compiler is the only thing that has both, so it passes them along:
|
|
400
|
-
*
|
|
401
|
-
* console:log(a, double)
|
|
402
|
-
* -- becomes
|
|
403
|
-
* tilua_console.log({ [2] = { "(x: number) => number", "(x: number) => x * 2" } }, a, double)
|
|
404
|
-
*
|
|
405
|
-
* The meta table is `nil` when nothing was worth saying, which is most
|
|
406
|
-
* calls. `console` itself is never passed: it holds nothing. */
|
|
407
|
-
consoleCall(node) {
|
|
408
|
-
if (node.object.type !== "Identifier" || node.object.name !== "console") return void 0;
|
|
409
|
-
const method = node.method.name;
|
|
410
|
-
if (method !== "log" && method !== "warn" && method !== "error") return void 0;
|
|
411
|
-
const binding = this.scopes.bindingOf.get(node.object);
|
|
412
|
-
if (binding !== void 0 && this.scopes.bindings.get(binding)?.kind !== "global") return void 0;
|
|
413
|
-
const meta = this.consoleMeta(node.arguments);
|
|
414
|
-
return call(
|
|
415
|
-
member(this.helper("console"), method),
|
|
416
|
-
[meta ?? nil(), ...this.values(node.arguments)]
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
/** What the compiler knows about the arguments and the runtime cannot:
|
|
420
|
-
* for each function argument, the type it was inferred as and the code it
|
|
421
|
-
* was written as. Keyed by position, so an argument that needs nothing
|
|
422
|
-
* costs nothing. */
|
|
423
|
-
consoleMeta(args) {
|
|
424
|
-
const fields = [];
|
|
425
|
-
args.forEach((argument, index2) => {
|
|
426
|
-
const type = this.options.types?.typeOf.get(argument);
|
|
427
|
-
if (!type || !isFunctionType(type)) return;
|
|
428
|
-
const written = this.writtenSource(this.declarationOf(argument) ?? argument);
|
|
429
|
-
fields.push({
|
|
430
|
-
type: "TableFieldComputed",
|
|
431
|
-
key: number(index2 + 1),
|
|
432
|
-
value: table([
|
|
433
|
-
{ type: "TableFieldPositional", value: string(formatType(type)) },
|
|
434
|
-
{ type: "TableFieldPositional", value: string(written) }
|
|
435
|
-
])
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
return fields.length ? table(fields) : void 0;
|
|
439
|
-
}
|
|
440
255
|
/** What a name was declared as: the value of `const f = ...`, or the whole
|
|
441
|
-
* `function f() { ... }`. Built once, on the first
|
|
442
|
-
*
|
|
256
|
+
* `function f() { ... }`. Built once, on the first call a library asks
|
|
257
|
+
* about — most files have none. */
|
|
443
258
|
declarationOf(argument) {
|
|
444
259
|
if (argument.type !== "Identifier") return void 0;
|
|
445
260
|
if (!this.declarations) {
|
|
@@ -478,21 +293,131 @@ var Lowerer = class {
|
|
|
478
293
|
const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
|
|
479
294
|
return [first, ...middle.map(trim), trim(last)].join("\n");
|
|
480
295
|
}
|
|
296
|
+
/** Where `node` was written, as a library is told it. */
|
|
297
|
+
callSite(node) {
|
|
298
|
+
return { file: this.options.file, line: node.line.start, column: node.column.start };
|
|
299
|
+
}
|
|
300
|
+
/** What the compiler knows about each argument and the runtime cannot:
|
|
301
|
+
* its type, and the code it — or what it names — was written as. Built
|
|
302
|
+
* only if a library reads it. */
|
|
303
|
+
argumentInfo(args) {
|
|
304
|
+
return args.map((argument) => {
|
|
305
|
+
const declaration = this.declarationOf(argument);
|
|
306
|
+
const type = this.options.types?.typeOf.get(argument);
|
|
307
|
+
return {
|
|
308
|
+
type,
|
|
309
|
+
typeText: type ? formatType(type) : void 0,
|
|
310
|
+
source: this.writtenSource(argument),
|
|
311
|
+
declaration: declaration ? this.writtenSource(declaration) : void 0,
|
|
312
|
+
spread: argument.type === "SpreadElement"
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** The name a global is read by at `node`, or `undefined` when `node` is
|
|
317
|
+
* not a global — a local of the same name is the author's. */
|
|
318
|
+
globalName(node) {
|
|
319
|
+
if (node.type === "MemberExpression" && !node.optional) {
|
|
320
|
+
const base = this.globalName(node.object);
|
|
321
|
+
return base === void 0 ? void 0 : `${base}.${node.property.name}`;
|
|
322
|
+
}
|
|
323
|
+
if (node.type !== "Identifier") return void 0;
|
|
324
|
+
const id = this.scopes.bindingOf.get(node);
|
|
325
|
+
if (id === void 0) return node.name;
|
|
326
|
+
return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
|
|
327
|
+
}
|
|
328
|
+
/** What a library says `receiver:method(...)` is. The last library
|
|
329
|
+
* loaded is asked first, so a project's own library can answer for a
|
|
330
|
+
* method an earlier one also claims; no answer leaves an ordinary Luau
|
|
331
|
+
* method call, which is what a value that answers to the method itself
|
|
332
|
+
* wants (`text:upper()`). */
|
|
481
333
|
loweredMethodCall(node) {
|
|
482
334
|
const lowerings = this.options.lowerings;
|
|
483
|
-
if (!lowerings?.
|
|
335
|
+
if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
|
|
484
336
|
const receiver = this.options.types?.typeOf.get(node.object);
|
|
337
|
+
const receiverGlobal = this.globalName(node.object);
|
|
338
|
+
const info = this.lazyArguments(node.arguments);
|
|
485
339
|
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
486
|
-
const
|
|
340
|
+
const plugin = lowerings[i].plugin;
|
|
341
|
+
if (!plugin.methodCall) continue;
|
|
342
|
+
const answer = plugin.methodCall({
|
|
487
343
|
method: node.method.name,
|
|
488
344
|
receiver,
|
|
345
|
+
receiverGlobal,
|
|
489
346
|
argumentCount: node.arguments.length,
|
|
347
|
+
at: this.callSite(node.method),
|
|
348
|
+
get arguments() {
|
|
349
|
+
return info();
|
|
350
|
+
},
|
|
351
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
352
|
+
});
|
|
353
|
+
if (answer) {
|
|
354
|
+
return {
|
|
355
|
+
callee: answer.callee,
|
|
356
|
+
prepend: this.prepended(answer.prepend, i, node),
|
|
357
|
+
passReceiver: answer.passReceiver !== false
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
/** What a library says a call to a global — `print(x)` — is. */
|
|
364
|
+
loweredGlobalCall(node) {
|
|
365
|
+
const lowerings = this.options.lowerings;
|
|
366
|
+
if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
|
|
367
|
+
const name = this.globalName(node.callee);
|
|
368
|
+
if (name === void 0) return void 0;
|
|
369
|
+
const info = this.lazyArguments(node.arguments);
|
|
370
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
371
|
+
const plugin = lowerings[i].plugin;
|
|
372
|
+
if (!plugin.globalCall) continue;
|
|
373
|
+
const answer = plugin.globalCall({
|
|
374
|
+
name,
|
|
375
|
+
at: this.callSite(node.callee),
|
|
376
|
+
get arguments() {
|
|
377
|
+
return info();
|
|
378
|
+
},
|
|
379
|
+
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
380
|
+
});
|
|
381
|
+
if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
/** What a library says a global read as a value — `local p = print` — is. */
|
|
386
|
+
loweredGlobalValue(node) {
|
|
387
|
+
const lowerings = this.options.lowerings;
|
|
388
|
+
if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
|
|
389
|
+
const name = this.globalName(node);
|
|
390
|
+
if (name === void 0) return void 0;
|
|
391
|
+
for (let i = lowerings.length - 1; i >= 0; i--) {
|
|
392
|
+
const answer = lowerings[i].plugin.globalValue?.({
|
|
393
|
+
name,
|
|
394
|
+
at: this.callSite(node),
|
|
490
395
|
use: (runtime2) => this.loweringRuntime(i, runtime2)
|
|
491
396
|
});
|
|
492
|
-
if (answer) return
|
|
397
|
+
if (answer !== void 0) return this.libraryExpression(answer, i, node);
|
|
493
398
|
}
|
|
494
399
|
return void 0;
|
|
495
400
|
}
|
|
401
|
+
lazyArguments(args) {
|
|
402
|
+
let info;
|
|
403
|
+
return () => info ??= this.argumentInfo(args);
|
|
404
|
+
}
|
|
405
|
+
prepended(sources, index2, node) {
|
|
406
|
+
return (sources ?? []).flatMap((source) => {
|
|
407
|
+
const expression = this.libraryExpression(source, index2, node);
|
|
408
|
+
return expression ? [expression] : [];
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** Luau a library wrote as an expression. One that does not parse is the
|
|
412
|
+
* library's mistake, reported at the call it was written for. */
|
|
413
|
+
libraryExpression(source, index2, node) {
|
|
414
|
+
try {
|
|
415
|
+
return parseExpressionFromSource(source);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
496
421
|
helperDefinitions() {
|
|
497
422
|
const out = [];
|
|
498
423
|
const assign2 = this.helpers.get("assign");
|
|
@@ -520,12 +445,9 @@ var Lowerer = class {
|
|
|
520
445
|
});
|
|
521
446
|
continue;
|
|
522
447
|
}
|
|
523
|
-
out.push(...parseLuau(source.replace(/__NAME__/g, name)).body.statements);
|
|
524
|
-
}
|
|
525
|
-
const console = this.helpers.get("console");
|
|
526
|
-
if (console) {
|
|
527
448
|
const lines = this.options.module?.lines ?? "nil";
|
|
528
|
-
|
|
449
|
+
const fail = this.options.module?.fail ?? "nil";
|
|
450
|
+
out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
|
|
529
451
|
}
|
|
530
452
|
const concat = this.helpers.get("concat");
|
|
531
453
|
if (concat) {
|
|
@@ -694,7 +616,7 @@ var Lowerer = class {
|
|
|
694
616
|
break;
|
|
695
617
|
}
|
|
696
618
|
case "ReturnStatement":
|
|
697
|
-
if (declaration.
|
|
619
|
+
if (declaration.argument) this.report(declaration, "A module cannot return a value; export it instead");
|
|
698
620
|
body.push(returns([]));
|
|
699
621
|
break;
|
|
700
622
|
case "ImportStatement":
|
|
@@ -873,7 +795,7 @@ var Lowerer = class {
|
|
|
873
795
|
case "CallStatement": {
|
|
874
796
|
const chain = optionalChain(node.expression);
|
|
875
797
|
if (chain) return [this.optionalCallStatement(chain)];
|
|
876
|
-
const expression = this.
|
|
798
|
+
const expression = this.callValues(node.expression);
|
|
877
799
|
if (expression.type !== "CallExpression" && expression.type !== "MethodCallExpression") {
|
|
878
800
|
this.report(node, "A statement must be a call");
|
|
879
801
|
return [];
|
|
@@ -926,23 +848,26 @@ var Lowerer = class {
|
|
|
926
848
|
}];
|
|
927
849
|
case "GenericForStatement": {
|
|
928
850
|
const prelude = [];
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
if (variables.length === 1 && this.options.types?.iteratesValues.has(node)) {
|
|
936
|
-
variables.unshift(this.names.fresh("_"));
|
|
851
|
+
let item;
|
|
852
|
+
if (node.variable.type === "IdentifierPattern") {
|
|
853
|
+
item = this.name(node.variable.name);
|
|
854
|
+
} else {
|
|
855
|
+
item = this.names.fresh("item");
|
|
856
|
+
prelude.push(...this.destructure(node.variable, identifier(item), "declare"));
|
|
937
857
|
}
|
|
858
|
+
const form = this.options.types?.loops.get(node) ?? { walks: "values", viaIter: false };
|
|
859
|
+
let source = this.expression(node.iterator);
|
|
860
|
+
if (form.viaIter) source = methodCall(source, "__iter", []);
|
|
861
|
+
const variables = form.walks === "values" ? [this.names.fresh("_"), item] : [item];
|
|
862
|
+
const iterators = form.walks === "iteration" ? [call(this.builtin("unpack"), [source, number(1), number(3)])] : [source];
|
|
938
863
|
return [genericFor(
|
|
939
864
|
variables,
|
|
940
|
-
|
|
865
|
+
iterators,
|
|
941
866
|
this.loopBody51(this.block(node.body, prelude)).statements
|
|
942
867
|
)];
|
|
943
868
|
}
|
|
944
869
|
case "ReturnStatement":
|
|
945
|
-
return [returns(this.
|
|
870
|
+
return [returns(node.argument ? [this.expression(node.argument)] : [])];
|
|
946
871
|
case "BreakStatement":
|
|
947
872
|
return [{ type: "BreakStatement", ...spanOf(node) }];
|
|
948
873
|
case "ContinueStatement":
|
|
@@ -986,7 +911,7 @@ var Lowerer = class {
|
|
|
986
911
|
// end
|
|
987
912
|
// function Dog.speak(this) ... end
|
|
988
913
|
//
|
|
989
|
-
// `new
|
|
914
|
+
// `Dog.new(x)` builds one, `dog:speak()` passes the instance
|
|
990
915
|
// as `this`, and `super.speak()` is `Animal.speak(this)` — the
|
|
991
916
|
// base's own function, run on this instance.
|
|
992
917
|
// ============================================================
|
|
@@ -1012,6 +937,7 @@ var Lowerer = class {
|
|
|
1012
937
|
classRuntimeStatements() {
|
|
1013
938
|
if (!this.classHelpers) return [];
|
|
1014
939
|
const { build, accessors } = this.classHelpers;
|
|
940
|
+
const metamethods = `{ ${CLASS_METAMETHODS.map((name) => `"${name}"`).join(", ")} }`;
|
|
1015
941
|
return parseLuau(`
|
|
1016
942
|
local function ${build}(base)
|
|
1017
943
|
local class = { __getters = {}, __setters = {} }
|
|
@@ -1022,6 +948,9 @@ local function ${build}(base)
|
|
|
1022
948
|
setmetatable(class, { __index = base })
|
|
1023
949
|
setmetatable(class.__getters, { __index = base.__getters })
|
|
1024
950
|
setmetatable(class.__setters, { __index = base.__setters })
|
|
951
|
+
for _, key in ipairs(${metamethods}) do
|
|
952
|
+
class[key] = base[key]
|
|
953
|
+
end
|
|
1025
954
|
end
|
|
1026
955
|
return class
|
|
1027
956
|
end
|
|
@@ -1084,6 +1013,7 @@ end
|
|
|
1084
1013
|
}
|
|
1085
1014
|
break;
|
|
1086
1015
|
case "ClassMethod":
|
|
1016
|
+
if (member2.isAbstract) break;
|
|
1087
1017
|
out.push(this.functionStatement(
|
|
1088
1018
|
member(self, member2.name.name),
|
|
1089
1019
|
member2,
|
|
@@ -1105,7 +1035,7 @@ end
|
|
|
1105
1035
|
}
|
|
1106
1036
|
}
|
|
1107
1037
|
out.push(this.classInit(node, self));
|
|
1108
|
-
out.push(this.classNew(node, self));
|
|
1038
|
+
if (!node.isAbstract) out.push(this.classNew(node, self));
|
|
1109
1039
|
out.push(callStatement(call(identifier(accessors), [self])));
|
|
1110
1040
|
} finally {
|
|
1111
1041
|
this.classContext = previous;
|
|
@@ -1383,7 +1313,11 @@ end
|
|
|
1383
1313
|
}
|
|
1384
1314
|
switch (node.type) {
|
|
1385
1315
|
case "Identifier":
|
|
1386
|
-
|
|
1316
|
+
if (node.name === "scriptArgs" && this.isLanguageGlobal(node)) {
|
|
1317
|
+
this.usesScriptArgs = true;
|
|
1318
|
+
return identifier("scriptArgs");
|
|
1319
|
+
}
|
|
1320
|
+
return this.loweredGlobalValue(node) ?? this.reference(node);
|
|
1387
1321
|
case "NilLiteral":
|
|
1388
1322
|
return nil();
|
|
1389
1323
|
case "BooleanLiteral":
|
|
@@ -1392,8 +1326,6 @@ end
|
|
|
1392
1326
|
return number(node.value, this.numberRaw(node));
|
|
1393
1327
|
case "StringLiteral":
|
|
1394
1328
|
return string(node.value);
|
|
1395
|
-
case "VarargExpression":
|
|
1396
|
-
return vararg();
|
|
1397
1329
|
// Only a recovering parse makes one, and a syntax error stops the build.
|
|
1398
1330
|
case "ErrorExpression":
|
|
1399
1331
|
this.report(node, "Syntax error");
|
|
@@ -1414,11 +1346,6 @@ end
|
|
|
1414
1346
|
}
|
|
1415
1347
|
case "UnaryExpression":
|
|
1416
1348
|
return unary(node.operator, this.expression(node.argument));
|
|
1417
|
-
case "NewExpression":
|
|
1418
|
-
return call(
|
|
1419
|
-
member(this.expression(node.callee), "new"),
|
|
1420
|
-
this.values(node.arguments)
|
|
1421
|
-
);
|
|
1422
1349
|
case "SuperExpression":
|
|
1423
1350
|
return this.superClassReference(node);
|
|
1424
1351
|
case "SpreadElement":
|
|
@@ -1426,12 +1353,15 @@ end
|
|
|
1426
1353
|
return nil();
|
|
1427
1354
|
case "ClassExpression":
|
|
1428
1355
|
return this.classExpression(node);
|
|
1429
|
-
case "MemberExpression":
|
|
1430
|
-
case "IndexExpression":
|
|
1431
1356
|
case "CallExpression":
|
|
1432
|
-
case "MethodCallExpression":
|
|
1357
|
+
case "MethodCallExpression":
|
|
1358
|
+
return this.callValues(node);
|
|
1359
|
+
case "MemberExpression":
|
|
1360
|
+
case "IndexExpression": {
|
|
1433
1361
|
const chain = optionalChain(node);
|
|
1434
1362
|
if (chain) return this.optionalChainExpression(chain);
|
|
1363
|
+
const global = node.type === "MemberExpression" ? this.loweredGlobalValue(node) : void 0;
|
|
1364
|
+
if (global) return global;
|
|
1435
1365
|
return this.link(node, this.expression(linkObject(node)));
|
|
1436
1366
|
}
|
|
1437
1367
|
case "ParenthesizedExpression":
|
|
@@ -1451,14 +1381,47 @@ end
|
|
|
1451
1381
|
};
|
|
1452
1382
|
}
|
|
1453
1383
|
}
|
|
1384
|
+
/** A call. */
|
|
1385
|
+
callValues(node) {
|
|
1386
|
+
const chain = optionalChain(node);
|
|
1387
|
+
if (chain) return this.optionalChainExpression(chain);
|
|
1388
|
+
const object = linkObject(node);
|
|
1389
|
+
if (node.type === "CallExpression" && object.type === "Identifier") {
|
|
1390
|
+
return this.link(node, this.reference(object));
|
|
1391
|
+
}
|
|
1392
|
+
return this.link(node, this.expression(object));
|
|
1393
|
+
}
|
|
1394
|
+
/** A call is one value in tilua, and Luau would hand on every value one
|
|
1395
|
+
* in the last place of an argument list or an array returns. Types say
|
|
1396
|
+
* how many a call returns; where a type says nothing — `any`, `unknown`
|
|
1397
|
+
* — the call is kept to one: `f((g()))`. */
|
|
1398
|
+
keptToOne(nodes, values) {
|
|
1399
|
+
const node = nodes[nodes.length - 1];
|
|
1400
|
+
const last = values[values.length - 1];
|
|
1401
|
+
if (!node || !last || !expandsToMany(last)) return values;
|
|
1402
|
+
const type = this.options.types?.typeOf.get(node);
|
|
1403
|
+
if (type && type.kind !== "any" && type.kind !== "unknown") return values;
|
|
1404
|
+
return [...values.slice(0, -1), parenthesized(last)];
|
|
1405
|
+
}
|
|
1406
|
+
/** Is this `scriptArgs` the language's own, not a name the file declared? */
|
|
1407
|
+
isLanguageGlobal(node) {
|
|
1408
|
+
const id = this.bindingIdOf(node);
|
|
1409
|
+
return id === void 0 || this.scopes.bindings.get(id)?.kind === "global";
|
|
1410
|
+
}
|
|
1454
1411
|
/** A list of values — a call's arguments, a `return`'s, a declaration's,
|
|
1455
1412
|
* an assignment's. `f(a, ...xs)` is Lua's own last-value expansion,
|
|
1456
1413
|
* `f(a, table.unpack(xs))`; a spread anywhere else cannot be, since only
|
|
1457
1414
|
* the last value of a list expands, so the whole list is built as an
|
|
1458
|
-
* array first and that is what expands.
|
|
1459
|
-
|
|
1415
|
+
* array first and that is what expands.
|
|
1416
|
+
*
|
|
1417
|
+
* `arguments` is set for a call's arguments, where Luau hands on every
|
|
1418
|
+
* value the last one returns — see `keptToOne`. */
|
|
1419
|
+
values(list, asArguments = false) {
|
|
1460
1420
|
const spreads = list.filter((a) => a.type === "SpreadElement");
|
|
1461
|
-
if (!spreads.length)
|
|
1421
|
+
if (!spreads.length) {
|
|
1422
|
+
const lowered = list.map((a) => this.expression(a));
|
|
1423
|
+
return asArguments ? this.keptToOne(list, lowered) : lowered;
|
|
1424
|
+
}
|
|
1462
1425
|
const unpack = (value) => call(member(this.builtin("table"), "unpack"), [value]);
|
|
1463
1426
|
const last = list[list.length - 1];
|
|
1464
1427
|
if (spreads.length === 1 && last.type === "SpreadElement") {
|
|
@@ -1477,15 +1440,21 @@ end
|
|
|
1477
1440
|
return member(object, node.property.name);
|
|
1478
1441
|
case "IndexExpression":
|
|
1479
1442
|
return index(object, this.expression(node.index));
|
|
1480
|
-
case "CallExpression":
|
|
1481
|
-
|
|
1443
|
+
case "CallExpression": {
|
|
1444
|
+
const lowered = this.loweredGlobalCall(node);
|
|
1445
|
+
const args = this.values(node.arguments, true);
|
|
1446
|
+
if (!lowered) return call(object, args);
|
|
1447
|
+
return call(calleePath(lowered.callee), [...lowered.prepend, ...args]);
|
|
1448
|
+
}
|
|
1482
1449
|
case "MethodCallExpression": {
|
|
1483
|
-
const consoleCall = this.consoleCall(node);
|
|
1484
|
-
if (consoleCall) return consoleCall;
|
|
1485
|
-
const args = this.values(node.arguments);
|
|
1486
1450
|
const lowered = this.loweredMethodCall(node);
|
|
1451
|
+
const args = this.values(node.arguments, true);
|
|
1487
1452
|
if (lowered) {
|
|
1488
|
-
return call(calleePath(lowered.callee),
|
|
1453
|
+
return call(calleePath(lowered.callee), [
|
|
1454
|
+
...lowered.prepend,
|
|
1455
|
+
...lowered.passReceiver ? [object] : [],
|
|
1456
|
+
...args
|
|
1457
|
+
]);
|
|
1489
1458
|
}
|
|
1490
1459
|
if (!isLuauName(node.method.name)) {
|
|
1491
1460
|
this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
|
|
@@ -1713,7 +1682,8 @@ end
|
|
|
1713
1682
|
* runs of plain elements and the spread arrays are joined by `concat`. */
|
|
1714
1683
|
arrayExpression(node) {
|
|
1715
1684
|
if (!node.elements.some((e) => e.type === "SpreadElement")) {
|
|
1716
|
-
|
|
1685
|
+
const elements = node.elements;
|
|
1686
|
+
return table(this.keptToOne(elements, elements.map((e) => this.expression(e))).map((value) => ({ type: "TableFieldPositional", value })));
|
|
1717
1687
|
}
|
|
1718
1688
|
const parts = [];
|
|
1719
1689
|
let current = [];
|
|
@@ -1898,10 +1868,6 @@ function mentions(expression, name) {
|
|
|
1898
1868
|
walk(expression);
|
|
1899
1869
|
return found;
|
|
1900
1870
|
}
|
|
1901
|
-
function isFunctionType(type) {
|
|
1902
|
-
if (type.kind === "function") return true;
|
|
1903
|
-
return type.kind === "intersection" && type.types.some((t) => t.kind === "function");
|
|
1904
|
-
}
|
|
1905
1871
|
function allNodes(root) {
|
|
1906
1872
|
const out = [];
|
|
1907
1873
|
const visit = (value) => {
|
|
@@ -1916,6 +1882,24 @@ function allNodes(root) {
|
|
|
1916
1882
|
visit(root);
|
|
1917
1883
|
return out;
|
|
1918
1884
|
}
|
|
1885
|
+
var CLASS_METAMETHODS = [
|
|
1886
|
+
"__add",
|
|
1887
|
+
"__sub",
|
|
1888
|
+
"__mul",
|
|
1889
|
+
"__div",
|
|
1890
|
+
"__idiv",
|
|
1891
|
+
"__mod",
|
|
1892
|
+
"__pow",
|
|
1893
|
+
"__unm",
|
|
1894
|
+
"__concat",
|
|
1895
|
+
"__len",
|
|
1896
|
+
"__eq",
|
|
1897
|
+
"__lt",
|
|
1898
|
+
"__le",
|
|
1899
|
+
"__call",
|
|
1900
|
+
"__tostring",
|
|
1901
|
+
"__iter"
|
|
1902
|
+
];
|
|
1919
1903
|
|
|
1920
1904
|
// src/lowering.ts
|
|
1921
1905
|
import { pathToFileURL } from "url";
|
|
@@ -1960,7 +1944,7 @@ import {
|
|
|
1960
1944
|
ParseError,
|
|
1961
1945
|
LexError
|
|
1962
1946
|
} from "@tilua/parser";
|
|
1963
|
-
import { parse as parseLuau2, parseExpressionFromSource, print } from "luau-parser";
|
|
1947
|
+
import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
|
|
1964
1948
|
|
|
1965
1949
|
// src/linemap.ts
|
|
1966
1950
|
function statementsOf(node) {
|
|
@@ -2043,16 +2027,19 @@ async function bundle(options) {
|
|
|
2043
2027
|
const file = pending.shift();
|
|
2044
2028
|
if (modules.has(file)) continue;
|
|
2045
2029
|
const source = sources.get(file);
|
|
2030
|
+
const shown = relative(root, file).replace(/\\/g, "/");
|
|
2046
2031
|
const lowered = lower(source.program, source.scopes, {
|
|
2047
2032
|
names,
|
|
2048
2033
|
types: analysis.types.get(file),
|
|
2049
2034
|
lowerings,
|
|
2050
2035
|
target: options.target ?? config?.target,
|
|
2051
2036
|
source: source.text,
|
|
2037
|
+
file: shown,
|
|
2052
2038
|
module: {
|
|
2053
2039
|
name: source.name,
|
|
2054
2040
|
require: requireExpression,
|
|
2055
2041
|
lines: `${G}.lines`,
|
|
2042
|
+
fail: `${G}.fail`,
|
|
2056
2043
|
resolve: (specifier) => {
|
|
2057
2044
|
const target = resolveModulePath(file, specifier, config);
|
|
2058
2045
|
if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
|
|
@@ -2062,7 +2049,6 @@ async function bundle(options) {
|
|
|
2062
2049
|
}
|
|
2063
2050
|
});
|
|
2064
2051
|
for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
|
|
2065
|
-
const shown = relative(root, file).replace(/\\/g, "/");
|
|
2066
2052
|
for (const statement of allStatements(lowered.statements)) {
|
|
2067
2053
|
const line = lowered.origins.get(statement);
|
|
2068
2054
|
if (line !== void 0) origins.set(statement, { file: shown, line });
|
|
@@ -2071,7 +2057,8 @@ async function bundle(options) {
|
|
|
2071
2057
|
name: source.name,
|
|
2072
2058
|
statements: lowered.statements,
|
|
2073
2059
|
exportsName: lowered.exportsName,
|
|
2074
|
-
info: lowered.module
|
|
2060
|
+
info: lowered.module,
|
|
2061
|
+
usesScriptArgs: lowered.usesScriptArgs
|
|
2075
2062
|
});
|
|
2076
2063
|
}
|
|
2077
2064
|
for (const [file, fileDirectives] of directives) {
|
|
@@ -2096,6 +2083,9 @@ async function bundle(options) {
|
|
|
2096
2083
|
}
|
|
2097
2084
|
const program2 = parseLuau2(runtime(G));
|
|
2098
2085
|
const assignment = program2.body.statements[1];
|
|
2086
|
+
if ([...modules.values()].some((m) => m.usesScriptArgs)) {
|
|
2087
|
+
program2.body.statements.unshift(local(["scriptArgs"], [table([{ type: "TableFieldPositional", value: { type: "VarargExpression" } }])]));
|
|
2088
|
+
}
|
|
2099
2089
|
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;
|
|
2100
2090
|
if (!modulesTable) throw new Error("@tilua/compiler: the bundle runtime has no modules table");
|
|
2101
2091
|
for (const module of modules.values()) {
|
|
@@ -2112,13 +2102,23 @@ async function bundle(options) {
|
|
|
2112
2102
|
fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
|
|
2113
2103
|
modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: table(fields) });
|
|
2114
2104
|
}
|
|
2115
|
-
const
|
|
2116
|
-
|
|
2105
|
+
const ok = names.fresh("ok");
|
|
2106
|
+
const result = names.fresh("result");
|
|
2107
|
+
const entryStatements = parseLuau2(`
|
|
2108
|
+
local ${ok}, ${result} = xpcall(function()
|
|
2109
|
+
return ${G}.require(${JSON.stringify(sources.get(entry).name)})
|
|
2110
|
+
end, ${G}.fail)
|
|
2111
|
+
if not ${ok} then
|
|
2112
|
+
error(${result}, 0)
|
|
2113
|
+
end
|
|
2114
|
+
${modules.get(entry).info.exports ? `return ${result}` : ""}
|
|
2115
|
+
`).body.statements;
|
|
2116
|
+
program2.body.statements.push(...entryStatements);
|
|
2117
2117
|
const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
|
|
2118
2118
|
if (map.size) {
|
|
2119
|
-
program2.body.statements.splice(program2.body.statements.length -
|
|
2119
|
+
program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
|
|
2120
2120
|
[member(identifier(G), "lines")],
|
|
2121
|
-
[
|
|
2121
|
+
[parseExpressionFromSource2(lineMapSource(map))]
|
|
2122
2122
|
));
|
|
2123
2123
|
}
|
|
2124
2124
|
return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
|
|
@@ -2128,6 +2128,52 @@ function runtime(G) {
|
|
|
2128
2128
|
local ${G}
|
|
2129
2129
|
${G} = {
|
|
2130
2130
|
modules = {},
|
|
2131
|
+
-- Filled in just before the entry runs: bundle line -> { file, line }.
|
|
2132
|
+
lines = nil,
|
|
2133
|
+
-- What an error nothing caught says, in the project's files: the bundle's
|
|
2134
|
+
-- own positions in the message are replaced by the places they came from,
|
|
2135
|
+
-- and a traceback of those places follows. A message that is not a string
|
|
2136
|
+
-- is left as it is, and a position that is not this bundle's (a library
|
|
2137
|
+
-- that raised its error with one already mapped) is left alone.
|
|
2138
|
+
fail = function(message)
|
|
2139
|
+
if type(message) ~= "string" then
|
|
2140
|
+
return message
|
|
2141
|
+
end
|
|
2142
|
+
local lines = ${G}.lines
|
|
2143
|
+
if lines == nil then
|
|
2144
|
+
return debug.traceback(message, 2)
|
|
2145
|
+
end
|
|
2146
|
+
local chunk
|
|
2147
|
+
if debug.info ~= nil then
|
|
2148
|
+
chunk = debug.info(1, "s")
|
|
2149
|
+
else
|
|
2150
|
+
chunk = debug.getinfo(1, "S").short_src
|
|
2151
|
+
end
|
|
2152
|
+
local prefix = chunk:gsub("%p", "%%%0")
|
|
2153
|
+
local function place(line)
|
|
2154
|
+
local origin = lines[tonumber(line)]
|
|
2155
|
+
if origin == nil then
|
|
2156
|
+
return nil
|
|
2157
|
+
end
|
|
2158
|
+
return origin[1] .. ":" .. tostring(origin[2])
|
|
2159
|
+
end
|
|
2160
|
+
local mapped = message:gsub(prefix .. ":(%d+)", function(line)
|
|
2161
|
+
return place(line)
|
|
2162
|
+
end)
|
|
2163
|
+
local frames = {}
|
|
2164
|
+
for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
|
|
2165
|
+
local line = frame:match(prefix .. ":(%d+)")
|
|
2166
|
+
local at = line ~= nil and place(line) or nil
|
|
2167
|
+
if at ~= nil then
|
|
2168
|
+
local name = frame:match("function ([%w_.:]+)")
|
|
2169
|
+
frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
|
|
2170
|
+
end
|
|
2171
|
+
end
|
|
2172
|
+
if #frames == 0 then
|
|
2173
|
+
return mapped
|
|
2174
|
+
end
|
|
2175
|
+
return mapped .. "\\n" .. table.concat(frames, "\\n")
|
|
2176
|
+
end,
|
|
2131
2177
|
records = {},
|
|
2132
2178
|
require = function(name)
|
|
2133
2179
|
local record = ${G}.records[name]
|