@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/README.md CHANGED
@@ -168,6 +168,33 @@ needed it; the receiver becomes the call's first argument unless
168
168
  `passReceiver: false`. The last library loaded is asked first, and `undefined`
169
169
  leaves an ordinary Luau method call.
170
170
 
171
+ Hooks also get the call site (`at`) and what the compiler knew about each
172
+ argument (`arguments`: type, the code it was written as, and what a name was
173
+ declared as). An answer can `prepend` Luau expressions ahead of the written
174
+ arguments. `globalCall` and `globalValue` answer for a global, such as
175
+ `print(x)` or a bare `print`. A runtime can read `__LINES__` to reach the
176
+ bundle's line map. See the `@tilua/parser` README for the details.
177
+ `console:log` is not built into the compiler: `@tilua-types/lua` implements it
178
+ with these hooks.
179
+
180
+ ## Errors at runtime
181
+
182
+ A bundle is one Luau file, so a position Luau reports names a line of that
183
+ file. The entry therefore runs under `xpcall`. If an error reaches the top,
184
+ its message is rewritten from bundle lines to the places in the project that
185
+ wrote them, a traceback of those places is added, and the error is raised
186
+ again:
187
+
188
+ ```
189
+ src/util.tilua:3: attempt to index nil with 'value'
190
+ at src/util.tilua:3 (boom)
191
+ at src/main.tilua:3 (load)
192
+ ```
193
+
194
+ A message that is not a string is raised unchanged. A position that is already
195
+ in the project's files, such as one from `error` in `@tilua-types/lua`, is
196
+ left as it is.
197
+
171
198
  ## Modules
172
199
 
173
200
  Roblox's `require` takes an Instance, so the bundle has its own. Each module
@@ -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) {
@@ -386,60 +248,9 @@ var Lowerer = class {
386
248
  }
387
249
  loweringNames = /* @__PURE__ */ new Map();
388
250
  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
251
  /** What a name was declared as: the value of `const f = ...`, or the whole
441
- * `function f() { ... }`. Built once, on the first `console` call that
442
- * needs it — most files have none. */
252
+ * `function f() { ... }`. Built once, on the first call a library asks
253
+ * about — most files have none. */
443
254
  declarationOf(argument) {
444
255
  if (argument.type !== "Identifier") return void 0;
445
256
  if (!this.declarations) {
@@ -478,21 +289,127 @@ var Lowerer = class {
478
289
  const trim = (line) => line.startsWith(indent) ? line.slice(indent.length) : line;
479
290
  return [first, ...middle.map(trim), trim(last)].join("\n");
480
291
  }
292
+ /** Where `node` was written, as a library is told it. */
293
+ callSite(node) {
294
+ return { file: this.options.file, line: node.line.start, column: node.column.start };
295
+ }
296
+ /** What the compiler knows about each argument and the runtime cannot:
297
+ * its type, and the code it — or what it names — was written as. Built
298
+ * only if a library reads it. */
299
+ argumentInfo(args) {
300
+ return args.map((argument) => {
301
+ const declaration = this.declarationOf(argument);
302
+ const type = this.options.types?.typeOf.get(argument);
303
+ return {
304
+ type,
305
+ typeText: type ? formatType(type) : void 0,
306
+ source: this.writtenSource(argument),
307
+ declaration: declaration ? this.writtenSource(declaration) : void 0,
308
+ spread: argument.type === "SpreadElement"
309
+ };
310
+ });
311
+ }
312
+ /** The name a global is read by at `node`, or `undefined` when `node` is
313
+ * not a global — a local of the same name is the author's. */
314
+ globalName(node) {
315
+ if (node.type !== "Identifier") return void 0;
316
+ const id = this.scopes.bindingOf.get(node);
317
+ if (id === void 0) return node.name;
318
+ return this.scopes.bindings.get(id)?.kind === "global" ? node.name : void 0;
319
+ }
320
+ /** What a library says `receiver:method(...)` is. The last library
321
+ * loaded is asked first, so a project's own library can answer for a
322
+ * method an earlier one also claims; no answer leaves an ordinary Luau
323
+ * method call, which is what a value that answers to the method itself
324
+ * wants (`text:upper()`). */
481
325
  loweredMethodCall(node) {
482
326
  const lowerings = this.options.lowerings;
483
- if (!lowerings?.length) return void 0;
327
+ if (!lowerings?.some((l) => l.plugin.methodCall)) return void 0;
484
328
  const receiver = this.options.types?.typeOf.get(node.object);
329
+ const receiverGlobal = this.globalName(node.object);
330
+ const info = this.lazyArguments(node.arguments);
485
331
  for (let i = lowerings.length - 1; i >= 0; i--) {
486
- const answer = lowerings[i].plugin.methodCall?.({
332
+ const plugin = lowerings[i].plugin;
333
+ if (!plugin.methodCall) continue;
334
+ const answer = plugin.methodCall({
487
335
  method: node.method.name,
488
336
  receiver,
337
+ receiverGlobal,
489
338
  argumentCount: node.arguments.length,
339
+ at: this.callSite(node.method),
340
+ get arguments() {
341
+ return info();
342
+ },
490
343
  use: (runtime2) => this.loweringRuntime(i, runtime2)
491
344
  });
492
- if (answer) return { callee: answer.callee, passReceiver: answer.passReceiver !== false };
345
+ if (answer) {
346
+ return {
347
+ callee: answer.callee,
348
+ prepend: this.prepended(answer.prepend, i, node),
349
+ passReceiver: answer.passReceiver !== false
350
+ };
351
+ }
493
352
  }
494
353
  return void 0;
495
354
  }
355
+ /** What a library says a call to a global — `print(x)` — is. */
356
+ loweredGlobalCall(node) {
357
+ const lowerings = this.options.lowerings;
358
+ if (!lowerings?.some((l) => l.plugin.globalCall)) return void 0;
359
+ const name = this.globalName(node.callee);
360
+ if (name === void 0) return void 0;
361
+ const info = this.lazyArguments(node.arguments);
362
+ for (let i = lowerings.length - 1; i >= 0; i--) {
363
+ const plugin = lowerings[i].plugin;
364
+ if (!plugin.globalCall) continue;
365
+ const answer = plugin.globalCall({
366
+ name,
367
+ at: this.callSite(node.callee),
368
+ get arguments() {
369
+ return info();
370
+ },
371
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
372
+ });
373
+ if (answer) return { callee: answer.callee, prepend: this.prepended(answer.prepend, i, node) };
374
+ }
375
+ return void 0;
376
+ }
377
+ /** What a library says a global read as a value — `local p = print` — is. */
378
+ loweredGlobalValue(node) {
379
+ const lowerings = this.options.lowerings;
380
+ if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
381
+ const name = this.globalName(node);
382
+ if (name === void 0) return void 0;
383
+ for (let i = lowerings.length - 1; i >= 0; i--) {
384
+ const answer = lowerings[i].plugin.globalValue?.({
385
+ name,
386
+ at: this.callSite(node),
387
+ use: (runtime2) => this.loweringRuntime(i, runtime2)
388
+ });
389
+ if (answer !== void 0) return this.libraryExpression(answer, i, node);
390
+ }
391
+ return void 0;
392
+ }
393
+ lazyArguments(args) {
394
+ let info;
395
+ return () => info ??= this.argumentInfo(args);
396
+ }
397
+ prepended(sources, index2, node) {
398
+ return (sources ?? []).flatMap((source) => {
399
+ const expression = this.libraryExpression(source, index2, node);
400
+ return expression ? [expression] : [];
401
+ });
402
+ }
403
+ /** Luau a library wrote as an expression. One that does not parse is the
404
+ * library's mistake, reported at the call it was written for. */
405
+ libraryExpression(source, index2, node) {
406
+ try {
407
+ return parseExpressionFromSource(source);
408
+ } catch (error) {
409
+ this.report(node, `'${this.options.lowerings[index2].from}' lowered this to Luau that does not parse: ${JSON.stringify(source)} (${error.message})`);
410
+ return void 0;
411
+ }
412
+ }
496
413
  helperDefinitions() {
497
414
  const out = [];
498
415
  const assign2 = this.helpers.get("assign");
@@ -520,12 +437,9 @@ var Lowerer = class {
520
437
  });
521
438
  continue;
522
439
  }
523
- out.push(...parseLuau(source.replace(/__NAME__/g, name)).body.statements);
524
- }
525
- const console = this.helpers.get("console");
526
- if (console) {
527
440
  const lines = this.options.module?.lines ?? "nil";
528
- out.push(...parseLuau(CONSOLE_RUNTIME.replace(/__NAME__/g, console).replace(/__LINES__/g, lines)).body.statements);
441
+ const fail = this.options.module?.fail ?? "nil";
442
+ out.push(...parseLuau(source.replace(/__NAME__/g, name).replace(/__LINES__/g, lines).replace(/__FAIL__/g, fail)).body.statements);
529
443
  }
530
444
  const concat = this.helpers.get("concat");
531
445
  if (concat) {
@@ -1383,7 +1297,7 @@ end
1383
1297
  }
1384
1298
  switch (node.type) {
1385
1299
  case "Identifier":
1386
- return this.reference(node);
1300
+ return this.loweredGlobalValue(node) ?? this.reference(node);
1387
1301
  case "NilLiteral":
1388
1302
  return nil();
1389
1303
  case "BooleanLiteral":
@@ -1432,7 +1346,11 @@ end
1432
1346
  case "MethodCallExpression": {
1433
1347
  const chain = optionalChain(node);
1434
1348
  if (chain) return this.optionalChainExpression(chain);
1435
- return this.link(node, this.expression(linkObject(node)));
1349
+ const object = linkObject(node);
1350
+ if (node.type === "CallExpression" && object.type === "Identifier") {
1351
+ return this.link(node, this.reference(object));
1352
+ }
1353
+ return this.link(node, this.expression(object));
1436
1354
  }
1437
1355
  case "ParenthesizedExpression":
1438
1356
  return parenthesized(this.expression(node.expression));
@@ -1477,15 +1395,21 @@ end
1477
1395
  return member(object, node.property.name);
1478
1396
  case "IndexExpression":
1479
1397
  return index(object, this.expression(node.index));
1480
- case "CallExpression":
1481
- return call(object, this.values(node.arguments));
1482
- case "MethodCallExpression": {
1483
- const consoleCall = this.consoleCall(node);
1484
- if (consoleCall) return consoleCall;
1398
+ case "CallExpression": {
1399
+ const lowered = this.loweredGlobalCall(node);
1485
1400
  const args = this.values(node.arguments);
1401
+ if (lowered) return call(calleePath(lowered.callee), [...lowered.prepend, ...args]);
1402
+ return call(object, args);
1403
+ }
1404
+ case "MethodCallExpression": {
1486
1405
  const lowered = this.loweredMethodCall(node);
1406
+ const args = this.values(node.arguments);
1487
1407
  if (lowered) {
1488
- return call(calleePath(lowered.callee), lowered.passReceiver ? [object, ...args] : args);
1408
+ return call(calleePath(lowered.callee), [
1409
+ ...lowered.prepend,
1410
+ ...lowered.passReceiver ? [object] : [],
1411
+ ...args
1412
+ ]);
1489
1413
  }
1490
1414
  if (!isLuauName(node.method.name)) {
1491
1415
  this.report(node.method, `'${node.method.name}' is a Luau keyword and cannot be called with ':'`);
@@ -1898,10 +1822,6 @@ function mentions(expression, name) {
1898
1822
  walk(expression);
1899
1823
  return found;
1900
1824
  }
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
1825
  function allNodes(root) {
1906
1826
  const out = [];
1907
1827
  const visit = (value) => {
@@ -1960,7 +1880,7 @@ import {
1960
1880
  ParseError,
1961
1881
  LexError
1962
1882
  } from "@tilua/parser";
1963
- import { parse as parseLuau2, parseExpressionFromSource, print } from "luau-parser";
1883
+ import { parse as parseLuau2, parseExpressionFromSource as parseExpressionFromSource2, print } from "luau-parser";
1964
1884
 
1965
1885
  // src/linemap.ts
1966
1886
  function statementsOf(node) {
@@ -2043,16 +1963,19 @@ async function bundle(options) {
2043
1963
  const file = pending.shift();
2044
1964
  if (modules.has(file)) continue;
2045
1965
  const source = sources.get(file);
1966
+ const shown = relative(root, file).replace(/\\/g, "/");
2046
1967
  const lowered = lower(source.program, source.scopes, {
2047
1968
  names,
2048
1969
  types: analysis.types.get(file),
2049
1970
  lowerings,
2050
1971
  target: options.target ?? config?.target,
2051
1972
  source: source.text,
1973
+ file: shown,
2052
1974
  module: {
2053
1975
  name: source.name,
2054
1976
  require: requireExpression,
2055
1977
  lines: `${G}.lines`,
1978
+ fail: `${G}.fail`,
2056
1979
  resolve: (specifier) => {
2057
1980
  const target = resolveModulePath(file, specifier, config);
2058
1981
  if (!target || target.endsWith(".d.tilua") || !sources.has(target)) return void 0;
@@ -2062,7 +1985,6 @@ async function bundle(options) {
2062
1985
  }
2063
1986
  });
2064
1987
  for (const d of lowered.diagnostics) diagnostics.push({ file, ...d, category: "module" });
2065
- const shown = relative(root, file).replace(/\\/g, "/");
2066
1988
  for (const statement of allStatements(lowered.statements)) {
2067
1989
  const line = lowered.origins.get(statement);
2068
1990
  if (line !== void 0) origins.set(statement, { file: shown, line });
@@ -2112,13 +2034,23 @@ async function bundle(options) {
2112
2034
  fields.push(field("load", functionExpression(functionBody([module.exportsName], module.statements, true))));
2113
2035
  modulesTable.fields.push({ type: "TableFieldComputed", key: string(module.name), value: table(fields) });
2114
2036
  }
2115
- const start = call(requireExpression, [string(sources.get(entry).name)]);
2116
- program2.body.statements.push(modules.get(entry).info.exports ? returns([start]) : callStatement(start));
2037
+ const ok = names.fresh("ok");
2038
+ const result = names.fresh("result");
2039
+ const entryStatements = parseLuau2(`
2040
+ local ${ok}, ${result} = xpcall(function()
2041
+ return ${G}.require(${JSON.stringify(sources.get(entry).name)})
2042
+ end, ${G}.fail)
2043
+ if not ${ok} then
2044
+ error(${result}, 0)
2045
+ end
2046
+ ${modules.get(entry).info.exports ? `return ${result}` : ""}
2047
+ `).body.statements;
2048
+ program2.body.statements.push(...entryStatements);
2117
2049
  const map = buildLineMap(program2, parseLuau2(print(program2)), origins);
2118
2050
  if (map.size) {
2119
- program2.body.statements.splice(program2.body.statements.length - 1, 0, assign(
2051
+ program2.body.statements.splice(program2.body.statements.length - entryStatements.length, 0, assign(
2120
2052
  [member(identifier(G), "lines")],
2121
- [parseExpressionFromSource(lineMapSource(map))]
2053
+ [parseExpressionFromSource2(lineMapSource(map))]
2122
2054
  ));
2123
2055
  }
2124
2056
  return { code: print(program2) + "\n", modules: moduleNames, diagnostics };
@@ -2128,6 +2060,52 @@ function runtime(G) {
2128
2060
  local ${G}
2129
2061
  ${G} = {
2130
2062
  modules = {},
2063
+ -- Filled in just before the entry runs: bundle line -> { file, line }.
2064
+ lines = nil,
2065
+ -- What an error nothing caught says, in the project's files: the bundle's
2066
+ -- own positions in the message are replaced by the places they came from,
2067
+ -- and a traceback of those places follows. A message that is not a string
2068
+ -- is left as it is, and a position that is not this bundle's (a library
2069
+ -- that raised its error with one already mapped) is left alone.
2070
+ fail = function(message)
2071
+ if type(message) ~= "string" then
2072
+ return message
2073
+ end
2074
+ local lines = ${G}.lines
2075
+ if lines == nil then
2076
+ return debug.traceback(message, 2)
2077
+ end
2078
+ local chunk
2079
+ if debug.info ~= nil then
2080
+ chunk = debug.info(1, "s")
2081
+ else
2082
+ chunk = debug.getinfo(1, "S").short_src
2083
+ end
2084
+ local prefix = chunk:gsub("%p", "%%%0")
2085
+ local function place(line)
2086
+ local origin = lines[tonumber(line)]
2087
+ if origin == nil then
2088
+ return nil
2089
+ end
2090
+ return origin[1] .. ":" .. tostring(origin[2])
2091
+ end
2092
+ local mapped = message:gsub(prefix .. ":(%d+)", function(line)
2093
+ return place(line)
2094
+ end)
2095
+ local frames = {}
2096
+ for frame in debug.traceback("", 2):gmatch("[^\\n]+") do
2097
+ local line = frame:match(prefix .. ":(%d+)")
2098
+ local at = line ~= nil and place(line) or nil
2099
+ if at ~= nil then
2100
+ local name = frame:match("function ([%w_.:]+)")
2101
+ frames[#frames + 1] = " at " .. at .. (name ~= nil and (" (" .. name .. ")") or "")
2102
+ end
2103
+ end
2104
+ if #frames == 0 then
2105
+ return mapped
2106
+ end
2107
+ return mapped .. "\\n" .. table.concat(frames, "\\n")
2108
+ end,
2131
2109
  records = {},
2132
2110
  require = function(name)
2133
2111
  local record = ${G}.records[name]