@tilua/compiler 1.0.2 → 1.1.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/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) {
@@ -398,60 +260,9 @@ var Lowerer = class {
398
260
  }
399
261
  loweringNames = /* @__PURE__ */ new Map();
400
262
  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
263
  /** What a name was declared as: the value of `const f = ...`, or the whole
453
- * `function f() { ... }`. Built once, on the first `console` call that
454
- * needs it — most files have none. */
264
+ * `function f() { ... }`. Built once, on the first call a library asks
265
+ * about — most files have none. */
455
266
  declarationOf(argument) {
456
267
  if (argument.type !== "Identifier") return void 0;
457
268
  if (!this.declarations) {
@@ -490,21 +301,127 @@ var Lowerer = class {
490
301
  const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
491
302
  return [first, ...middle.map(trim), trim(last)].join("\n");
492
303
  }
304
+ /** Where `node` was written, as a library is told it. */
305
+ callSite(node) {
306
+ return { file: this.options.file, line: node.line.start, column: node.column.start };
307
+ }
308
+ /** What the compiler knows about each argument and the runtime cannot:
309
+ * its type, and the code it — or what it names — was written as. Built
310
+ * only if a library reads it. */
311
+ argumentInfo(args2) {
312
+ return args2.map((argument) => {
313
+ const declaration = this.declarationOf(argument);
314
+ const type = this.options.types?.typeOf.get(argument);
315
+ return {
316
+ type,
317
+ typeText: type ? (0, import_parser2.formatType)(type) : void 0,
318
+ source: this.writtenSource(argument),
319
+ declaration: declaration ? this.writtenSource(declaration) : void 0,
320
+ spread: argument.type === "SpreadElement"
321
+ };
322
+ });
323
+ }
324
+ /** The name a global is read by at `node`, or `undefined` when `node` is
325
+ * not a global — a local of the same name is the author's. */
326
+ globalName(node) {
327
+ if (node.type !== "Identifier") return void 0;
328
+ const id = this.scopes.bindingOf.get(node);
329
+ if (id === void 0) return node.name;
330
+ return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
331
+ }
332
+ /** What a library says `receiver:method(...)` is. The last library
333
+ * loaded is asked first, so a project's own library can answer for a
334
+ * method an earlier one also claims; no answer leaves an ordinary Luau
335
+ * method call, which is what a value that answers to the method itself
336
+ * wants (`text:upper()`). */
493
337
  loweredMethodCall(node) {
494
338
  const lowerings = this.options.lowerings;
495
- if (!lowerings?.length) return void 0;
339
+ if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
496
340
  const receiver = this.options.types?.typeOf.get(node.object);
341
+ const receiverGlobal = this.globalName(node.object);
342
+ const info = this.lazyArguments(node.arguments);
497
343
  for (let i = lowerings.length - 1; i >= 0; i--) {
498
- const answer = lowerings[i].plugin.methodCall?.({
344
+ const plugin = lowerings[i].plugin;
345
+ if (!plugin.methodCall) continue;
346
+ const answer = plugin.methodCall({
499
347
  method: node.method.name,
500
348
  receiver,
349
+ receiverGlobal,
501
350
  argumentCount: node.arguments.length,
351
+ at: this.callSite(node.method),
352
+ get arguments() {
353
+ return info();
354
+ },
502
355
  use: (runtime2) => this.loweringRuntime(i, runtime2)
503
356
  });
504
- if (answer) return { callee: answer.callee, passReceiver: answer.passReceiver !== false };
357
+ if (answer) {
358
+ return {
359
+ callee: answer.callee,
360
+ prepend: this.prepended(answer.prepend, i, node),
361
+ passReceiver: answer.passReceiver !== false
362
+ };
363
+ }
505
364
  }
506
365
  return void 0;
507
366
  }
367
+ /** What a library says a call to a global — `print(x)` — is. */
368
+ loweredGlobalCall(node) {
369
+ const lowerings = this.options.lowerings;
370
+ if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
371
+ const name = this.globalName(node.callee);
372
+ if (name === void 0) return void 0;
373
+ const info = this.lazyArguments(node.arguments);
374
+ for (let i = lowerings.length - 1; i >= 0; i--) {
375
+ const plugin = lowerings[i].plugin;
376
+ if (!plugin.globalCall) continue;
377
+ const answer = plugin.globalCall({
378
+ name,
379
+ at: this.callSite(node.callee),
380
+ get arguments() {
381
+ return info();
382
+ },
383
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
384
+ });
385
+ if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
386
+ }
387
+ return void 0;
388
+ }
389
+ /** What a library says a global read as a value — `local p = print` — is. */
390
+ loweredGlobalValue(node) {
391
+ const lowerings = this.options.lowerings;
392
+ if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
393
+ const name = this.globalName(node);
394
+ if (name === void 0) return void 0;
395
+ for (let i = lowerings.length - 1; i >= 0; i--) {
396
+ const answer = lowerings[i].plugin.globalValue?.({
397
+ name,
398
+ at: this.callSite(node),
399
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
400
+ });
401
+ if (answer !== void 0) return this.libraryExpression(answer, i, node);
402
+ }
403
+ return void 0;
404
+ }
405
+ lazyArguments(args2) {
406
+ let info;
407
+ return () => info ??= this.argumentInfo(args2);
408
+ }
409
+ prepended(sources, index2, node) {
410
+ return (sources ?? []).flatMap((source) => {
411
+ const expression = this.libraryExpression(source, index2, node);
412
+ return expression ? [expression] : [];
413
+ });
414
+ }
415
+ /** Luau a library wrote as an expression. One that does not parse is the
416
+ * library's mistake, reported at the call it was written for. */
417
+ libraryExpression(source, index2, node) {
418
+ try {
419
+ return (0, import_luau_parser.parseExpressionFromSource)(source);
420
+ } catch (error) {
421
+ this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
422
+ return void 0;
423
+ }
424
+ }
508
425
  helperDefinitions() {
509
426
  const out2 = [];
510
427
  const assign2 = this.helpers.get("assign");
@@ -532,12 +449,9 @@ var Lowerer = class {
532
449
  });
533
450
  continue;
534
451
  }
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
452
  const lines = this.options.module?.lines ?? "nil";
540
- out2.push(...(0, import_luau_parser.parse)(CONSOLE_RUNTIME.replace(/__NAME__/g, console2).replace(/__LINES__/g, lines)).body.statements);
453
+ const fail = this.options.module?.fail ?? "nil";
454
+ out2.push(...(0, import_luau_parser.parse)(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
541
455
  }
542
456
  const concat = this.helpers.get("concat");
543
457
  if (concat) {
@@ -1395,7 +1309,7 @@ end
1395
1309
  }
1396
1310
  switch (node.type) {
1397
1311
  case "Identifier":
1398
- return this.reference(node);
1312
+ return this.loweredGlobalValue(node) ?? this.reference(node);
1399
1313
  case "NilLiteral":
1400
1314
  return nil();
1401
1315
  case "BooleanLiteral":
@@ -1444,7 +1358,11 @@ end
1444
1358
  case "MethodCallExpression": {
1445
1359
  const chain = optionalChain(node);
1446
1360
  if (chain) return this.optionalChainExpression(chain);
1447
- return this.link(node, this.expression(linkObject(node)));
1361
+ const object = linkObject(node);
1362
+ if (node.type === "CallExpression" && object.type === "Identifier") {
1363
+ return this.link(node, this.reference(object));
1364
+ }
1365
+ return this.link(node, this.expression(object));
1448
1366
  }
1449
1367
  case "ParenthesizedExpression":
1450
1368
  return parenthesized(this.expression(node.expression));
@@ -1489,15 +1407,21 @@ end
1489
1407
  return member(object, node.property.name);
1490
1408
  case "IndexExpression":
1491
1409
  return index(object, this.expression(node.index));
1492
- case "CallExpression":
1493
- return call(object, this.values(node.arguments));
1494
- case "MethodCallExpression": {
1495
- const consoleCall = this.consoleCall(node);
1496
- if (consoleCall) return consoleCall;
1410
+ case "CallExpression": {
1411
+ const lowered = this.loweredGlobalCall(node);
1497
1412
  const args2 = this.values(node.arguments);
1413
+ if (lowered) return call(calleePath(lowered.callee), [...lowered.prepend, ...args2]);
1414
+ return call(object, args2);
1415
+ }
1416
+ case "MethodCallExpression": {
1498
1417
  const lowered = this.loweredMethodCall(node);
1418
+ const args2 = this.values(node.arguments);
1499
1419
  if (lowered) {
1500
- return call(calleePath(lowered.callee), lowered.passReceiver ? [object, ...args2] : args2);
1420
+ return call(calleePath(lowered.callee), [
1421
+ ...lowered.prepend,
1422
+ ...lowered.passReceiver ? [object] : [],
1423
+ ...args2
1424
+ ]);
1501
1425
  }
1502
1426
  if (!isLuauName(node.method.name)) {
1503
1427
  this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
@@ -1910,10 +1834,6 @@ function mentions(expression, name) {
1910
1834
  walk(expression);
1911
1835
  return found;
1912
1836
  }
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
1837
  function allNodes(root) {
1918
1838
  const out2 = [];
1919
1839
  const visit = (value) => {
@@ -2037,16 +1957,19 @@ async function bundle(options) {
2037
1957
  const file = pending.shift();
2038
1958
  if (modules.has(file)) continue;
2039
1959
  const source = sources.get(file);
1960
+ const shown = (0, import_node_path2.relative)(root, file).replace(/\\/g, "/");
2040
1961
  const lowered = lower(source.program, source.scopes, {
2041
1962
  names,
2042
1963
  types: analysis.types.get(file),
2043
1964
  lowerings,
2044
1965
  target: options.target ?? config2?.target,
2045
1966
  source: source.text,
1967
+ file: shown,
2046
1968
  module: {
2047
1969
  name: source.name,
2048
1970
  require: requireExpression,
2049
1971
  lines: `${G}.lines`,
1972
+ fail: `${G}.fail`,
2050
1973
  resolve: (specifier) => {
2051
1974
  const target = (0, import_parser3.resolveModulePath)(file, specifier, config2);
2052
1975
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2056,7 +1979,6 @@ async function bundle(options) {
2056
1979
  }
2057
1980
  });
2058
1981
  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
1982
  for (const statement of allStatements(lowered.statements)) {
2061
1983
  const line = lowered.origins.get(statement);
2062
1984
  if (line !== void 0) origins.set(statement, { file: shown, line });
@@ -2106,11 +2028,21 @@ async function bundle(options) {
2106
2028
  fields.push(field("load", functionExpression(functionBody([module2.exportsName], module2.statements, true))));
2107
2029
  modulesTable.fields.push({ type: "TableFieldComputed", key: string(module2.name), value: table(fields) });
2108
2030
  }
2109
- const start = call(requireExpression, [string(sources.get(entry2).name)]);
2110
- program.body.statements.push(modules.get(entry2).info.exports ? returns([start]) : callStatement(start));
2031
+ const ok = names.fresh("ok");
2032
+ const result = names.fresh("result");
2033
+ const entryStatements = (0, import_luau_parser2.parse)(`
2034
+ local ${ok}, ${result} = xpcall(function()
2035
+ return ${G}.require(${JSON.stringify(sources.get(entry2).name)})
2036
+ end, ${G}.fail)
2037
+ if not ${ok} then
2038
+ error(${result}, 0)
2039
+ end
2040
+ ${modules.get(entry2).info.exports ? `return ${result}` : ""}
2041
+ `).body.statements;
2042
+ program.body.statements.push(...entryStatements);
2111
2043
  const map = buildLineMap(program, (0, import_luau_parser2.parse)((0, import_luau_parser2.print)(program)), origins);
2112
2044
  if (map.size) {
2113
- program.body.statements.splice(program.body.statements.length - 1, 0, assign(
2045
+ program.body.statements.splice(program.body.statements.length - entryStatements.length, 0, assign(
2114
2046
  [member(identifier(G), "lines")],
2115
2047
  [(0, import_luau_parser2.parseExpressionFromSource)(lineMapSource(map))]
2116
2048
  ));
@@ -2122,6 +2054,52 @@ function runtime(G) {
2122
2054
  local ${G}
2123
2055
  ${G} = {
2124
2056
  modules = {},
2057
+ -- Filled in just before the entry runs: bundle line -> { file, line }.
2058
+ lines = nil,
2059
+ -- What an error nothing caught says, in the project's files: the bundle's
2060
+ -- own positions in the message are replaced by the places they came from,
2061
+ -- and a traceback of those places follows. A message that is not a string
2062
+ -- is left as it is, and a position that is not this bundle's (a library
2063
+ -- that raised its error with one already mapped) is left alone.
2064
+ fail = function(message)
2065
+ if type(message) ~= "string" then
2066
+ return message
2067
+ end
2068
+ local lines = ${G}.lines
2069
+ if lines == nil then
2070
+ return debug.traceback(message, 2)
2071
+ end
2072
+ local chunk
2073
+ if debug.info ~= nil then
2074
+ chunk = debug.info(1, "s")
2075
+ else
2076
+ chunk = debug.getinfo(1, "S").short_src
2077
+ end
2078
+ local prefix = chunk:gsub("%p", "%%%0")
2079
+ local function place(line)
2080
+ local origin = lines[tonumber(line)]
2081
+ if origin == nil then
2082
+ return nil
2083
+ end
2084
+ return origin[1] .. ":" .. tostring(origin[2])
2085
+ end
2086
+ local mapped = message:gsub(prefix .. ":(%d+)", function(line)
2087
+ return place(line)
2088
+ end)
2089
+ local frames = {}
2090
+ for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
2091
+ local line = frame:match(prefix .. ":(%d+)")
2092
+ local at = line ~= nil and place(line) or nil
2093
+ if at ~= nil then
2094
+ local name = frame:match("function ([%w_.:]+)")
2095
+ frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2096
+ end
2097
+ end
2098
+ if #frames == 0 then
2099
+ return mapped
2100
+ end
2101
+ return mapped .. "\\n" .. table.concat(frames, "\\n")
2102
+ end,
2125
2103
  records = {},
2126
2104
  require = function(name)
2127
2105
  local record = ${G}.records[name]
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  bundle
4
- } from "./chunk-QSRDDIU4.js";
4
+ } from "./chunk-2IPPO4V3.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { writeFileSync } from "fs";