@silverbulletmd/silverbullet 2.10.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 (100) hide show
  1. package/README.md +4 -4
  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 +0 -4
  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/editor.ts +128 -53
  15. package/client/plugos/syscalls/event.ts +0 -2
  16. package/client/plugos/syscalls/fetch.ts +0 -4
  17. package/client/plugos/syscalls/icon.ts +46 -0
  18. package/client/plugos/syscalls/index.ts +33 -13
  19. package/client/plugos/syscalls/jsonschema.ts +0 -1
  20. package/client/plugos/syscalls/mq.ts +0 -1
  21. package/client/plugos/syscalls/navigator.ts +143 -0
  22. package/client/plugos/syscalls/search.ts +44 -0
  23. package/client/plugos/syscalls/shell.ts +0 -1
  24. package/client/plugos/syscalls/space.ts +250 -7
  25. package/client/plugos/syscalls/sync.ts +56 -4
  26. package/client/plugos/syscalls/system.ts +38 -0
  27. package/client/plugos/system.ts +7 -5
  28. package/client/plugos/worker_runtime.ts +0 -2
  29. package/client/space_lua/aggregates.ts +0 -16
  30. package/client/space_lua/ast.ts +0 -5
  31. package/client/space_lua/ast_narrow.ts +0 -5
  32. package/client/space_lua/budget.ts +113 -0
  33. package/client/space_lua/budget_ui.ts +49 -0
  34. package/client/space_lua/eval.ts +113 -88
  35. package/client/space_lua/labels.ts +0 -4
  36. package/client/space_lua/numeric.ts +0 -5
  37. package/client/space_lua/parse.ts +0 -17
  38. package/client/space_lua/quarantine.ts +127 -0
  39. package/client/space_lua/query_collection.ts +3 -47
  40. package/client/space_lua/query_env.ts +0 -2
  41. package/client/space_lua/render_lua_markdown.ts +2 -7
  42. package/client/space_lua/render_widget.ts +18 -1
  43. package/client/space_lua/runtime.ts +50 -70
  44. package/client/space_lua/stdlib/format.ts +0 -12
  45. package/client/space_lua/stdlib/js.ts +0 -2
  46. package/client/space_lua/stdlib/load.ts +0 -1
  47. package/client/space_lua/stdlib/math.ts +0 -7
  48. package/client/space_lua/stdlib/net.ts +0 -4
  49. package/client/space_lua/stdlib/os.ts +0 -10
  50. package/client/space_lua/stdlib/pattern.ts +10 -15
  51. package/client/space_lua/stdlib/space_lua.ts +1 -2
  52. package/client/space_lua/stdlib/string_pack.ts +0 -3
  53. package/client/space_lua/stdlib/table.ts +0 -5
  54. package/client/space_lua/stdlib.ts +18 -31
  55. package/client/space_lua/tonumber.ts +0 -9
  56. package/dist/plug-compile.js +2 -3
  57. package/package.json +13 -3
  58. package/plug-api/lib/async.ts +1 -6
  59. package/plug-api/lib/collation.ts +22 -0
  60. package/plug-api/lib/crypto.ts +16 -15
  61. package/plug-api/lib/dates.ts +33 -0
  62. package/plug-api/lib/fuzzy.ts +282 -0
  63. package/plug-api/lib/json.ts +0 -6
  64. package/plug-api/lib/limited_map.ts +0 -2
  65. package/plug-api/lib/link_write.ts +27 -0
  66. package/plug-api/lib/ref.ts +28 -5
  67. package/plug-api/lib/resolve.ts +0 -1
  68. package/plug-api/lib/resolve_path.ts +290 -0
  69. package/plug-api/lib/shortcut.ts +15 -0
  70. package/plug-api/lib/tags.ts +0 -3
  71. package/plug-api/lib/transclusion.ts +28 -1
  72. package/plug-api/lib/tree.ts +0 -5
  73. package/plug-api/lib/yaml.ts +3 -42
  74. package/plug-api/syscall.ts +1 -5
  75. package/plug-api/syscalls/config.ts +2 -2
  76. package/plug-api/syscalls/editor.ts +59 -213
  77. package/plug-api/syscalls/icon.ts +18 -0
  78. package/plug-api/syscalls/index.ts +22 -4
  79. package/plug-api/syscalls/search.ts +11 -0
  80. package/plug-api/syscalls/space.ts +122 -5
  81. package/plug-api/syscalls/sync.ts +23 -2
  82. package/plug-api/syscalls/system.ts +46 -0
  83. package/plug-api/syscalls.ts +2 -0
  84. package/plug-api/system_mock.ts +13 -0
  85. package/plug-api/types/client.ts +4 -3
  86. package/plug-api/types/datastore.ts +11 -2
  87. package/plug-api/types/index.ts +10 -0
  88. package/plug-api/types/profile.ts +13 -0
  89. package/plug-api/types/revisions.ts +67 -0
  90. package/plug-api/ui/description.ts +64 -0
  91. package/plug-api/ui/hover.ts +69 -0
  92. package/plug-api/ui/index.ts +58 -2
  93. package/plug-api/ui/scroll.ts +30 -0
  94. package/plug-api/ui/tree_model.ts +263 -0
  95. package/plug-api/ui/tree_types.ts +66 -0
  96. package/plug-api/ui/use_fit_collapse.ts +67 -0
  97. package/plugs/builtin_plugs.ts +0 -1
  98. package/plugs/index/types.ts +9 -0
  99. package/plug-api/lib/memory_cache.ts +0 -21
  100. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
@@ -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) {
@@ -1109,7 +1104,6 @@ export function evalExpression(
1109
1104
  );
1110
1105
 
1111
1106
  if (fromSource.kind === "cross") {
1112
- // Materialize Cartesian product, then query
1113
1107
  return (async () => {
1114
1108
  const rows = await evalCrossJoinSources(
1115
1109
  fromSource.sources,
@@ -1119,13 +1113,11 @@ export function evalExpression(
1119
1113
  );
1120
1114
  const collection: any = toCollection(rows);
1121
1115
 
1122
- // Build up query object
1123
1116
  const query: LuaCollectionQuery = {
1124
1117
  objectVariable: undefined,
1125
1118
  distinct: true,
1126
1119
  };
1127
1120
 
1128
- // Map clauses to query parameters
1129
1121
  for (const clause of q.clauses) {
1130
1122
  switch (clause.type) {
1131
1123
  case "Where": {
@@ -1179,12 +1171,11 @@ export function evalExpression(
1179
1171
  }
1180
1172
 
1181
1173
  return (collection as any)
1182
- .query(query, env, sf, globalThis.client?.config)
1174
+ .query(query, env, sf, getClientConfig())
1183
1175
  .then(jsToLuaValue);
1184
1176
  })();
1185
1177
  }
1186
1178
 
1187
- // Single-source
1188
1179
  const { objectVariable, expression: objectExpression } = fromSource;
1189
1180
  return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
1190
1181
  async (collection: LuaValue) => {
@@ -1200,33 +1191,27 @@ export function evalExpression(
1200
1191
  "query" in collection &&
1201
1192
  typeof (collection as any).query === "function"
1202
1193
  ) {
1203
- // Already queryable, use as-is
1204
1194
  } else if (collection instanceof LuaTable && collection.empty()) {
1205
- // Empty table → empty array
1206
1195
  collection = toCollection([]);
1207
1196
  } else if (collection instanceof LuaTable) {
1208
1197
  if (collection.length > 0) {
1209
- // Array-like table: extract array items, keep as LuaTables
1210
1198
  const arr: any[] = [];
1211
1199
  for (let i = 1; i <= collection.length; i++) {
1212
1200
  arr.push(collection.rawGet(i));
1213
1201
  }
1214
1202
  collection = toCollection(arr);
1215
1203
  } else {
1216
- // Record-like table (no array part): treat as singleton
1217
1204
  collection = toCollection([collection]);
1218
1205
  }
1219
1206
  } else {
1220
1207
  collection = toCollection(luaValueToJS(collection, sf));
1221
1208
  }
1222
1209
 
1223
- // Build up query object
1224
1210
  const query: LuaCollectionQuery = {
1225
1211
  objectVariable,
1226
1212
  distinct: true,
1227
1213
  };
1228
1214
 
1229
- // Map clauses to query parameters
1230
1215
  for (const clause of q.clauses) {
1231
1216
  switch (clause.type) {
1232
1217
  case "Where": {
@@ -1279,9 +1264,8 @@ export function evalExpression(
1279
1264
  }
1280
1265
  }
1281
1266
 
1282
- // Always use the possibly-wrapped collection
1283
1267
  return (collection as any)
1284
- .query(query, env, sf, globalThis.client?.config)
1268
+ .query(query, env, sf, getClientConfig())
1285
1269
  .then(jsToLuaValue);
1286
1270
  },
1287
1271
  );
@@ -1293,12 +1277,12 @@ export function evalExpression(
1293
1277
  );
1294
1278
  }
1295
1279
  } catch (err: any) {
1296
- // Repackage any non Lua-specific exceptions with some position information
1297
- if (!err.constructor.name.startsWith("Lua")) {
1298
- throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
1299
- } 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) {
1300
1283
  throw err;
1301
1284
  }
1285
+ throw new LuaRuntimeError(err.message, sf.withCtx(e.ctx), err);
1302
1286
  }
1303
1287
  }
1304
1288
 
@@ -1322,7 +1306,6 @@ function evalPrefixExpression(
1322
1306
  return rpThen(evalExpression(p.expression, env, sf), singleResult);
1323
1307
  }
1324
1308
 
1325
- // <<expr>>[<<expr>>]
1326
1309
  case "TableAccess": {
1327
1310
  const ta = asTableAccess(e);
1328
1311
  // Sync-first: evaluate object and key without allocating Promise when both are sync.
@@ -1342,7 +1325,6 @@ function evalPrefixExpression(
1342
1325
  );
1343
1326
  }
1344
1327
 
1345
- // <expr>.property
1346
1328
  case "PropertyAccess": {
1347
1329
  const pa = asPropertyAccess(e);
1348
1330
  // Sync-first: evaluate object; avoid Promise when object is sync.
@@ -1378,7 +1360,6 @@ function evalPrefixExpression(
1378
1360
  throw new LuaRuntimeError(nilMsg, sf.withCtx(fc.prefix.ctx));
1379
1361
  }
1380
1362
 
1381
- // Fast path: non-method call with sync prefix
1382
1363
  if (!fc.name && !isPromise(prefixValue)) {
1383
1364
  const argsVal = evalExpressions(fc.args, env, sf);
1384
1365
  if (!isPromise(argsVal)) {
@@ -1393,7 +1374,6 @@ function evalPrefixExpression(
1393
1374
  calleeVal: LuaValue,
1394
1375
  selfArgs: LuaValue[],
1395
1376
  ): LuaValue | Promise<LuaValue> => {
1396
- // Normal argument handling for hello:there(a, b, c) type calls
1397
1377
  if (fc.name) {
1398
1378
  const self = calleeVal;
1399
1379
  calleeVal = luaIndexValue(calleeVal, fc.name, sf);
@@ -1441,7 +1421,6 @@ function evalPrefixExpression(
1441
1421
  }
1442
1422
  }
1443
1423
 
1444
- // Helper functions to reduce duplication
1445
1424
  function evalMetamethod(
1446
1425
  left: any,
1447
1426
  right: any,
@@ -1466,7 +1445,6 @@ function evalMetamethod(
1466
1445
  }
1467
1446
  }
1468
1447
 
1469
- // Unary metamethod lookup and call
1470
1448
  function evalUnaryMetamethod(
1471
1449
  value: any,
1472
1450
  metaMethod: "__unm" | "__bnot",
@@ -1484,7 +1462,6 @@ function evalUnaryMetamethod(
1484
1462
  return luaCall(fn, [value], ctx, sf);
1485
1463
  }
1486
1464
 
1487
- // Unary metamethod handling (with fallback)
1488
1465
  function unaryWithMeta(
1489
1466
  arg: any,
1490
1467
  meta: "__unm" | "__bnot",
@@ -1502,7 +1479,6 @@ function unaryWithMeta(
1502
1479
  return fallback();
1503
1480
  }
1504
1481
 
1505
- // Logical short-circuit evaluation
1506
1482
  function evalLogical(
1507
1483
  op: "and" | "or",
1508
1484
  leftExpr: LuaExpression,
@@ -1771,12 +1747,10 @@ function luaRelWithMetamethod(
1771
1747
  * - throw error otherwise.
1772
1748
  */
1773
1749
  function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1774
- // Strings: ignore `__len`
1775
1750
  if (typeof val === "string") {
1776
1751
  return val.length;
1777
1752
  }
1778
1753
 
1779
- // Tables: prefer metatable `__len` to raw length
1780
1754
  if (val instanceof LuaTable) {
1781
1755
  const mt = getMetatable(val, sf);
1782
1756
  if (mt) {
@@ -1788,7 +1762,6 @@ function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1788
1762
  return val.length;
1789
1763
  }
1790
1764
 
1791
- // Other values: allow metatable `__len` first
1792
1765
  {
1793
1766
  const mt = getMetatable(val, sf);
1794
1767
  if (mt) {
@@ -1799,12 +1772,10 @@ function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1799
1772
  }
1800
1773
  }
1801
1774
 
1802
- // JS arrays (interop): length if no `__len` override
1803
1775
  if (Array.isArray(val)) {
1804
1776
  return val.length;
1805
1777
  }
1806
1778
 
1807
- // Otherwise error with type
1808
1779
  const t = luaTypeOf(val) as LuaType;
1809
1780
  throw new LuaRuntimeError(
1810
1781
  `attempt to get length of a ${t} value`,
@@ -1820,7 +1791,6 @@ function evalExpressions(
1820
1791
  const len = es.length;
1821
1792
  if (len === 0) return [];
1822
1793
 
1823
- // Evaluate all arguments (sync-first); avoid .map() closure overhead
1824
1794
  const parts = new Array(len);
1825
1795
  for (let i = 0; i < len; i++) {
1826
1796
  parts[i] = evalExpression(es[i], env, sf);
@@ -1831,11 +1801,9 @@ function evalExpressions(
1831
1801
  const finalize = (argsResolved: any[]) => {
1832
1802
  const out: LuaValue[] = [];
1833
1803
  const lastIdx = argsResolved.length - 1;
1834
- // All but last expression produce a single value
1835
1804
  for (let i = 0; i < lastIdx; i++) {
1836
1805
  out.push(singleResult(argsResolved[i]));
1837
1806
  }
1838
- // Last expression preserves multiple results
1839
1807
  const last = argsResolved[lastIdx];
1840
1808
  if (last instanceof LuaMultiRes) {
1841
1809
  out.push(...last.flatten().values);
@@ -1867,6 +1835,8 @@ function runStatementsNoGoto(
1867
1835
  const result = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1868
1836
  if (isPromise(result)) {
1869
1837
  return (result as Promise<any>).then((res) => {
1838
+ const budget = sf.threadState.budget;
1839
+ if (budget !== undefined) budget.awaited = true;
1870
1840
  if (res !== undefined) {
1871
1841
  if (isGotoSignal(res)) {
1872
1842
  throw new LuaRuntimeError(
@@ -1895,6 +1865,35 @@ function runStatementsNoGoto(
1895
1865
  return processFrom(startIdx);
1896
1866
  }
1897
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
+
1898
1897
  function withCloseBoundary(
1899
1898
  sf: LuaStackFrame,
1900
1899
  mark: number,
@@ -1915,17 +1914,7 @@ function withCloseBoundary(
1915
1914
  return isPromise(r) ? (r as Promise<void>).then(() => res) : res;
1916
1915
  };
1917
1916
 
1918
- const onRejected = (e: any) => {
1919
- const errObj: LuaValue =
1920
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
1921
- const r = luaCloseFromMark(sf, mark, errObj);
1922
- if (isPromise(r)) {
1923
- return (r as Promise<void>).then(() => {
1924
- throw e;
1925
- });
1926
- }
1927
- throw e;
1928
- };
1917
+ const onRejected = (e: any) => closeThenRethrow(sf, mark, e);
1929
1918
 
1930
1919
  return p.then(onFulfilled, onRejected);
1931
1920
  }
@@ -1965,6 +1954,8 @@ function evalBlockNoClose(
1965
1954
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1966
1955
  if (isPromise(r)) {
1967
1956
  return (r as Promise<any>).then((res) => {
1957
+ const budget = sf.threadState.budget;
1958
+ if (budget !== undefined) budget.awaited = true;
1968
1959
  if (isGotoSignal(res)) return res;
1969
1960
  if (res !== undefined) return res;
1970
1961
  return runFrom(i + 1);
@@ -2005,11 +1996,13 @@ function evalBlockNoClose(
2005
1996
  const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
2006
1997
  const stmts = b.statements;
2007
1998
 
1999
+ const budget = sf.threadState.budget;
2008
2000
  const runFrom = (i: number): EvalBlockResult => {
2009
2001
  for (; i < stmts.length; i++) {
2010
2002
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
2011
2003
  if (isPromise(r)) {
2012
2004
  return (r as Promise<any>).then((res) => {
2005
+ if (budget !== undefined) budget.awaited = true;
2013
2006
  const consumed = consumeGotoInBlock(res, meta!.labels);
2014
2007
  if (typeof consumed === "number") {
2015
2008
  return runFrom(consumed);
@@ -2022,6 +2015,13 @@ function evalBlockNoClose(
2022
2015
  }
2023
2016
  const consumed = consumeGotoInBlock(r, meta.labels);
2024
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
+ }
2025
2025
  i = consumed - 1;
2026
2026
  continue;
2027
2027
  }
@@ -2060,7 +2060,6 @@ export function evalStatement(
2060
2060
  );
2061
2061
 
2062
2062
  const apply = (values: LuaValue[], lvalues: { env: any; key: any }[]) => {
2063
- // Create the error-reporting frame once, not per-lvalue
2064
2063
  let errSf: LuaStackFrame | undefined;
2065
2064
  const ps: Promise<any>[] = [];
2066
2065
  for (let i = 0; i < lvalues.length; i++) {
@@ -2233,22 +2232,13 @@ export function evalStatement(
2233
2232
  try {
2234
2233
  out = evalBlockNoClose(b, env, sf, returnOnReturn);
2235
2234
  } catch (e: any) {
2236
- const errObj: LuaValue =
2237
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
2238
- const r = luaCloseFromMark(sf, mark, errObj);
2239
- if (isPromise(r)) {
2240
- return (r as Promise<void>).then(() => {
2241
- throw e;
2242
- });
2243
- }
2244
- throw e;
2235
+ return closeThenRethrow(sf, mark, e);
2245
2236
  }
2246
2237
 
2247
2238
  return withCloseBoundary(sf, mark, out);
2248
2239
  }
2249
2240
  case "If": {
2250
2241
  const iff = asIf(s);
2251
- // Evaluate conditions in order; avoid awaiting when not necessary
2252
2242
  const conds = iff.conditions;
2253
2243
 
2254
2244
  const runFrom = (
@@ -2279,6 +2269,7 @@ export function evalStatement(
2279
2269
  }
2280
2270
  case "While": {
2281
2271
  const w = asWhile(s);
2272
+ const budget = sf.threadState.budget;
2282
2273
 
2283
2274
  // Sync-first loop that re-enters sync mode after each async iteration
2284
2275
  const runSyncFirst = ():
@@ -2286,9 +2277,14 @@ export function evalStatement(
2286
2277
  | ControlSignal
2287
2278
  | Promise<undefined | ControlSignal> => {
2288
2279
  while (true) {
2280
+ if (budget !== undefined && --budget.ticks <= 0) {
2281
+ const y = budgetTick(budget);
2282
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2283
+ }
2289
2284
  const c = evalExpression(w.condition, env, sf);
2290
2285
  if (isPromise(c)) {
2291
2286
  return (c as Promise<any>).then((cv) => {
2287
+ if (budget !== undefined) budget.awaited = true;
2292
2288
  if (!luaTruthy(cv)) return;
2293
2289
  return rpThen(
2294
2290
  evalStatement(w.block, env, sf, returnOnReturn),
@@ -2305,6 +2301,7 @@ export function evalStatement(
2305
2301
  const r = evalStatement(w.block, env, sf, returnOnReturn);
2306
2302
  if (isPromise(r)) {
2307
2303
  return (r as Promise<any>).then((res) => {
2304
+ if (budget !== undefined) budget.awaited = true;
2308
2305
  if (res !== undefined) {
2309
2306
  return isBreakSignal(res) ? undefined : res;
2310
2307
  }
@@ -2323,6 +2320,7 @@ export function evalStatement(
2323
2320
  }
2324
2321
  case "Repeat": {
2325
2322
  const rep = asRepeat(s);
2323
+ const budget = sf.threadState.budget;
2326
2324
 
2327
2325
  // Sync-first loop that re-enters sync mode after each async iteration
2328
2326
  const runSyncFirst = ():
@@ -2330,9 +2328,14 @@ export function evalStatement(
2330
2328
  | ControlSignal
2331
2329
  | Promise<undefined | ControlSignal> => {
2332
2330
  while (true) {
2331
+ if (budget !== undefined && --budget.ticks <= 0) {
2332
+ const y = budgetTick(budget);
2333
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2334
+ }
2333
2335
  const rr = evalStatement(rep.block, env, sf, returnOnReturn);
2334
2336
  if (isPromise(rr)) {
2335
2337
  return (rr as Promise<any>).then((res) => {
2338
+ if (budget !== undefined) budget.awaited = true;
2336
2339
  if (res !== undefined) {
2337
2340
  return isBreakSignal(res) ? undefined : res;
2338
2341
  }
@@ -2348,9 +2351,10 @@ export function evalStatement(
2348
2351
 
2349
2352
  const c = evalExpression(rep.condition, env, sf);
2350
2353
  if (isPromise(c)) {
2351
- return (c as Promise<any>).then((cv) =>
2352
- luaTruthy(cv) ? undefined : runSyncFirst(),
2353
- );
2354
+ return (c as Promise<any>).then((cv) => {
2355
+ if (budget !== undefined) budget.awaited = true;
2356
+ return luaTruthy(cv) ? undefined : runSyncFirst();
2357
+ });
2354
2358
  }
2355
2359
  if (luaTruthy(c)) break;
2356
2360
  }
@@ -2375,7 +2379,6 @@ export function evalStatement(
2375
2379
  let body = fn.body;
2376
2380
  let propNames = fn.name.propNames;
2377
2381
  if (fn.name.colonName) {
2378
- // function hello:there() -> function hello.there(self) transformation
2379
2382
  body = {
2380
2383
  ...fn.body,
2381
2384
  parameters: ["self", ...fn.body.parameters],
@@ -2502,10 +2505,22 @@ export function evalStatement(
2502
2505
  const shouldContinue =
2503
2506
  step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2504
2507
 
2508
+ const budget = sf.threadState.budget;
2509
+
2505
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
+ }
2506
2520
  const r = executeIteration(loopEnv, i, loopType);
2507
2521
  if (isPromise(r)) {
2508
2522
  return (r as Promise<any>).then((res) => {
2523
+ if (budget !== undefined) budget.awaited = true;
2509
2524
  if (res !== undefined) {
2510
2525
  return isBreakSignal(res) ? undefined : res;
2511
2526
  }
@@ -2534,11 +2549,22 @@ export function evalStatement(
2534
2549
  step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2535
2550
 
2536
2551
  const loopEnv = new LuaEnv(env);
2552
+ const budget = sf.threadState.budget;
2537
2553
 
2538
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
+ }
2539
2564
  const r = executeIteration(loopEnv, i, loopType);
2540
2565
  if (isPromise(r)) {
2541
2566
  return (r as Promise<any>).then((res) => {
2567
+ if (budget !== undefined) budget.awaited = true;
2542
2568
  if (res !== undefined) {
2543
2569
  if (isBreakSignal(res)) {
2544
2570
  return;
@@ -2573,6 +2599,8 @@ export function evalStatement(
2573
2599
  isPromise(endV) ? endV : Promise.resolve(endV),
2574
2600
  isPromise(stepV) ? stepV : Promise.resolve(stepV),
2575
2601
  ]).then(([start, end, step]) => {
2602
+ const budget = sf.threadState.budget;
2603
+ if (budget !== undefined) budget.awaited = true;
2576
2604
  return runSyncFirst(
2577
2605
  untagNumber(start) as number,
2578
2606
  untagNumber(end) as number,
@@ -2602,8 +2630,6 @@ export function evalStatement(
2602
2630
  const afterExprs = (resolved: any[]) => {
2603
2631
  const iteratorMultiRes = new LuaMultiRes(resolved).flatten();
2604
2632
  let iteratorValue: ILuaFunction | any = iteratorMultiRes.values[0];
2605
- // Handle the case where the iterator is a table and we need
2606
- // to call the `each` function.
2607
2633
  if (Array.isArray(iteratorValue) || iteratorValue instanceof LuaTable) {
2608
2634
  iteratorValue = (env.get("each") as ILuaFunction).call(
2609
2635
  sf,
@@ -2630,26 +2656,14 @@ export function evalStatement(
2630
2656
  luaMarkToBeClosed(sf, closing, fi.ctx);
2631
2657
  }
2632
2658
 
2633
- const errObjFrom = (e: any): LuaValue =>
2634
- e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
2635
-
2636
2659
  const finish = (res: any) => {
2637
2660
  const r = luaCloseFromMark(sf, mark, null);
2638
2661
  return isPromise(r) ? (r as Promise<void>).then(() => res) : res;
2639
2662
  };
2640
2663
 
2641
- const finishErr = (e: any): Promise<never> | never => {
2642
- const errObj = errObjFrom(e);
2643
- const r = luaCloseFromMark(sf, mark, errObj);
2644
- if (isPromise(r)) {
2645
- return (r as Promise<void>).then(() => {
2646
- throw e;
2647
- });
2648
- }
2649
- throw e;
2650
- };
2664
+ const finishErr = (e: any): Promise<never> | never =>
2665
+ closeThenRethrow(sf, mark, e);
2651
2666
 
2652
- // Allocate the reusable env once before the loop
2653
2667
  const loopEnv = canReuseEnv ? new LuaEnv(env) : null;
2654
2668
 
2655
2669
  const makeIterEnv = (): LuaEnv => {
@@ -2659,9 +2673,14 @@ export function evalStatement(
2659
2673
  return new LuaEnv(env);
2660
2674
  };
2661
2675
 
2676
+ const budget = sf.threadState.budget;
2662
2677
  // Sync-first loop that re-enters sync mode after each async iteration
2663
2678
  const runSyncFirst = (): any => {
2664
2679
  while (true) {
2680
+ if (budget !== undefined && --budget.ticks <= 0) {
2681
+ const y = budgetTick(budget);
2682
+ if (y !== undefined) return rpThen(y, () => runSyncFirst());
2683
+ }
2665
2684
  const iterCall = luaCall(
2666
2685
  iteratorValue,
2667
2686
  [state, control],
@@ -2670,6 +2689,7 @@ export function evalStatement(
2670
2689
  );
2671
2690
 
2672
2691
  const afterIterCall = (itv: any): any => {
2692
+ if (budget !== undefined) budget.awaited = true;
2673
2693
  const iterResult = new LuaMultiRes(itv).flatten();
2674
2694
  const nextControl = iterResult.values[0];
2675
2695
  if (nextControl === null || nextControl === undefined) {
@@ -2712,6 +2732,7 @@ export function evalStatement(
2712
2732
  if (isPromise(r)) {
2713
2733
  return (r as Promise<any>)
2714
2734
  .then((res) => {
2735
+ if (budget !== undefined) budget.awaited = true;
2715
2736
  if (res !== undefined) {
2716
2737
  if (isBreakSignal(res)) {
2717
2738
  return finish(undefined);
@@ -2739,7 +2760,11 @@ export function evalStatement(
2739
2760
  };
2740
2761
 
2741
2762
  if (isPromise(exprVals)) {
2742
- 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
+ });
2743
2768
  }
2744
2769
  return afterExprs(exprVals as any[]);
2745
2770
  }
@@ -35,7 +35,6 @@ class LabelResolveError extends Error {
35
35
  }
36
36
  }
37
37
 
38
- // Cache
39
38
  const functionMetaByRoot = new WeakMap<LuaBlock, FunctionMeta>();
40
39
  const functionMetaByAnyBlock = new WeakMap<LuaBlock, FunctionMeta>();
41
40
 
@@ -107,11 +106,9 @@ function resolveFunction(root: LuaBlock): FunctionMeta {
107
106
  roleByBlock,
108
107
  );
109
108
 
110
- // Validate gotos
111
109
  for (const g of vctx.gotos) {
112
110
  const target = g.node.name;
113
111
 
114
- // Search current block for the label, then ancestors
115
112
  let searchBlock: LuaBlock | undefined = g.block;
116
113
  let labelIndex: number | undefined;
117
114
  let labelDefBlock: LuaBlock | undefined;
@@ -151,7 +148,6 @@ function resolveFunction(root: LuaBlock): FunctionMeta {
151
148
  );
152
149
  }
153
150
 
154
- // Local scope forward jump check
155
151
  let entersLocalScope = false;
156
152
  let entersCloseScope = false;
157
153