@silverbulletmd/silverbullet 2.9.0 → 2.11.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.
Files changed (123) hide show
  1. package/README.md +38 -25
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +24 -14
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/asset.ts +61 -23
  15. package/client/plugos/syscalls/clientStore.ts +32 -6
  16. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  17. package/client/plugos/syscalls/code_widget.ts +26 -11
  18. package/client/plugos/syscalls/config.ts +146 -30
  19. package/client/plugos/syscalls/datastore.ts +146 -40
  20. package/client/plugos/syscalls/editor.ts +1518 -722
  21. package/client/plugos/syscalls/event.ts +44 -13
  22. package/client/plugos/syscalls/fetch.ts +120 -83
  23. package/client/plugos/syscalls/icon.ts +46 -0
  24. package/client/plugos/syscalls/index.ts +267 -121
  25. package/client/plugos/syscalls/jsonschema.ts +107 -9
  26. package/client/plugos/syscalls/language.ts +38 -12
  27. package/client/plugos/syscalls/markdown.ts +166 -50
  28. package/client/plugos/syscalls/mq.ts +92 -20
  29. package/client/plugos/syscalls/navigator.ts +143 -0
  30. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  31. package/client/plugos/syscalls/search.ts +44 -0
  32. package/client/plugos/syscalls/service_registry.ts +74 -20
  33. package/client/plugos/syscalls/shell.ts +51 -21
  34. package/client/plugos/syscalls/space.ts +412 -113
  35. package/client/plugos/syscalls/sync.ts +103 -20
  36. package/client/plugos/syscalls/system.ts +274 -139
  37. package/client/plugos/system.ts +28 -10
  38. package/client/plugos/worker_runtime.ts +0 -2
  39. package/client/space_lua/aggregates.ts +0 -16
  40. package/client/space_lua/api_documentation.ts +107 -0
  41. package/client/space_lua/ast.ts +13 -5
  42. package/client/space_lua/ast_narrow.ts +0 -5
  43. package/client/space_lua/budget.ts +113 -0
  44. package/client/space_lua/budget_ui.ts +49 -0
  45. package/client/space_lua/eval.ts +125 -92
  46. package/client/space_lua/labels.ts +0 -4
  47. package/client/space_lua/numeric.ts +0 -5
  48. package/client/space_lua/parse.ts +243 -125
  49. package/client/space_lua/pretty_print.ts +128 -54
  50. package/client/space_lua/quarantine.ts +127 -0
  51. package/client/space_lua/query_collection.ts +7 -51
  52. package/client/space_lua/query_env.ts +0 -2
  53. package/client/space_lua/render_lua_markdown.ts +2 -7
  54. package/client/space_lua/render_widget.ts +173 -0
  55. package/client/space_lua/runtime.ts +133 -92
  56. package/client/space_lua/stdlib/crypto.ts +8 -3
  57. package/client/space_lua/stdlib/encoding.ts +27 -9
  58. package/client/space_lua/stdlib/format.ts +0 -12
  59. package/client/space_lua/stdlib/js.ts +136 -23
  60. package/client/space_lua/stdlib/load.ts +24 -16
  61. package/client/space_lua/stdlib/math.ts +331 -133
  62. package/client/space_lua/stdlib/net.ts +57 -13
  63. package/client/space_lua/stdlib/os.ts +111 -54
  64. package/client/space_lua/stdlib/pattern.ts +10 -15
  65. package/client/space_lua/stdlib/space_lua.ts +344 -21
  66. package/client/space_lua/stdlib/string.ts +282 -104
  67. package/client/space_lua/stdlib/string_pack.ts +58 -22
  68. package/client/space_lua/stdlib/table.ts +195 -45
  69. package/client/space_lua/stdlib.ts +354 -125
  70. package/client/space_lua/syscalls.ts +270 -0
  71. package/client/space_lua/tonumber.ts +0 -9
  72. package/dist/plug-compile.js +2 -3
  73. package/package.json +15 -5
  74. package/plug-api/lib/async.ts +1 -6
  75. package/plug-api/lib/collation.ts +22 -0
  76. package/plug-api/lib/crypto.ts +16 -15
  77. package/plug-api/lib/dates.ts +33 -0
  78. package/plug-api/lib/fuzzy.ts +282 -0
  79. package/plug-api/lib/json.ts +0 -6
  80. package/plug-api/lib/limited_map.ts +0 -2
  81. package/plug-api/lib/link_write.ts +27 -0
  82. package/plug-api/{ui → lib}/panel_styles.ts +4 -3
  83. package/plug-api/lib/ref.ts +124 -5
  84. package/plug-api/lib/resolve.ts +0 -1
  85. package/plug-api/lib/resolve_path.ts +290 -0
  86. package/plug-api/lib/shortcut.ts +17 -1
  87. package/plug-api/lib/tags.ts +0 -3
  88. package/plug-api/lib/transclusion.ts +28 -1
  89. package/plug-api/lib/tree.ts +0 -5
  90. package/plug-api/lib/yaml.ts +3 -42
  91. package/plug-api/syscall.ts +1 -5
  92. package/plug-api/syscalls/config.ts +7 -5
  93. package/plug-api/syscalls/editor.ts +70 -208
  94. package/plug-api/syscalls/icon.ts +18 -0
  95. package/plug-api/syscalls/index.ts +39 -5
  96. package/plug-api/syscalls/jsonschema.ts +9 -0
  97. package/plug-api/syscalls/lua.ts +7 -0
  98. package/plug-api/syscalls/search.ts +11 -0
  99. package/plug-api/syscalls/shell.ts +0 -1
  100. package/plug-api/syscalls/space.ts +122 -5
  101. package/plug-api/syscalls/sync.ts +23 -2
  102. package/plug-api/syscalls/system.ts +53 -0
  103. package/plug-api/syscalls.ts +2 -0
  104. package/plug-api/system_mock.ts +15 -2
  105. package/plug-api/types/client.ts +4 -3
  106. package/plug-api/types/datastore.ts +11 -2
  107. package/plug-api/types/index.ts +62 -1
  108. package/plug-api/types/manifest.ts +22 -4
  109. package/plug-api/types/profile.ts +13 -0
  110. package/plug-api/types/revisions.ts +67 -0
  111. package/plug-api/ui/cx.ts +1 -3
  112. package/plug-api/ui/description.ts +64 -0
  113. package/plug-api/ui/hover.ts +69 -0
  114. package/plug-api/ui/index.ts +61 -2
  115. package/plug-api/ui/scroll.ts +30 -0
  116. package/plug-api/ui/slugify.ts +44 -0
  117. package/plug-api/ui/tree_model.ts +263 -0
  118. package/plug-api/ui/tree_types.ts +66 -0
  119. package/plug-api/ui/use_fit_collapse.ts +67 -0
  120. package/plugs/builtin_plugs.ts +0 -1
  121. package/plugs/index/types.ts +9 -0
  122. package/client/plugos/syscalls/lua.ts +0 -81
  123. package/plug-api/lib/memory_cache.ts +0 -21
@@ -7,7 +7,9 @@ import type {
7
7
  LuaTableField,
8
8
  NumericType,
9
9
  } from "./ast.ts";
10
+ import type { Config } from "../config.ts";
10
11
  import { LuaAttribute } from "./ast.ts";
12
+ import { budgetTick, LuaBudgetStopped } from "./budget.ts";
11
13
  import { evalPromiseValues } from "./util.ts";
12
14
  import {
13
15
  getMetatable,
@@ -91,6 +93,11 @@ import { getBlockGotoMeta } from "./labels.ts";
91
93
 
92
94
  const astNumberKindCache = new WeakMap<LuaExpression, NumericType>();
93
95
 
96
+ function getClientConfig(): Config | undefined {
97
+ return (globalThis as typeof globalThis & { client?: { config: Config } })
98
+ .client?.config;
99
+ }
100
+
94
101
  function astNumberKind(e: LuaExpression | undefined): NumericType | undefined {
95
102
  if (!e) return undefined;
96
103
 
@@ -258,7 +265,6 @@ export function luaOp(
258
265
  ): any {
259
266
  switch (op) {
260
267
  case "+": {
261
- // Ultra-fast path: both plain numbers with no float type annotation (int + int)
262
268
  if (
263
269
  typeof left === "number" &&
264
270
  typeof right === "number" &&
@@ -308,11 +314,9 @@ export function luaOp(
308
314
  );
309
315
  }
310
316
  case "..": {
311
- // Fast path: string .. string (most common in SilverBullet — key building, templates)
312
317
  if (typeof left === "string" && typeof right === "string") {
313
318
  return left + right;
314
319
  }
315
- // Fast path: string .. number or number .. string
316
320
  if (typeof left === "string" && typeof right === "number") {
317
321
  return left + luaFormatNumber(right);
318
322
  }
@@ -352,7 +356,6 @@ export function luaOp(
352
356
  }
353
357
  }
354
358
  case "==": {
355
- // Fast path for same-type primitives
356
359
  if (typeof left === typeof right && typeof left !== "object") {
357
360
  return left === right;
358
361
  }
@@ -370,11 +373,9 @@ export function luaOp(
370
373
  return !luaEqWithMetamethod(left, right, ctx, sf);
371
374
  }
372
375
  case "<": {
373
- // Fast path: both plain numbers
374
376
  if (typeof left === "number" && typeof right === "number") {
375
377
  return left < right;
376
378
  }
377
- // Fast path: both strings
378
379
  if (typeof left === "string" && typeof right === "string") {
379
380
  return left < right;
380
381
  }
@@ -404,7 +405,6 @@ export function luaOp(
404
405
  }
405
406
  }
406
407
 
407
- // Remaining operators: //, %, bitwise
408
408
  const handler = operatorsMetaMethods[op];
409
409
  if (!handler) {
410
410
  throw new LuaRuntimeError(`Unknown operator ${op}`, sf.withCtx(ctx));
@@ -820,7 +820,6 @@ async function evalCrossJoinSources(
820
820
  sf: LuaStackFrame,
821
821
  ctx: ASTCtx,
822
822
  ): Promise<LuaTable[]> {
823
- // Evaluate each source and normalize to arrays
824
823
  const arrays: { name: string; items: any[] }[] = [];
825
824
  for (const src of sources) {
826
825
  const val = await evalExpression(src.expression, env, sf);
@@ -834,7 +833,6 @@ async function evalCrossJoinSources(
834
833
  arrays.push({ name: src.name, items });
835
834
  }
836
835
 
837
- // Cartesian product
838
836
  let product: Record<string, any>[] = [{}];
839
837
  for (const { name, items } of arrays) {
840
838
  const newProduct: Record<string, any>[] = [];
@@ -846,7 +844,6 @@ async function evalCrossJoinSources(
846
844
  product = newProduct;
847
845
  }
848
846
 
849
- // Convert each combination to a `LuaTable` row
850
847
  return product.map((combo) => {
851
848
  const row = new LuaTable();
852
849
  for (const key in combo) {
@@ -894,7 +891,6 @@ export function evalExpression(
894
891
  case "Unary": {
895
892
  const u = asUnary(e);
896
893
 
897
- // Fast path: negation of numeric literal
898
894
  if (u.operator === "-" && u.argument.type === "Number") {
899
895
  const num = u.argument;
900
896
  if (num.value === 0) {
@@ -914,7 +910,6 @@ export function evalExpression(
914
910
  const arg = singleResult(typed.value);
915
911
 
916
912
  return unaryWithMeta(arg, "__unm", u.ctx, sf, () => {
917
- // Numeric-string coercion for unary minus
918
913
  if (typeof arg === "string") {
919
914
  const n = coerceToNumber(arg);
920
915
  if (n === null) {
@@ -1015,7 +1010,8 @@ export function evalExpression(
1015
1010
  case "Variable":
1016
1011
  case "FunctionCall":
1017
1012
  case "TableAccess":
1018
- case "PropertyAccess": {
1013
+ case "PropertyAccess":
1014
+ case "Parenthesized": {
1019
1015
  return evalPrefixExpression(e, env, sf);
1020
1016
  }
1021
1017
  case "TableConstructor": {
@@ -1108,7 +1104,6 @@ export function evalExpression(
1108
1104
  );
1109
1105
 
1110
1106
  if (fromSource.kind === "cross") {
1111
- // Materialize Cartesian product, then query
1112
1107
  return (async () => {
1113
1108
  const rows = await evalCrossJoinSources(
1114
1109
  fromSource.sources,
@@ -1118,13 +1113,11 @@ export function evalExpression(
1118
1113
  );
1119
1114
  const collection: any = toCollection(rows);
1120
1115
 
1121
- // Build up query object
1122
1116
  const query: LuaCollectionQuery = {
1123
1117
  objectVariable: undefined,
1124
1118
  distinct: true,
1125
1119
  };
1126
1120
 
1127
- // Map clauses to query parameters
1128
1121
  for (const clause of q.clauses) {
1129
1122
  switch (clause.type) {
1130
1123
  case "Where": {
@@ -1178,12 +1171,11 @@ export function evalExpression(
1178
1171
  }
1179
1172
 
1180
1173
  return (collection as any)
1181
- .query(query, env, sf, globalThis.client?.config)
1174
+ .query(query, env, sf, getClientConfig())
1182
1175
  .then(jsToLuaValue);
1183
1176
  })();
1184
1177
  }
1185
1178
 
1186
- // Single-source
1187
1179
  const { objectVariable, expression: objectExpression } = fromSource;
1188
1180
  return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
1189
1181
  async (collection: LuaValue) => {
@@ -1199,33 +1191,27 @@ export function evalExpression(
1199
1191
  "query" in collection &&
1200
1192
  typeof (collection as any).query === "function"
1201
1193
  ) {
1202
- // Already queryable, use as-is
1203
1194
  } else if (collection instanceof LuaTable && collection.empty()) {
1204
- // Empty table → empty array
1205
1195
  collection = toCollection([]);
1206
1196
  } else if (collection instanceof LuaTable) {
1207
1197
  if (collection.length > 0) {
1208
- // Array-like table: extract array items, keep as LuaTables
1209
1198
  const arr: any[] = [];
1210
1199
  for (let i = 1; i <= collection.length; i++) {
1211
1200
  arr.push(collection.rawGet(i));
1212
1201
  }
1213
1202
  collection = toCollection(arr);
1214
1203
  } else {
1215
- // Record-like table (no array part): treat as singleton
1216
1204
  collection = toCollection([collection]);
1217
1205
  }
1218
1206
  } else {
1219
1207
  collection = toCollection(luaValueToJS(collection, sf));
1220
1208
  }
1221
1209
 
1222
- // Build up query object
1223
1210
  const query: LuaCollectionQuery = {
1224
1211
  objectVariable,
1225
1212
  distinct: true,
1226
1213
  };
1227
1214
 
1228
- // Map clauses to query parameters
1229
1215
  for (const clause of q.clauses) {
1230
1216
  switch (clause.type) {
1231
1217
  case "Where": {
@@ -1278,9 +1264,8 @@ export function evalExpression(
1278
1264
  }
1279
1265
  }
1280
1266
 
1281
- // Always use the possibly-wrapped collection
1282
1267
  return (collection as any)
1283
- .query(query, env, sf, globalThis.client?.config)
1268
+ .query(query, env, sf, getClientConfig())
1284
1269
  .then(jsToLuaValue);
1285
1270
  },
1286
1271
  );
@@ -1292,12 +1277,12 @@ export function evalExpression(
1292
1277
  );
1293
1278
  }
1294
1279
  } catch (err: any) {
1295
- // Repackage any non Lua-specific exceptions with some position information
1296
- if (!err.constructor.name.startsWith("Lua")) {
1297
- throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
1298
- } else {
1280
+ // instanceof, not constructor.name: the production build minifies class
1281
+ // names, so a name-based test silently fails in the shipped client.
1282
+ if (err instanceof LuaRuntimeError || err instanceof LuaBudgetStopped) {
1299
1283
  throw err;
1300
1284
  }
1285
+ throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
1301
1286
  }
1302
1287
  }
1303
1288
 
@@ -1318,10 +1303,9 @@ function evalPrefixExpression(
1318
1303
 
1319
1304
  case "Parenthesized": {
1320
1305
  const p = asParenthesized(e);
1321
- return evalExpression(p.expression, env, sf);
1306
+ return rpThen(evalExpression(p.expression, env, sf), singleResult);
1322
1307
  }
1323
1308
 
1324
- // <<expr>>[<<expr>>]
1325
1309
  case "TableAccess": {
1326
1310
  const ta = asTableAccess(e);
1327
1311
  // Sync-first: evaluate object and key without allocating Promise when both are sync.
@@ -1341,7 +1325,6 @@ function evalPrefixExpression(
1341
1325
  );
1342
1326
  }
1343
1327
 
1344
- // <expr>.property
1345
1328
  case "PropertyAccess": {
1346
1329
  const pa = asPropertyAccess(e);
1347
1330
  // Sync-first: evaluate object; avoid Promise when object is sync.
@@ -1377,7 +1360,6 @@ function evalPrefixExpression(
1377
1360
  throw new LuaRuntimeError(nilMsg, sf.withCtx(fc.prefix.ctx));
1378
1361
  }
1379
1362
 
1380
- // Fast path: non-method call with sync prefix
1381
1363
  if (!fc.name && !isPromise(prefixValue)) {
1382
1364
  const argsVal = evalExpressions(fc.args, env, sf);
1383
1365
  if (!isPromise(argsVal)) {
@@ -1392,7 +1374,6 @@ function evalPrefixExpression(
1392
1374
  calleeVal: LuaValue,
1393
1375
  selfArgs: LuaValue[],
1394
1376
  ): LuaValue | Promise<LuaValue> => {
1395
- // Normal argument handling for hello:there(a, b, c) type calls
1396
1377
  if (fc.name) {
1397
1378
  const self = calleeVal;
1398
1379
  calleeVal = luaIndexValue(calleeVal, fc.name, sf);
@@ -1440,7 +1421,6 @@ function evalPrefixExpression(
1440
1421
  }
1441
1422
  }
1442
1423
 
1443
- // Helper functions to reduce duplication
1444
1424
  function evalMetamethod(
1445
1425
  left: any,
1446
1426
  right: any,
@@ -1465,7 +1445,6 @@ function evalMetamethod(
1465
1445
  }
1466
1446
  }
1467
1447
 
1468
- // Unary metamethod lookup and call
1469
1448
  function evalUnaryMetamethod(
1470
1449
  value: any,
1471
1450
  metaMethod: "__unm" | "__bnot",
@@ -1483,7 +1462,6 @@ function evalUnaryMetamethod(
1483
1462
  return luaCall(fn, [value], ctx, sf);
1484
1463
  }
1485
1464
 
1486
- // Unary metamethod handling (with fallback)
1487
1465
  function unaryWithMeta(
1488
1466
  arg: any,
1489
1467
  meta: "__unm" | "__bnot",
@@ -1501,7 +1479,6 @@ function unaryWithMeta(
1501
1479
  return fallback();
1502
1480
  }
1503
1481
 
1504
- // Logical short-circuit evaluation
1505
1482
  function evalLogical(
1506
1483
  op: "and" | "or",
1507
1484
  leftExpr: LuaExpression,
@@ -1770,12 +1747,10 @@ function luaRelWithMetamethod(
1770
1747
  * - throw error otherwise.
1771
1748
  */
1772
1749
  function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1773
- // Strings: ignore `__len`
1774
1750
  if (typeof val === "string") {
1775
1751
  return val.length;
1776
1752
  }
1777
1753
 
1778
- // Tables: prefer metatable `__len` to raw length
1779
1754
  if (val instanceof LuaTable) {
1780
1755
  const mt = getMetatable(val, sf);
1781
1756
  if (mt) {
@@ -1787,7 +1762,6 @@ function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1787
1762
  return val.length;
1788
1763
  }
1789
1764
 
1790
- // Other values: allow metatable `__len` first
1791
1765
  {
1792
1766
  const mt = getMetatable(val, sf);
1793
1767
  if (mt) {
@@ -1798,12 +1772,10 @@ function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1798
1772
  }
1799
1773
  }
1800
1774
 
1801
- // JS arrays (interop): length if no `__len` override
1802
1775
  if (Array.isArray(val)) {
1803
1776
  return val.length;
1804
1777
  }
1805
1778
 
1806
- // Otherwise error with type
1807
1779
  const t = luaTypeOf(val) as LuaType;
1808
1780
  throw new LuaRuntimeError(
1809
1781
  `attempt to get length of a ${t} value`,
@@ -1819,7 +1791,6 @@ function evalExpressions(
1819
1791
  const len = es.length;
1820
1792
  if (len === 0) return [];
1821
1793
 
1822
- // Evaluate all arguments (sync-first); avoid .map() closure overhead
1823
1794
  const parts = new Array(len);
1824
1795
  for (let i = 0; i < len; i++) {
1825
1796
  parts[i] = evalExpression(es[i], env, sf);
@@ -1830,11 +1801,9 @@ function evalExpressions(
1830
1801
  const finalize = (argsResolved: any[]) => {
1831
1802
  const out: LuaValue[] = [];
1832
1803
  const lastIdx = argsResolved.length - 1;
1833
- // All but last expression produce a single value
1834
1804
  for (let i = 0; i < lastIdx; i++) {
1835
1805
  out.push(singleResult(argsResolved[i]));
1836
1806
  }
1837
- // Last expression preserves multiple results
1838
1807
  const last = argsResolved[lastIdx];
1839
1808
  if (last instanceof LuaMultiRes) {
1840
1809
  out.push(...last.flatten().values);
@@ -1866,6 +1835,8 @@ function runStatementsNoGoto(
1866
1835
  const result = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1867
1836
  if (isPromise(result)) {
1868
1837
  return (result as Promise<any>).then((res) => {
1838
+ const budget = sf.threadState.budget;
1839
+ if (budget !== undefined) budget.awaited = true;
1869
1840
  if (res !== undefined) {
1870
1841
  if (isGotoSignal(res)) {
1871
1842
  throw new LuaRuntimeError(
@@ -1894,6 +1865,35 @@ function runStatementsNoGoto(
1894
1865
  return processFrom(startIdx);
1895
1866
  }
1896
1867
 
1868
+ function closeThenRethrow(
1869
+ sf: LuaStackFrame,
1870
+ mark: number,
1871
+ e: any,
1872
+ ): never | Promise<never> {
1873
+ const errObj: LuaValue =
1874
+ e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
1875
+ // Lua 5.4 lets an error from __close replace the one being unwound, but a
1876
+ // user-initiated stop is not a Lua error and outranks it.
1877
+ const preferOriginal = (closeErr: any): never => {
1878
+ if (e instanceof LuaBudgetStopped) {
1879
+ throw e;
1880
+ }
1881
+ throw closeErr;
1882
+ };
1883
+ let r: void | Promise<void>;
1884
+ try {
1885
+ r = luaCloseFromMark(sf, mark, errObj);
1886
+ } catch (closeErr: any) {
1887
+ return preferOriginal(closeErr);
1888
+ }
1889
+ if (isPromise(r)) {
1890
+ return (r as Promise<void>).then(() => {
1891
+ throw e;
1892
+ }, preferOriginal);
1893
+ }
1894
+ throw e;
1895
+ }
1896
+
1897
1897
  function withCloseBoundary(
1898
1898
  sf: LuaStackFrame,
1899
1899
  mark: number,
@@ -1914,17 +1914,7 @@ function withCloseBoundary(
1914
1914
  return isPromise(r) ? (r as Promise<void>).then(() => res) : res;
1915
1915
  };
1916
1916
 
1917
- const onRejected = (e: any) => {
1918
- const errObj: LuaValue =
1919
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
1920
- const r = luaCloseFromMark(sf, mark, errObj);
1921
- if (isPromise(r)) {
1922
- return (r as Promise<void>).then(() => {
1923
- throw e;
1924
- });
1925
- }
1926
- throw e;
1927
- };
1917
+ const onRejected = (e: any) => closeThenRethrow(sf, mark, e);
1928
1918
 
1929
1919
  return p.then(onFulfilled, onRejected);
1930
1920
  }
@@ -1964,6 +1954,8 @@ function evalBlockNoClose(
1964
1954
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1965
1955
  if (isPromise(r)) {
1966
1956
  return (r as Promise<any>).then((res) => {
1957
+ const budget = sf.threadState.budget;
1958
+ if (budget !== undefined) budget.awaited = true;
1967
1959
  if (isGotoSignal(res)) return res;
1968
1960
  if (res !== undefined) return res;
1969
1961
  return runFrom(i + 1);
@@ -2004,11 +1996,13 @@ function evalBlockNoClose(
2004
1996
  const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
2005
1997
  const stmts = b.statements;
2006
1998
 
1999
+ const budget = sf.threadState.budget;
2007
2000
  const runFrom = (i: number): EvalBlockResult => {
2008
2001
  for (; i < stmts.length; i++) {
2009
2002
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
2010
2003
  if (isPromise(r)) {
2011
2004
  return (r as Promise<any>).then((res) => {
2005
+ if (budget !== undefined) budget.awaited = true;
2012
2006
  const consumed = consumeGotoInBlock(res, meta!.labels);
2013
2007
  if (typeof consumed === "number") {
2014
2008
  return runFrom(consumed);
@@ -2021,6 +2015,13 @@ function evalBlockNoClose(
2021
2015
  }
2022
2016
  const consumed = consumeGotoInBlock(r, meta.labels);
2023
2017
  if (typeof consumed === "number") {
2018
+ if (budget !== undefined && --budget.ticks <= 0) {
2019
+ const y = budgetTick(budget);
2020
+ if (y !== undefined) {
2021
+ const resumeAt = consumed;
2022
+ return rpThen(y, () => runFrom(resumeAt));
2023
+ }
2024
+ }
2024
2025
  i = consumed - 1;
2025
2026
  continue;
2026
2027
  }
@@ -2059,10 +2060,15 @@ export function evalStatement(
2059
2060
  );
2060
2061
 
2061
2062
  const apply = (values: LuaValue[], lvalues: { env: any; key: any }[]) => {
2062
- // Create the error-reporting frame once, not per-lvalue
2063
2063
  let errSf: LuaStackFrame | undefined;
2064
2064
  const ps: Promise<any>[] = [];
2065
2065
  for (let i = 0; i < lvalues.length; i++) {
2066
+ if (
2067
+ values[i] instanceof LuaFunction &&
2068
+ typeof lvalues[i].key === "string"
2069
+ ) {
2070
+ values[i].info.name ??= lvalues[i].key;
2071
+ }
2066
2072
  const r = luaSet(
2067
2073
  lvalues[i].env,
2068
2074
  lvalues[i].key,
@@ -2115,6 +2121,7 @@ export function evalStatement(
2115
2121
  }
2116
2122
 
2117
2123
  const bindOne = (name: any, v: LuaValue) => {
2124
+ if (v instanceof LuaFunction) v.info.name ??= name.name;
2118
2125
  const isConst = name.attributes?.includes(LuaAttribute.Const) === true;
2119
2126
  const isClose = name.attributes?.includes(LuaAttribute.Close) === true;
2120
2127
 
@@ -2225,22 +2232,13 @@ export function evalStatement(
2225
2232
  try {
2226
2233
  out = evalBlockNoClose(b, env, sf, returnOnReturn);
2227
2234
  } catch (e: any) {
2228
- const errObj: LuaValue =
2229
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
2230
- const r = luaCloseFromMark(sf, mark, errObj);
2231
- if (isPromise(r)) {
2232
- return (r as Promise<void>).then(() => {
2233
- throw e;
2234
- });
2235
- }
2236
- throw e;
2235
+ return closeThenRethrow(sf, mark, e);
2237
2236
  }
2238
2237
 
2239
2238
  return withCloseBoundary(sf, mark, out);
2240
2239
  }
2241
2240
  case "If": {
2242
2241
  const iff = asIf(s);
2243
- // Evaluate conditions in order; avoid awaiting when not necessary
2244
2242
  const conds = iff.conditions;
2245
2243
 
2246
2244
  const runFrom = (
@@ -2271,6 +2269,7 @@ export function evalStatement(
2271
2269
  }
2272
2270
  case "While": {
2273
2271
  const w = asWhile(s);
2272
+ const budget = sf.threadState.budget;
2274
2273
 
2275
2274
  // Sync-first loop that re-enters sync mode after each async iteration
2276
2275
  const runSyncFirst = ():
@@ -2278,9 +2277,14 @@ export function evalStatement(
2278
2277
  | ControlSignal
2279
2278
  | Promise<undefined | ControlSignal> => {
2280
2279
  while (true) {
2280
+ if (budget !== undefined && --budget.ticks <= 0) {
2281
+ const y = budgetTick(budget);
2282
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2283
+ }
2281
2284
  const c = evalExpression(w.condition, env, sf);
2282
2285
  if (isPromise(c)) {
2283
2286
  return (c as Promise<any>).then((cv) => {
2287
+ if (budget !== undefined) budget.awaited = true;
2284
2288
  if (!luaTruthy(cv)) return;
2285
2289
  return rpThen(
2286
2290
  evalStatement(w.block, env, sf, returnOnReturn),
@@ -2297,6 +2301,7 @@ export function evalStatement(
2297
2301
  const r = evalStatement(w.block, env, sf, returnOnReturn);
2298
2302
  if (isPromise(r)) {
2299
2303
  return (r as Promise<any>).then((res) => {
2304
+ if (budget !== undefined) budget.awaited = true;
2300
2305
  if (res !== undefined) {
2301
2306
  return isBreakSignal(res) ? undefined : res;
2302
2307
  }
@@ -2315,6 +2320,7 @@ export function evalStatement(
2315
2320
  }
2316
2321
  case "Repeat": {
2317
2322
  const rep = asRepeat(s);
2323
+ const budget = sf.threadState.budget;
2318
2324
 
2319
2325
  // Sync-first loop that re-enters sync mode after each async iteration
2320
2326
  const runSyncFirst = ():
@@ -2322,9 +2328,14 @@ export function evalStatement(
2322
2328
  | ControlSignal
2323
2329
  | Promise<undefined | ControlSignal> => {
2324
2330
  while (true) {
2331
+ if (budget !== undefined && --budget.ticks <= 0) {
2332
+ const y = budgetTick(budget);
2333
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2334
+ }
2325
2335
  const rr = evalStatement(rep.block, env, sf, returnOnReturn);
2326
2336
  if (isPromise(rr)) {
2327
2337
  return (rr as Promise<any>).then((res) => {
2338
+ if (budget !== undefined) budget.awaited = true;
2328
2339
  if (res !== undefined) {
2329
2340
  return isBreakSignal(res) ? undefined : res;
2330
2341
  }
@@ -2340,9 +2351,10 @@ export function evalStatement(
2340
2351
 
2341
2352
  const c = evalExpression(rep.condition, env, sf);
2342
2353
  if (isPromise(c)) {
2343
- return (c as Promise<any>).then((cv) =>
2344
- luaTruthy(cv) ? undefined : runSyncFirst(),
2345
- );
2354
+ return (c as Promise<any>).then((cv) => {
2355
+ if (budget !== undefined) budget.awaited = true;
2356
+ return luaTruthy(cv) ? undefined : runSyncFirst();
2357
+ });
2346
2358
  }
2347
2359
  if (luaTruthy(c)) break;
2348
2360
  }
@@ -2367,7 +2379,6 @@ export function evalStatement(
2367
2379
  let body = fn.body;
2368
2380
  let propNames = fn.name.propNames;
2369
2381
  if (fn.name.colonName) {
2370
- // function hello:there() -> function hello.there(self) transformation
2371
2382
  body = {
2372
2383
  ...fn.body,
2373
2384
  parameters: ["self", ...fn.body.parameters],
@@ -2386,13 +2397,13 @@ export function evalStatement(
2386
2397
  }
2387
2398
  (settable as any).set(
2388
2399
  propNames[propNames.length - 1],
2389
- new LuaFunction(body, env),
2400
+ new LuaFunction(body, env, { name: propNames.join(".") }),
2390
2401
  );
2391
2402
  return;
2392
2403
  }
2393
2404
  case "LocalFunction": {
2394
2405
  const lf = asLocalFunction(s);
2395
- env.setLocal(lf.name, new LuaFunction(lf.body, env));
2406
+ env.setLocal(lf.name, new LuaFunction(lf.body, env, { name: lf.name }));
2396
2407
  return;
2397
2408
  }
2398
2409
  case "Return": {
@@ -2494,10 +2505,22 @@ export function evalStatement(
2494
2505
  const shouldContinue =
2495
2506
  step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2496
2507
 
2508
+ const budget = sf.threadState.budget;
2509
+
2497
2510
  for (let i = startIndex; shouldContinue(i); i += step) {
2511
+ if (budget !== undefined && --budget.ticks <= 0) {
2512
+ const y = budgetTick(budget);
2513
+ if (y !== undefined) {
2514
+ const resumeAt = i;
2515
+ return rpThen(y, () =>
2516
+ runFromIndex(loopEnv, end, step, resumeAt, loopType),
2517
+ );
2518
+ }
2519
+ }
2498
2520
  const r = executeIteration(loopEnv, i, loopType);
2499
2521
  if (isPromise(r)) {
2500
2522
  return (r as Promise<any>).then((res) => {
2523
+ if (budget !== undefined) budget.awaited = true;
2501
2524
  if (res !== undefined) {
2502
2525
  return isBreakSignal(res) ? undefined : res;
2503
2526
  }
@@ -2526,11 +2549,22 @@ export function evalStatement(
2526
2549
  step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2527
2550
 
2528
2551
  const loopEnv = new LuaEnv(env);
2552
+ const budget = sf.threadState.budget;
2529
2553
 
2530
2554
  for (let i = start; shouldContinue(i); i += step) {
2555
+ if (budget !== undefined && --budget.ticks <= 0) {
2556
+ const y = budgetTick(budget);
2557
+ if (y !== undefined) {
2558
+ const resumeAt = i;
2559
+ return rpThen(y, () =>
2560
+ runSyncFirst(resumeAt, end, step, loopType),
2561
+ );
2562
+ }
2563
+ }
2531
2564
  const r = executeIteration(loopEnv, i, loopType);
2532
2565
  if (isPromise(r)) {
2533
2566
  return (r as Promise<any>).then((res) => {
2567
+ if (budget !== undefined) budget.awaited = true;
2534
2568
  if (res !== undefined) {
2535
2569
  if (isBreakSignal(res)) {
2536
2570
  return;
@@ -2565,6 +2599,8 @@ export function evalStatement(
2565
2599
  isPromise(endV) ? endV : Promise.resolve(endV),
2566
2600
  isPromise(stepV) ? stepV : Promise.resolve(stepV),
2567
2601
  ]).then(([start, end, step]) => {
2602
+ const budget = sf.threadState.budget;
2603
+ if (budget !== undefined) budget.awaited = true;
2568
2604
  return runSyncFirst(
2569
2605
  untagNumber(start) as number,
2570
2606
  untagNumber(end) as number,
@@ -2594,8 +2630,6 @@ export function evalStatement(
2594
2630
  const afterExprs = (resolved: any[]) => {
2595
2631
  const iteratorMultiRes = new LuaMultiRes(resolved).flatten();
2596
2632
  let iteratorValue: ILuaFunction | any = iteratorMultiRes.values[0];
2597
- // Handle the case where the iterator is a table and we need
2598
- // to call the `each` function.
2599
2633
  if (Array.isArray(iteratorValue) || iteratorValue instanceof LuaTable) {
2600
2634
  iteratorValue = (env.get("each") as ILuaFunction).call(
2601
2635
  sf,
@@ -2622,26 +2656,14 @@ export function evalStatement(
2622
2656
  luaMarkToBeClosed(sf, closing, fi.ctx);
2623
2657
  }
2624
2658
 
2625
- const errObjFrom = (e: any): LuaValue =>
2626
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
2627
-
2628
2659
  const finish = (res: any) => {
2629
2660
  const r = luaCloseFromMark(sf, mark, null);
2630
2661
  return isPromise(r) ? (r as Promise<void>).then(() => res) : res;
2631
2662
  };
2632
2663
 
2633
- const finishErr = (e: any): Promise<never> | never => {
2634
- const errObj = errObjFrom(e);
2635
- const r = luaCloseFromMark(sf, mark, errObj);
2636
- if (isPromise(r)) {
2637
- return (r as Promise<void>).then(() => {
2638
- throw e;
2639
- });
2640
- }
2641
- throw e;
2642
- };
2664
+ const finishErr = (e: any): Promise<never> | never =>
2665
+ closeThenRethrow(sf, mark, e);
2643
2666
 
2644
- // Allocate the reusable env once before the loop
2645
2667
  const loopEnv = canReuseEnv ? new LuaEnv(env) : null;
2646
2668
 
2647
2669
  const makeIterEnv = (): LuaEnv => {
@@ -2651,9 +2673,14 @@ export function evalStatement(
2651
2673
  return new LuaEnv(env);
2652
2674
  };
2653
2675
 
2676
+ const budget = sf.threadState.budget;
2654
2677
  // Sync-first loop that re-enters sync mode after each async iteration
2655
2678
  const runSyncFirst = (): any => {
2656
2679
  while (true) {
2680
+ if (budget !== undefined && --budget.ticks <= 0) {
2681
+ const y = budgetTick(budget);
2682
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2683
+ }
2657
2684
  const iterCall = luaCall(
2658
2685
  iteratorValue,
2659
2686
  [state, control],
@@ -2662,6 +2689,7 @@ export function evalStatement(
2662
2689
  );
2663
2690
 
2664
2691
  const afterIterCall = (itv: any): any => {
2692
+ if (budget !== undefined) budget.awaited = true;
2665
2693
  const iterResult = new LuaMultiRes(itv).flatten();
2666
2694
  const nextControl = iterResult.values[0];
2667
2695
  if (nextControl === null || nextControl === undefined) {
@@ -2704,6 +2732,7 @@ export function evalStatement(
2704
2732
  if (isPromise(r)) {
2705
2733
  return (r as Promise<any>)
2706
2734
  .then((res) => {
2735
+ if (budget !== undefined) budget.awaited = true;
2707
2736
  if (res !== undefined) {
2708
2737
  if (isBreakSignal(res)) {
2709
2738
  return finish(undefined);
@@ -2731,7 +2760,11 @@ export function evalStatement(
2731
2760
  };
2732
2761
 
2733
2762
  if (isPromise(exprVals)) {
2734
- return (exprVals as Promise<any[]>).then(afterExprs);
2763
+ return (exprVals as Promise<any[]>).then((v) => {
2764
+ const budget = sf.threadState.budget;
2765
+ if (budget !== undefined) budget.awaited = true;
2766
+ return afterExprs(v);
2767
+ });
2735
2768
  }
2736
2769
  return afterExprs(exprVals as any[]);
2737
2770
  }