@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
@@ -1,4 +1,5 @@
1
1
  import type { ASTCtx, LuaFunctionBody, NumericType } from "./ast.ts";
2
+ import type { LuaBudget } from "./budget.ts";
2
3
  import { evalStatement } from "./eval.ts";
3
4
  import { asyncQuickSort } from "./util.ts";
4
5
  import { isPromise, rpAll } from "./rp.ts";
@@ -10,6 +11,14 @@ import type {
10
11
  LuaFunctionInfo,
11
12
  } from "../../plug-api/types/index.ts";
12
13
 
14
+ let boundaryBudgetFactory: (() => LuaBudget) | undefined;
15
+
16
+ export function setBoundaryBudgetFactory(
17
+ f: (() => LuaBudget) | undefined,
18
+ ): void {
19
+ boundaryBudgetFactory = f;
20
+ }
21
+
13
22
  export type LuaType =
14
23
  | "nil"
15
24
  | "boolean"
@@ -20,7 +29,6 @@ export type LuaType =
20
29
  | "userdata"
21
30
  | "thread";
22
31
 
23
- // These types are for documentation only
24
32
  export type LuaValue = any;
25
33
  export type JSValue = any;
26
34
 
@@ -53,7 +61,6 @@ export interface ILuaGettable {
53
61
  getNumericType?(key: LuaValue): NumericType | undefined;
54
62
  }
55
63
 
56
- // Small helpers for type safety/readability
57
64
  export function isILuaFunction(v: unknown): v is ILuaFunction {
58
65
  return !!v && typeof (v as any).call === "function";
59
66
  }
@@ -81,11 +88,11 @@ const EMPTY_CTX = {} as ASTCtx;
81
88
 
82
89
  const MAX_TAG_LOOP = 200;
83
90
 
84
- // Close-stack support
85
91
  export type LuaCloseEntry = { value: LuaValue; ctx: ASTCtx };
86
92
 
87
93
  type LuaThreadState = {
88
94
  closeStack?: LuaCloseEntry[];
95
+ budget?: LuaBudget;
89
96
  };
90
97
 
91
98
  function isLuaNumber(v: any): boolean {
@@ -336,14 +343,10 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
336
343
  }
337
344
 
338
345
  get(name: string, _sf?: LuaStackFrame): Promise<LuaValue> | LuaValue | null {
339
- // Fast path: single Map.get() instead of has() + get() for the common case
340
- // where the variable exists and has a non-undefined value (null = Lua nil is fine).
341
346
  const v = this.variables.get(name);
342
347
  if (v !== undefined) {
343
348
  return v;
344
349
  }
345
- // Variable set to null (Lua nil) returns null from Map.get() which is !== undefined,
346
- // so it's handled above. Only fall through when the key is truly absent.
347
350
  if (this.parent) {
348
351
  return this.parent.get(name, _sf);
349
352
  }
@@ -488,39 +491,31 @@ export class LuaFunction implements ILuaFunction {
488
491
  }
489
492
 
490
493
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
491
- // Create a new environment that chains to the captured environment
492
494
  const env = new LuaEnv(this.capturedEnv);
493
- // Set _CTX to the thread local environment from the stack frame
494
495
  env.setLocal("_CTX", sf.threadLocal);
495
496
 
496
- // Eval using a stack frame that knows the current function
497
497
  const sfWithFn = sf.currentFunction === this ? sf : sf.withFunction(this);
498
498
 
499
499
  const params = this.body.parameters;
500
500
  const hasVarargs = params.length > 0 && params[params.length - 1] === "...";
501
501
 
502
- // Resolve args (sync-first)
503
502
  const argsRP = rpAll(args as any[]);
504
503
  const resolveArgs = (resolvedArgs: any[]) => {
505
504
  if (hasVarargs) {
506
- // Variadic function: bind named params, collect rest into varargs
507
505
  const varargStart = params.length - 1;
508
506
  for (let i = 0; i < varargStart; i++) {
509
507
  env.setLocal(params[i], resolvedArgs[i] ?? null);
510
508
  }
511
509
  env.setLocal("...", new LuaMultiRes(resolvedArgs.slice(varargStart)));
512
510
  } else {
513
- // Non-variadic: bind all named params directly
514
511
  for (let i = 0; i < params.length; i++) {
515
512
  env.setLocal(params[i], resolvedArgs[i] ?? null);
516
513
  }
517
514
  }
518
515
 
519
- // Evaluate the function body with returnOnReturn set to true
520
516
  const r = evalStatement(this.body.block, env, sfWithFn, true);
521
517
 
522
518
  if (!isPromise(r)) {
523
- // Fast path: synchronous result
524
519
  if (r === undefined) return;
525
520
  if (r && typeof r === "object" && r.ctrl === "return") {
526
521
  return mapFunctionReturnValue(r.values);
@@ -675,14 +670,10 @@ export class LuaBuiltinFunction implements ILuaFunction {
675
670
  }
676
671
 
677
672
  export class LuaTable implements ILuaSettable, ILuaGettable {
678
- // To optimize the table implementation we use a combination of different data structures
679
673
  public metatable: LuaTable | null;
680
674
 
681
- // When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
682
675
  private stringKeys: Record<string, any>;
683
- // Other keys we can support using a Map as a fallback
684
676
  private otherKeys: Map<any, any> | null;
685
- // When tables are used as arrays, we use a native JavaScript array for that
686
677
  private arrayPart: any[];
687
678
 
688
679
  // Numeric type metadata at storage boundaries (lazily allocated)
@@ -719,15 +710,12 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
719
710
 
720
711
  const numVal = LuaTable.numKeyValue(key);
721
712
  if (numVal !== null) {
722
- // Normalize -0 to +0
723
713
  if (isNegativeZero(numVal)) {
724
714
  return 0;
725
715
  }
726
- // Integer-valued numbers normalize to integers
727
716
  if (Number.isInteger(numVal)) {
728
717
  return numVal;
729
718
  }
730
- // Non-integer floats
731
719
  return numVal;
732
720
  }
733
721
  return key;
@@ -799,7 +787,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
799
787
  return this.stringKeys[key] !== undefined;
800
788
  }
801
789
 
802
- // Fast path for plain integer keys (common in for loops)
803
790
  if (typeof key === "number") {
804
791
  if (key >= 1 && (key | 0) === key) {
805
792
  if (this.arrayPart[key - 1] !== undefined) return true;
@@ -892,7 +879,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
892
879
  return value.then((v) => this.rawSet(key, v, numType));
893
880
  }
894
881
 
895
- // Fast path: string keys (the dominant case)
896
882
  if (typeof key === "string") {
897
883
  if (value === null || value === undefined) {
898
884
  delete this.stringKeys[key];
@@ -951,7 +937,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
951
937
  this.promoteIntegerKeysFromHash();
952
938
  }
953
939
 
954
- // Trailing nil shrink
955
940
  if (value === null || value === undefined) {
956
941
  let n = this.arrayPart.length;
957
942
  while (n > 0) {
@@ -971,7 +956,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
971
956
  return;
972
957
  }
973
958
 
974
- // Sparse numeric key
975
959
  if (!this.otherKeys) {
976
960
  this.otherKeys = new Map();
977
961
  }
@@ -1033,12 +1017,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1033
1017
  );
1034
1018
  }
1035
1019
 
1036
- // Fast path: no metatable — skip has() check and metamethod machinery
1037
1020
  if (this.metatable === null) {
1038
1021
  return this.rawSet(key, value, numType);
1039
1022
  }
1040
1023
 
1041
- // Key exists — rawSet directly, no metamethod needed
1042
1024
  if (this.has(key)) {
1043
1025
  return this.rawSet(key, value, numType);
1044
1026
  }
@@ -1049,7 +1031,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1049
1031
  return this.rawSet(key, value, numType);
1050
1032
  }
1051
1033
 
1052
- // Slow path: __newindex metamethod chain (rare)
1053
1034
  const errSf = sf || LuaStackFrame.lostFrame;
1054
1035
  const ctx = sf?.astCtx ?? EMPTY_CTX;
1055
1036
  const k: LuaValue = key;
@@ -1078,12 +1059,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1078
1059
  return t.rawSet(k, v, nt);
1079
1060
  }
1080
1061
 
1081
- // Function metamethod: call and stop
1082
1062
  if (typeof mm === "function" || isILuaFunction(mm)) {
1083
1063
  return luaCall(mm, [t, k, v], ctx, errSf);
1084
1064
  }
1085
1065
 
1086
- // Table/env metamethod: forward assignment
1087
1066
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1088
1067
  target = mm;
1089
1068
  continue;
@@ -1113,7 +1092,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1113
1092
  if (typeof key === "string") {
1114
1093
  return this.stringKeyTypes ? this.stringKeyTypes.get(key) : undefined;
1115
1094
  }
1116
- // Fast path for plain integer keys
1117
1095
  if (typeof key === "number") {
1118
1096
  if (key >= 1 && (key | 0) === key) {
1119
1097
  return this.arrayTypes ? this.arrayTypes[key - 1] : undefined;
@@ -1136,7 +1114,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1136
1114
  return this.stringKeys[key];
1137
1115
  }
1138
1116
 
1139
- // Fast path for plain integer keys (common in for loops)
1140
1117
  if (typeof key === "number") {
1141
1118
  if (key >= 1 && (key | 0) === key) {
1142
1119
  const v = this.arrayPart[key - 1];
@@ -1144,7 +1121,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1144
1121
  if (this.otherKeys) return this.otherKeys.get(key);
1145
1122
  return undefined;
1146
1123
  }
1147
- // Non-integer or zero/negative number keys
1148
1124
  if (this.otherKeys) return this.otherKeys.get(key);
1149
1125
  return undefined;
1150
1126
  }
@@ -1178,7 +1154,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1178
1154
  }
1179
1155
 
1180
1156
  get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null {
1181
- // Fast path: no metatable means rawGet is the final answer (most tables in SilverBullet)
1182
1157
  if (this.metatable === null) {
1183
1158
  const raw = this.rawGet(key);
1184
1159
  return raw !== undefined ? raw : null;
@@ -1212,11 +1187,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1212
1187
  result[k] = luaValueToJS(this.stringKeys[k], sf);
1213
1188
  }
1214
1189
  }
1215
- // Include array part with 1-based keys
1216
1190
  for (let i = 0; i < this.arrayPart.length; i++) {
1217
1191
  result[i + 1] = luaValueToJS(this.arrayPart[i], sf);
1218
1192
  }
1219
- // Include other keys
1220
1193
  if (this.otherKeys) {
1221
1194
  for (const [key, val] of this.otherKeys) {
1222
1195
  result[key] = luaValueToJS(val, sf);
@@ -1299,14 +1272,12 @@ export function luaIndexValue(
1299
1272
  let t: LuaValue = value;
1300
1273
 
1301
1274
  for (let loop = 0; loop < MAX_TAG_LOOP; loop++) {
1302
- // Primitive get when table
1303
1275
  if (t instanceof LuaTable) {
1304
1276
  const raw = t.rawGet(key);
1305
1277
  if (raw !== undefined) {
1306
1278
  if (isSqlNull(raw)) return null;
1307
1279
  return raw;
1308
1280
  }
1309
- // If no metatable, raw miss => nil
1310
1281
  if (t.metatable === null) {
1311
1282
  return null;
1312
1283
  }
@@ -1328,18 +1299,15 @@ export function luaIndexValue(
1328
1299
  );
1329
1300
  }
1330
1301
 
1331
- // Function metamethod
1332
1302
  if (typeof mm === "function" || isILuaFunction(mm)) {
1333
1303
  return luaCall(mm, [t, key], ctx, errSf);
1334
1304
  }
1335
1305
 
1336
- // Table/metatable delegation: repeat with mm as new "t"
1337
1306
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1338
1307
  t = mm;
1339
1308
  continue;
1340
1309
  }
1341
1310
 
1342
- // Bad metamethod type: make it a Lua-like type error
1343
1311
  const ty = luaTypeOf(mm) as any as string;
1344
1312
  throw new LuaRuntimeError(
1345
1313
  `attempt to index a ${ty} value`,
@@ -1413,11 +1381,9 @@ export function luaGet(
1413
1381
  if (isTaggedFloat(key)) {
1414
1382
  return (obj as any[])[key.value - 1] ?? null;
1415
1383
  }
1416
- // Native JS object
1417
1384
  const k = toNumKey(key);
1418
1385
  const val = (obj as Record<string | number, any>)[k];
1419
1386
  if (typeof val === "function") {
1420
- // Automatically bind the function to the object
1421
1387
  return val.bind(obj);
1422
1388
  }
1423
1389
  if (val === undefined) {
@@ -1438,7 +1404,6 @@ export function luaLen(
1438
1404
  return obj.length;
1439
1405
  }
1440
1406
  if (obj instanceof LuaTable) {
1441
- // Check __len metamethod unless raw access is requested
1442
1407
  if (!raw) {
1443
1408
  const mt = getMetatable(obj, sf || LuaStackFrame.lostFrame);
1444
1409
  const mm = mt ? mt.rawGet("__len") : null;
@@ -1473,7 +1438,6 @@ export function luaCall(
1473
1438
  );
1474
1439
  }
1475
1440
 
1476
- // Fast path: native JS function
1477
1441
  if (typeof callee === "function") {
1478
1442
  const baseSf = sf || LuaStackFrame.lostFrame;
1479
1443
  const len = args.length;
@@ -1494,7 +1458,6 @@ export function luaCall(
1494
1458
  return (callee as (...a: any[]) => any)(...args);
1495
1459
  }
1496
1460
 
1497
- // Lua table: may be callable via __call metamethod
1498
1461
  if (callee instanceof LuaTable) {
1499
1462
  const metatable = getMetatable(callee, sf);
1500
1463
 
@@ -1525,7 +1488,6 @@ export function luaCall(
1525
1488
  }
1526
1489
  }
1527
1490
 
1528
- // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1529
1491
  if (isILuaFunction(callee)) {
1530
1492
  // Fast path for builtins: skip withFunction, defer withCtx
1531
1493
  if (callee instanceof LuaBuiltinFunction) {
@@ -1550,7 +1512,6 @@ export function luaCall(
1550
1512
  }
1551
1513
 
1552
1514
  export function luaEquals(a: any, b: any): boolean {
1553
- // Normalize nil variants (null, undefined, SQL NULL) to null
1554
1515
  const an =
1555
1516
  a === null || a === undefined || isSqlNull(a)
1556
1517
  ? null
@@ -1630,7 +1591,6 @@ export class LuaRuntimeError extends Error {
1630
1591
  if (!ctx || !ctx.from || !ctx.to) {
1631
1592
  break;
1632
1593
  }
1633
- // Find the line and column
1634
1594
  let line = 1;
1635
1595
  let column = 0;
1636
1596
  let lastNewline = -1;
@@ -1644,7 +1604,6 @@ export class LuaRuntimeError extends Error {
1644
1604
  }
1645
1605
  }
1646
1606
 
1647
- // Get the full line of code for context
1648
1607
  const lineStart = lastNewline + 1;
1649
1608
  const lineEnd = code.indexOf("\n", ctx.from);
1650
1609
  const codeLine = code.substring(
@@ -1652,7 +1611,6 @@ export class LuaRuntimeError extends Error {
1652
1611
  lineEnd === -1 ? undefined : lineEnd,
1653
1612
  );
1654
1613
 
1655
- // Add position indicator
1656
1614
  const pointer = `${" ".repeat(column)}^`;
1657
1615
 
1658
1616
  traceStr +=
@@ -1704,12 +1662,10 @@ export function luaToString(
1704
1662
  return luaFormatNumber(value);
1705
1663
  }
1706
1664
 
1707
- // Check for circular references
1708
1665
  if (typeof value === "object" && visited.has(value)) {
1709
1666
  return "<circular reference>";
1710
1667
  }
1711
1668
  if ((value as any).toStringAsync) {
1712
- // Add to visited before recursing
1713
1669
  visited.add(value);
1714
1670
  return (value as any).toStringAsync();
1715
1671
  }
@@ -1721,15 +1677,12 @@ export function luaToString(
1721
1677
  // Don't recurse into the function body, just show the function signature
1722
1678
  return `<lua-function (${value.body.parameters.join(", ")})>`;
1723
1679
  }
1724
- // Handle plain JavaScript objects in a Lua-like format
1725
1680
  if (typeof value === "object") {
1726
- // Add to visited before recursing
1727
1681
  visited.add(value);
1728
1682
  return (async () => {
1729
1683
  let result = "{";
1730
1684
  let first = true;
1731
1685
 
1732
- // Handle arrays
1733
1686
  if (Array.isArray(value)) {
1734
1687
  for (const val of value) {
1735
1688
  if (first) {
@@ -1737,14 +1690,12 @@ export function luaToString(
1737
1690
  } else {
1738
1691
  result += ", ";
1739
1692
  }
1740
- // Recursively stringify the value, passing the visited set
1741
1693
  const strVal = await luaToString(val, visited);
1742
1694
  result += strVal;
1743
1695
  }
1744
1696
  return `${result}}`;
1745
1697
  }
1746
1698
 
1747
- // Handle objects
1748
1699
  for (const [key, val] of Object.entries(value)) {
1749
1700
  if (first) {
1750
1701
  first = false;
@@ -1756,7 +1707,6 @@ export function luaToString(
1756
1707
  } else {
1757
1708
  result += `["${key}"] = `;
1758
1709
  }
1759
- // Recursively stringify the value, passing the visited set
1760
1710
  const strVal = await luaToString(val, visited);
1761
1711
  result += strVal;
1762
1712
  }
@@ -1777,7 +1727,6 @@ export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1777
1727
  if (n === 0) {
1778
1728
  return 1 / n === -Infinity ? "-0.0" : "0.0";
1779
1729
  }
1780
- // Delegate to luaFormat for `%.14g`
1781
1730
  const s = luaFormat("%.14g", n);
1782
1731
  // Guarantee `.01 suffix for integer-valued floats
1783
1732
  if (s.indexOf(".") === -1 && s.indexOf("e") === -1) {
@@ -1834,7 +1783,6 @@ export function jsToLuaValue(value: any): any {
1834
1783
  return value;
1835
1784
  }
1836
1785
  if (Array.isArray(value) && "index" in value && "input" in value) {
1837
- // This is a RegExpMatchArray
1838
1786
  const regexMatch = value as RegExpMatchArray;
1839
1787
  const regexMatchTable = new LuaTable();
1840
1788
  for (let i = 0; i < regexMatch.length; i++) {
@@ -1865,7 +1813,6 @@ export function jsToLuaValue(value: any): any {
1865
1813
  return value;
1866
1814
  }
1867
1815
 
1868
- // Inverse of jsToLuaValue
1869
1816
  export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1870
1817
  if (isPromise(value)) {
1871
1818
  return (value as Promise<any>).then((v) => luaValueToJS(v, sf));
@@ -1879,14 +1826,47 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1879
1826
  value instanceof LuaBuiltinFunction
1880
1827
  ) {
1881
1828
  return (...args: any[]) => {
1882
- const jsArgs = rpAll(args.map((v) => luaValueToJS(v, sf)));
1883
- if (isPromise(jsArgs)) {
1884
- return luaValueToJS(
1885
- jsArgs.then((jsArgs) => (value as ILuaFunction).call(sf, ...jsArgs)),
1886
- sf,
1887
- );
1829
+ // A converted closure captures one frame forever; without a fresh
1830
+ // thread state per call, every invocation would share one budget.
1831
+ const boundaryBudget = boundaryBudgetFactory?.();
1832
+ const callSf =
1833
+ boundaryBudget === undefined
1834
+ ? sf
1835
+ : new LuaStackFrame(sf.threadLocal, sf.astCtx, undefined, undefined, {
1836
+ closeStack: undefined,
1837
+ budget: boundaryBudget,
1838
+ });
1839
+ // Marks this call's own budget (if any) done, so a Stop click that
1840
+ // arrives after the call has already returned is a no-op rather than
1841
+ // quarantining/disabling something that isn't running anymore.
1842
+ const markFinished = () => {
1843
+ if (boundaryBudget !== undefined) {
1844
+ boundaryBudget.finished = true;
1845
+ }
1846
+ };
1847
+ try {
1848
+ const jsArgs = rpAll(args.map((v) => luaValueToJS(v, callSf)));
1849
+ if (isPromise(jsArgs)) {
1850
+ return luaValueToJS(
1851
+ jsArgs
1852
+ .then((jsArgs) => (value as ILuaFunction).call(callSf, ...jsArgs))
1853
+ .finally(markFinished),
1854
+ callSf,
1855
+ );
1856
+ }
1857
+ const result = (value as ILuaFunction).call(callSf, ...jsArgs);
1858
+ if (isPromise(result)) {
1859
+ return luaValueToJS(
1860
+ (result as Promise<any>).finally(markFinished),
1861
+ callSf,
1862
+ );
1863
+ }
1864
+ markFinished();
1865
+ return luaValueToJS(result, callSf);
1866
+ } catch (e) {
1867
+ markFinished();
1868
+ throw e;
1888
1869
  }
1889
- return luaValueToJS((value as ILuaFunction).call(sf, ...jsArgs), sf);
1890
1870
  };
1891
1871
  }
1892
1872
  if (isTaggedFloat(value)) {
@@ -56,7 +56,6 @@ function parseSpec(
56
56
  }
57
57
  }
58
58
 
59
- // Parse width
60
59
  let width = 0;
61
60
  if (i < len && fmt.charCodeAt(i) === 42) {
62
61
  // '*'
@@ -69,7 +68,6 @@ function parseSpec(
69
68
  }
70
69
  }
71
70
 
72
- // Parse precision
73
71
  let hasPrec = false;
74
72
  let prec = 0;
75
73
  if (i < len && fmt.charCodeAt(i) === 46) {
@@ -183,7 +181,6 @@ function formatInt(n: number, spec: FormatSpec): string {
183
181
 
184
182
  if (upper) digits = digits.toUpperCase();
185
183
 
186
- // Precision
187
184
  if (spec.hasPrec) {
188
185
  if (spec.prec === 0 && v === 0) {
189
186
  digits = "";
@@ -192,7 +189,6 @@ function formatInt(n: number, spec: FormatSpec): string {
192
189
  }
193
190
  }
194
191
 
195
- // Alt flag
196
192
  let prefix = "";
197
193
  if (spec.flags & FLAG_HASH) {
198
194
  if (base === 8 && (digits.length === 0 || digits.charCodeAt(0) !== 48)) {
@@ -277,7 +273,6 @@ function formatFloat(n: number, spec: FormatSpec): string {
277
273
  // Alt flag for 'f'/'e': ensure decimal point exists
278
274
  if (spec.flags & FLAG_HASH && lower !== 103) {
279
275
  if (body.indexOf(".") === -1) {
280
- // Insert dot before 'e' if present, else append
281
276
  const eIdx = body.indexOf("e");
282
277
  const EIdx = body.indexOf("E");
283
278
  const expIdx = eIdx !== -1 ? eIdx : EIdx;
@@ -348,7 +343,6 @@ function stripTrailingZerosG(s: string): string {
348
343
  // '0'
349
344
  end--;
350
345
  }
351
- // Remove dot if nothing after it
352
346
  if (end === dotIdx + 1) {
353
347
  end = dotIdx;
354
348
  }
@@ -452,13 +446,11 @@ function hexFloatBody(abs: number, spec: FormatSpec): string {
452
446
  let mantBits: bigint;
453
447
 
454
448
  if (biasedExp === 0) {
455
- // Subnormal
456
449
  if (frac === 0n) return "0x0p+0";
457
450
  const shift = 52 - bitLength(frac) + 1;
458
451
  mantBits = frac << BigInt(shift);
459
452
  exponent = -1022 - shift;
460
453
  } else {
461
- // Normal
462
454
  exponent = biasedExp - 1023;
463
455
  mantBits = frac | (1n << 52n);
464
456
  }
@@ -680,8 +672,6 @@ export function luaFormat(fmt: string, ...args: any[]): string {
680
672
  while (i < len) {
681
673
  const c = fmt.charCodeAt(i);
682
674
  if (c !== 37) {
683
- // not '%'
684
- // Fast path: scan for next '%' or end
685
675
  let j = i + 1;
686
676
  while (j < len && fmt.charCodeAt(j) !== 37) j++;
687
677
  out += fmt.slice(i, j);
@@ -689,7 +679,6 @@ export function luaFormat(fmt: string, ...args: any[]): string {
689
679
  continue;
690
680
  }
691
681
 
692
- // '%' found
693
682
  i++;
694
683
  if (i >= len) {
695
684
  throw new Error("invalid format (ends with '%')");
@@ -705,7 +694,6 @@ export function luaFormat(fmt: string, ...args: any[]): string {
705
694
  const { spec, end } = parseSpec(fmt, i);
706
695
  i = end + 1;
707
696
 
708
- // Resolve `*` width and precision from args
709
697
  let width = spec.width;
710
698
  if (width === -1) {
711
699
  width = Number(args[ai++]) || 0;
@@ -41,7 +41,6 @@ export const jsApi = new LuaTable({
41
41
  import: new LuaBuiltinFunction({
42
42
  callback: async (_sf, url) => {
43
43
  let m = await import(url);
44
- // Unwrap default if it exists
45
44
  if (Object.keys(m).length === 1 && m.default) {
46
45
  m = m.default;
47
46
  }
@@ -179,6 +178,5 @@ export const jsApi = new LuaTable({
179
178
  examples: [{ code: "print(js.stringify({1, 2, 3})) -- [1,2,3]" }],
180
179
  }),
181
180
 
182
- // Expose the global window object
183
181
  window: globalThis,
184
182
  });
@@ -16,7 +16,6 @@ export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue {
16
16
  const block = parseBlock(s, sf.astCtx || {});
17
17
  const globalEnvMaybe = sf.threadLocal.get("_GLOBAL");
18
18
 
19
- // Be vocal when no _GLOBAL is set
20
19
  if (!globalEnvMaybe) {
21
20
  console.warn("load() called without _GLOBAL in thread-local environment");
22
21
  return new LuaMultiRes([null, "Global environment not set"]);
@@ -18,7 +18,6 @@ function untagNumber(x: any): number {
18
18
  }
19
19
 
20
20
  export const mathApi = new LuaTable({
21
- // math constants
22
21
  huge: 1 / 0,
23
22
  pi: Math.PI,
24
23
 
@@ -145,7 +144,6 @@ export const mathApi = new LuaTable({
145
144
  ],
146
145
  }),
147
146
 
148
- // Basic functions
149
147
  abs: new LuaBuiltinFunction({
150
148
  callback: (_sf, x: number) => Math.abs(untagNumber(x)),
151
149
  description: "Returns the absolute value of `x`.",
@@ -177,7 +175,6 @@ export const mathApi = new LuaTable({
177
175
  returns: [{ type: "number" }],
178
176
  }),
179
177
 
180
- // Rounding and remainder
181
178
  fmod: new LuaBuiltinFunction({
182
179
  callback: (_sf, x: number, y: number) => untagNumber(x) % untagNumber(y),
183
180
  description:
@@ -248,7 +245,6 @@ export const mathApi = new LuaTable({
248
245
  returns: [{ type: "number" }],
249
246
  }),
250
247
 
251
- // Power and logarithms
252
248
  exp: new LuaBuiltinFunction({
253
249
  callback: (_sf, x: number) => Math.exp(untagNumber(x)),
254
250
  description: "Returns `e` raised to `x`.",
@@ -289,7 +285,6 @@ export const mathApi = new LuaTable({
289
285
  returns: [{ type: "number" }],
290
286
  }),
291
287
 
292
- // Trigonometric functions
293
288
  cos: new LuaBuiltinFunction({
294
289
  callback: (_sf, x: number) => Math.cos(untagNumber(x)),
295
290
  description: "Returns the cosine of `x` radians.",
@@ -359,7 +354,6 @@ export const mathApi = new LuaTable({
359
354
  deprecated: "Retained for compatibility with older Lua versions.",
360
355
  }),
361
356
 
362
- // Additional utility
363
357
  deg: new LuaBuiltinFunction({
364
358
  callback: (_sf, x: number) => (untagNumber(x) * 180) / Math.PI,
365
359
  description: "Converts an angle from radians to degrees.",
@@ -386,7 +380,6 @@ export const mathApi = new LuaTable({
386
380
  examples: [{ code: "print(math.ult(2, 3)) -- true" }],
387
381
  }),
388
382
 
389
- // Keep the cosineSimilarity utility function
390
383
  cosineSimilarity: new LuaBuiltinFunction({
391
384
  callback: (sf, vecA: LuaTable | number[], vecB: LuaTable | number[]) => {
392
385
  if (vecA instanceof LuaTable) {
@@ -12,7 +12,6 @@ export const netApi = new LuaTable({
12
12
  url: string,
13
13
  options: ProxyFetchRequest = {},
14
14
  ): Promise<ProxyFetchResponse> => {
15
- // JSONify any non-serializable body
16
15
  if (
17
16
  options?.body &&
18
17
  typeof options.body !== "string" &&
@@ -40,7 +39,6 @@ export const netApi = new LuaTable({
40
39
  body: await resp.text(),
41
40
  };
42
41
  }
43
- // Do sensible things with the body based on the content type
44
42
  // Read as ArrayBuffer first to safely handle empty responses (e.g.
45
43
  // PUT/DELETE returning 204 with Content-Type: application/json).
46
44
  // resp.arrayBuffer() never throws on an empty body, whereas
@@ -138,10 +136,8 @@ export const netApi = new LuaTable({
138
136
  }),
139
137
  });
140
138
 
141
- // Utility functions
142
139
  function buildProxyUrl(client: Client, url: string) {
143
140
  url = url.replace(/^https?:\/\//, "");
144
- // Strip off the /.fs and replace with /.proxy
145
141
  return (
146
142
  client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
147
143
  "/.proxy/" +