@silverbulletmd/silverbullet 2.10.0 → 2.12.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 (105) hide show
  1. package/README.md +6 -6
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/data/kv_primitives.ts +2 -2
  5. package/client/markdown_parser/constants.ts +5 -1
  6. package/client/plugos/hooks/command.ts +8 -6
  7. package/client/plugos/hooks/event.ts +60 -24
  8. package/client/plugos/hooks/mq.ts +0 -3
  9. package/client/plugos/hooks/slash_command.ts +3 -20
  10. package/client/plugos/hooks/syscall.ts +0 -4
  11. package/client/plugos/manifest_cache.ts +2 -34
  12. package/client/plugos/plug.ts +0 -3
  13. package/client/plugos/plug_compile.ts +3 -13
  14. package/client/plugos/sandboxes/sandbox.ts +2 -2
  15. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  16. package/client/plugos/syscalls/capture.ts +104 -0
  17. package/client/plugos/syscalls/editor.ts +197 -56
  18. package/client/plugos/syscalls/event.ts +12 -9
  19. package/client/plugos/syscalls/fetch.ts +0 -4
  20. package/client/plugos/syscalls/icon.ts +46 -0
  21. package/client/plugos/syscalls/index.ts +33 -13
  22. package/client/plugos/syscalls/jsonschema.ts +0 -1
  23. package/client/plugos/syscalls/mq.ts +0 -1
  24. package/client/plugos/syscalls/navigator.ts +138 -0
  25. package/client/plugos/syscalls/search.ts +44 -0
  26. package/client/plugos/syscalls/shell.ts +0 -1
  27. package/client/plugos/syscalls/space.ts +250 -7
  28. package/client/plugos/syscalls/sync.ts +70 -4
  29. package/client/plugos/syscalls/system.ts +39 -0
  30. package/client/plugos/system.ts +9 -7
  31. package/client/plugos/types.ts +6 -6
  32. package/client/plugos/worker_runtime.ts +0 -2
  33. package/client/space_lua/aggregates.ts +6 -22
  34. package/client/space_lua/ast.ts +0 -5
  35. package/client/space_lua/ast_narrow.ts +0 -5
  36. package/client/space_lua/budget.ts +113 -0
  37. package/client/space_lua/budget_ui.ts +49 -0
  38. package/client/space_lua/eval.ts +113 -88
  39. package/client/space_lua/labels.ts +0 -4
  40. package/client/space_lua/numeric.ts +2 -7
  41. package/client/space_lua/parse.ts +0 -17
  42. package/client/space_lua/quarantine.ts +127 -0
  43. package/client/space_lua/query_collection.ts +18 -51
  44. package/client/space_lua/query_env.ts +0 -2
  45. package/client/space_lua/render_lua_markdown.ts +2 -7
  46. package/client/space_lua/render_widget.ts +18 -1
  47. package/client/space_lua/runtime.ts +64 -77
  48. package/client/space_lua/stdlib/format.ts +0 -12
  49. package/client/space_lua/stdlib/js.ts +0 -2
  50. package/client/space_lua/stdlib/load.ts +0 -1
  51. package/client/space_lua/stdlib/math.ts +0 -7
  52. package/client/space_lua/stdlib/net.ts +0 -4
  53. package/client/space_lua/stdlib/os.ts +0 -10
  54. package/client/space_lua/stdlib/pattern.ts +18 -23
  55. package/client/space_lua/stdlib/space_lua.ts +1 -2
  56. package/client/space_lua/stdlib/string_pack.ts +4 -7
  57. package/client/space_lua/stdlib/table.ts +0 -5
  58. package/client/space_lua/stdlib.ts +18 -31
  59. package/client/space_lua/tonumber.ts +0 -9
  60. package/dist/plug-compile.js +3 -4
  61. package/package.json +52 -41
  62. package/plug-api/lib/async.ts +1 -6
  63. package/plug-api/lib/collation.ts +22 -0
  64. package/plug-api/lib/crypto.ts +16 -15
  65. package/plug-api/lib/dates.ts +33 -0
  66. package/plug-api/lib/fuzzy.ts +282 -0
  67. package/plug-api/lib/json.ts +0 -6
  68. package/plug-api/lib/limited_map.ts +0 -2
  69. package/plug-api/lib/link_write.ts +27 -0
  70. package/plug-api/lib/ref.ts +28 -5
  71. package/plug-api/lib/resolve.ts +1 -9
  72. package/plug-api/lib/resolve_path.ts +290 -0
  73. package/plug-api/lib/shortcut.ts +15 -0
  74. package/plug-api/lib/tags.ts +0 -3
  75. package/plug-api/lib/transclusion.ts +28 -1
  76. package/plug-api/lib/tree.ts +0 -5
  77. package/plug-api/lib/yaml.ts +5 -44
  78. package/plug-api/syscall.ts +1 -5
  79. package/plug-api/syscalls/config.ts +2 -2
  80. package/plug-api/syscalls/editor.ts +75 -213
  81. package/plug-api/syscalls/icon.ts +18 -0
  82. package/plug-api/syscalls/index.ts +22 -4
  83. package/plug-api/syscalls/search.ts +11 -0
  84. package/plug-api/syscalls/space.ts +122 -5
  85. package/plug-api/syscalls/sync.ts +31 -2
  86. package/plug-api/syscalls/system.ts +46 -0
  87. package/plug-api/syscalls.ts +2 -0
  88. package/plug-api/system_mock.ts +13 -0
  89. package/plug-api/types/client.ts +20 -4
  90. package/plug-api/types/datastore.ts +11 -2
  91. package/plug-api/types/index.ts +10 -0
  92. package/plug-api/types/manifest.ts +7 -0
  93. package/plug-api/types/profile.ts +13 -0
  94. package/plug-api/types/revisions.ts +67 -0
  95. package/plug-api/ui/description.ts +64 -0
  96. package/plug-api/ui/hover.ts +69 -0
  97. package/plug-api/ui/index.ts +58 -2
  98. package/plug-api/ui/scroll.ts +30 -0
  99. package/plug-api/ui/tree_model.ts +285 -0
  100. package/plug-api/ui/tree_types.ts +67 -0
  101. package/plug-api/ui/use_fit_collapse.ts +67 -0
  102. package/plugs/builtin_plugs.ts +0 -1
  103. package/plugs/index/types.ts +9 -0
  104. package/plug-api/lib/memory_cache.ts +0 -21
  105. /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,15 @@ import type {
10
11
  LuaFunctionInfo,
11
12
  } from "../../plug-api/types/index.ts";
12
13
 
14
+ let boundaryBudgetFactory: (() => LuaBudget) | undefined;
15
+ export const luaFunctionInfo = Symbol("luaFunctionInfo");
16
+
17
+ export function setBoundaryBudgetFactory(
18
+ f: (() => LuaBudget) | undefined,
19
+ ): void {
20
+ boundaryBudgetFactory = f;
21
+ }
22
+
13
23
  export type LuaType =
14
24
  | "nil"
15
25
  | "boolean"
@@ -20,7 +30,6 @@ export type LuaType =
20
30
  | "userdata"
21
31
  | "thread";
22
32
 
23
- // These types are for documentation only
24
33
  export type LuaValue = any;
25
34
  export type JSValue = any;
26
35
 
@@ -31,29 +40,28 @@ export type LuaFunctionDefinition<Callback> = {
31
40
  callback: Callback;
32
41
  } & LuaFunctionDefinitionMetadata;
33
42
 
34
- export interface ILuaFunction {
43
+ export type ILuaFunction = {
35
44
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue;
36
45
 
37
46
  asString(): string;
38
47
 
39
48
  info?: LuaFunctionInfo;
40
- }
49
+ };
41
50
 
42
- export interface ILuaSettable {
51
+ export type ILuaSettable = {
43
52
  set(
44
53
  key: LuaValue,
45
54
  value: LuaValue,
46
55
  sf?: LuaStackFrame,
47
56
  numType?: NumericType,
48
57
  ): void | Promise<void>;
49
- }
58
+ };
50
59
 
51
- export interface ILuaGettable {
60
+ export type ILuaGettable = {
52
61
  get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null;
53
62
  getNumericType?(key: LuaValue): NumericType | undefined;
54
- }
63
+ };
55
64
 
56
- // Small helpers for type safety/readability
57
65
  export function isILuaFunction(v: unknown): v is ILuaFunction {
58
66
  return !!v && typeof (v as any).call === "function";
59
67
  }
@@ -81,11 +89,11 @@ const EMPTY_CTX = {} as ASTCtx;
81
89
 
82
90
  const MAX_TAG_LOOP = 200;
83
91
 
84
- // Close-stack support
85
92
  export type LuaCloseEntry = { value: LuaValue; ctx: ASTCtx };
86
93
 
87
94
  type LuaThreadState = {
88
95
  closeStack?: LuaCloseEntry[];
96
+ budget?: LuaBudget;
89
97
  };
90
98
 
91
99
  function isLuaNumber(v: any): boolean {
@@ -336,14 +344,10 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
336
344
  }
337
345
 
338
346
  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
347
  const v = this.variables.get(name);
342
348
  if (v !== undefined) {
343
349
  return v;
344
350
  }
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
351
  if (this.parent) {
348
352
  return this.parent.get(name, _sf);
349
353
  }
@@ -488,39 +492,31 @@ export class LuaFunction implements ILuaFunction {
488
492
  }
489
493
 
490
494
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
491
- // Create a new environment that chains to the captured environment
492
495
  const env = new LuaEnv(this.capturedEnv);
493
- // Set _CTX to the thread local environment from the stack frame
494
496
  env.setLocal("_CTX", sf.threadLocal);
495
497
 
496
- // Eval using a stack frame that knows the current function
497
498
  const sfWithFn = sf.currentFunction === this ? sf : sf.withFunction(this);
498
499
 
499
500
  const params = this.body.parameters;
500
501
  const hasVarargs = params.length > 0 && params[params.length - 1] === "...";
501
502
 
502
- // Resolve args (sync-first)
503
503
  const argsRP = rpAll(args as any[]);
504
504
  const resolveArgs = (resolvedArgs: any[]) => {
505
505
  if (hasVarargs) {
506
- // Variadic function: bind named params, collect rest into varargs
507
506
  const varargStart = params.length - 1;
508
507
  for (let i = 0; i < varargStart; i++) {
509
508
  env.setLocal(params[i], resolvedArgs[i] ?? null);
510
509
  }
511
510
  env.setLocal("...", new LuaMultiRes(resolvedArgs.slice(varargStart)));
512
511
  } else {
513
- // Non-variadic: bind all named params directly
514
512
  for (let i = 0; i < params.length; i++) {
515
513
  env.setLocal(params[i], resolvedArgs[i] ?? null);
516
514
  }
517
515
  }
518
516
 
519
- // Evaluate the function body with returnOnReturn set to true
520
517
  const r = evalStatement(this.body.block, env, sfWithFn, true);
521
518
 
522
519
  if (!isPromise(r)) {
523
- // Fast path: synchronous result
524
520
  if (r === undefined) return;
525
521
  if (r && typeof r === "object" && r.ctrl === "return") {
526
522
  return mapFunctionReturnValue(r.values);
@@ -675,14 +671,10 @@ export class LuaBuiltinFunction implements ILuaFunction {
675
671
  }
676
672
 
677
673
  export class LuaTable implements ILuaSettable, ILuaGettable {
678
- // To optimize the table implementation we use a combination of different data structures
679
674
  public metatable: LuaTable | null;
680
675
 
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
676
  private stringKeys: Record<string, any>;
683
- // Other keys we can support using a Map as a fallback
684
677
  private otherKeys: Map<any, any> | null;
685
- // When tables are used as arrays, we use a native JavaScript array for that
686
678
  private arrayPart: any[];
687
679
 
688
680
  // Numeric type metadata at storage boundaries (lazily allocated)
@@ -719,15 +711,12 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
719
711
 
720
712
  const numVal = LuaTable.numKeyValue(key);
721
713
  if (numVal !== null) {
722
- // Normalize -0 to +0
723
714
  if (isNegativeZero(numVal)) {
724
715
  return 0;
725
716
  }
726
- // Integer-valued numbers normalize to integers
727
717
  if (Number.isInteger(numVal)) {
728
718
  return numVal;
729
719
  }
730
- // Non-integer floats
731
720
  return numVal;
732
721
  }
733
722
  return key;
@@ -799,7 +788,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
799
788
  return this.stringKeys[key] !== undefined;
800
789
  }
801
790
 
802
- // Fast path for plain integer keys (common in for loops)
803
791
  if (typeof key === "number") {
804
792
  if (key >= 1 && (key | 0) === key) {
805
793
  if (this.arrayPart[key - 1] !== undefined) return true;
@@ -892,7 +880,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
892
880
  return value.then((v) => this.rawSet(key, v, numType));
893
881
  }
894
882
 
895
- // Fast path: string keys (the dominant case)
896
883
  if (typeof key === "string") {
897
884
  if (value === null || value === undefined) {
898
885
  delete this.stringKeys[key];
@@ -951,7 +938,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
951
938
  this.promoteIntegerKeysFromHash();
952
939
  }
953
940
 
954
- // Trailing nil shrink
955
941
  if (value === null || value === undefined) {
956
942
  let n = this.arrayPart.length;
957
943
  while (n > 0) {
@@ -971,7 +957,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
971
957
  return;
972
958
  }
973
959
 
974
- // Sparse numeric key
975
960
  if (!this.otherKeys) {
976
961
  this.otherKeys = new Map();
977
962
  }
@@ -1033,12 +1018,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1033
1018
  );
1034
1019
  }
1035
1020
 
1036
- // Fast path: no metatable — skip has() check and metamethod machinery
1037
1021
  if (this.metatable === null) {
1038
1022
  return this.rawSet(key, value, numType);
1039
1023
  }
1040
1024
 
1041
- // Key exists — rawSet directly, no metamethod needed
1042
1025
  if (this.has(key)) {
1043
1026
  return this.rawSet(key, value, numType);
1044
1027
  }
@@ -1049,7 +1032,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1049
1032
  return this.rawSet(key, value, numType);
1050
1033
  }
1051
1034
 
1052
- // Slow path: __newindex metamethod chain (rare)
1053
1035
  const errSf = sf || LuaStackFrame.lostFrame;
1054
1036
  const ctx = sf?.astCtx ?? EMPTY_CTX;
1055
1037
  const k: LuaValue = key;
@@ -1078,12 +1060,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1078
1060
  return t.rawSet(k, v, nt);
1079
1061
  }
1080
1062
 
1081
- // Function metamethod: call and stop
1082
1063
  if (typeof mm === "function" || isILuaFunction(mm)) {
1083
1064
  return luaCall(mm, [t, k, v], ctx, errSf);
1084
1065
  }
1085
1066
 
1086
- // Table/env metamethod: forward assignment
1087
1067
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1088
1068
  target = mm;
1089
1069
  continue;
@@ -1113,7 +1093,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1113
1093
  if (typeof key === "string") {
1114
1094
  return this.stringKeyTypes ? this.stringKeyTypes.get(key) : undefined;
1115
1095
  }
1116
- // Fast path for plain integer keys
1117
1096
  if (typeof key === "number") {
1118
1097
  if (key >= 1 && (key | 0) === key) {
1119
1098
  return this.arrayTypes ? this.arrayTypes[key - 1] : undefined;
@@ -1136,7 +1115,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1136
1115
  return this.stringKeys[key];
1137
1116
  }
1138
1117
 
1139
- // Fast path for plain integer keys (common in for loops)
1140
1118
  if (typeof key === "number") {
1141
1119
  if (key >= 1 && (key | 0) === key) {
1142
1120
  const v = this.arrayPart[key - 1];
@@ -1144,7 +1122,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1144
1122
  if (this.otherKeys) return this.otherKeys.get(key);
1145
1123
  return undefined;
1146
1124
  }
1147
- // Non-integer or zero/negative number keys
1148
1125
  if (this.otherKeys) return this.otherKeys.get(key);
1149
1126
  return undefined;
1150
1127
  }
@@ -1178,7 +1155,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1178
1155
  }
1179
1156
 
1180
1157
  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
1158
  if (this.metatable === null) {
1183
1159
  const raw = this.rawGet(key);
1184
1160
  return raw !== undefined ? raw : null;
@@ -1212,11 +1188,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1212
1188
  result[k] = luaValueToJS(this.stringKeys[k], sf);
1213
1189
  }
1214
1190
  }
1215
- // Include array part with 1-based keys
1216
1191
  for (let i = 0; i < this.arrayPart.length; i++) {
1217
1192
  result[i + 1] = luaValueToJS(this.arrayPart[i], sf);
1218
1193
  }
1219
- // Include other keys
1220
1194
  if (this.otherKeys) {
1221
1195
  for (const [key, val] of this.otherKeys) {
1222
1196
  result[key] = luaValueToJS(val, sf);
@@ -1299,14 +1273,12 @@ export function luaIndexValue(
1299
1273
  let t: LuaValue = value;
1300
1274
 
1301
1275
  for (let loop = 0; loop < MAX_TAG_LOOP; loop++) {
1302
- // Primitive get when table
1303
1276
  if (t instanceof LuaTable) {
1304
1277
  const raw = t.rawGet(key);
1305
1278
  if (raw !== undefined) {
1306
1279
  if (isSqlNull(raw)) return null;
1307
1280
  return raw;
1308
1281
  }
1309
- // If no metatable, raw miss => nil
1310
1282
  if (t.metatable === null) {
1311
1283
  return null;
1312
1284
  }
@@ -1328,18 +1300,15 @@ export function luaIndexValue(
1328
1300
  );
1329
1301
  }
1330
1302
 
1331
- // Function metamethod
1332
1303
  if (typeof mm === "function" || isILuaFunction(mm)) {
1333
1304
  return luaCall(mm, [t, key], ctx, errSf);
1334
1305
  }
1335
1306
 
1336
- // Table/metatable delegation: repeat with mm as new "t"
1337
1307
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1338
1308
  t = mm;
1339
1309
  continue;
1340
1310
  }
1341
1311
 
1342
- // Bad metamethod type: make it a Lua-like type error
1343
1312
  const ty = luaTypeOf(mm) as any as string;
1344
1313
  throw new LuaRuntimeError(
1345
1314
  `attempt to index a ${ty} value`,
@@ -1413,11 +1382,9 @@ export function luaGet(
1413
1382
  if (isTaggedFloat(key)) {
1414
1383
  return (obj as any[])[key.value - 1] ?? null;
1415
1384
  }
1416
- // Native JS object
1417
1385
  const k = toNumKey(key);
1418
1386
  const val = (obj as Record<string | number, any>)[k];
1419
1387
  if (typeof val === "function") {
1420
- // Automatically bind the function to the object
1421
1388
  return val.bind(obj);
1422
1389
  }
1423
1390
  if (val === undefined) {
@@ -1438,7 +1405,6 @@ export function luaLen(
1438
1405
  return obj.length;
1439
1406
  }
1440
1407
  if (obj instanceof LuaTable) {
1441
- // Check __len metamethod unless raw access is requested
1442
1408
  if (!raw) {
1443
1409
  const mt = getMetatable(obj, sf || LuaStackFrame.lostFrame);
1444
1410
  const mm = mt ? mt.rawGet("__len") : null;
@@ -1473,7 +1439,6 @@ export function luaCall(
1473
1439
  );
1474
1440
  }
1475
1441
 
1476
- // Fast path: native JS function
1477
1442
  if (typeof callee === "function") {
1478
1443
  const baseSf = sf || LuaStackFrame.lostFrame;
1479
1444
  const len = args.length;
@@ -1494,7 +1459,6 @@ export function luaCall(
1494
1459
  return (callee as (...a: any[]) => any)(...args);
1495
1460
  }
1496
1461
 
1497
- // Lua table: may be callable via __call metamethod
1498
1462
  if (callee instanceof LuaTable) {
1499
1463
  const metatable = getMetatable(callee, sf);
1500
1464
 
@@ -1525,7 +1489,6 @@ export function luaCall(
1525
1489
  }
1526
1490
  }
1527
1491
 
1528
- // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1529
1492
  if (isILuaFunction(callee)) {
1530
1493
  // Fast path for builtins: skip withFunction, defer withCtx
1531
1494
  if (callee instanceof LuaBuiltinFunction) {
@@ -1550,7 +1513,6 @@ export function luaCall(
1550
1513
  }
1551
1514
 
1552
1515
  export function luaEquals(a: any, b: any): boolean {
1553
- // Normalize nil variants (null, undefined, SQL NULL) to null
1554
1516
  const an =
1555
1517
  a === null || a === undefined || isSqlNull(a)
1556
1518
  ? null
@@ -1630,7 +1592,6 @@ export class LuaRuntimeError extends Error {
1630
1592
  if (!ctx || !ctx.from || !ctx.to) {
1631
1593
  break;
1632
1594
  }
1633
- // Find the line and column
1634
1595
  let line = 1;
1635
1596
  let column = 0;
1636
1597
  let lastNewline = -1;
@@ -1644,7 +1605,6 @@ export class LuaRuntimeError extends Error {
1644
1605
  }
1645
1606
  }
1646
1607
 
1647
- // Get the full line of code for context
1648
1608
  const lineStart = lastNewline + 1;
1649
1609
  const lineEnd = code.indexOf("\n", ctx.from);
1650
1610
  const codeLine = code.substring(
@@ -1652,7 +1612,6 @@ export class LuaRuntimeError extends Error {
1652
1612
  lineEnd === -1 ? undefined : lineEnd,
1653
1613
  );
1654
1614
 
1655
- // Add position indicator
1656
1615
  const pointer = `${" ".repeat(column)}^`;
1657
1616
 
1658
1617
  traceStr +=
@@ -1704,12 +1663,10 @@ export function luaToString(
1704
1663
  return luaFormatNumber(value);
1705
1664
  }
1706
1665
 
1707
- // Check for circular references
1708
1666
  if (typeof value === "object" && visited.has(value)) {
1709
1667
  return "<circular reference>";
1710
1668
  }
1711
1669
  if ((value as any).toStringAsync) {
1712
- // Add to visited before recursing
1713
1670
  visited.add(value);
1714
1671
  return (value as any).toStringAsync();
1715
1672
  }
@@ -1721,15 +1678,12 @@ export function luaToString(
1721
1678
  // Don't recurse into the function body, just show the function signature
1722
1679
  return `<lua-function (${value.body.parameters.join(", ")})>`;
1723
1680
  }
1724
- // Handle plain JavaScript objects in a Lua-like format
1725
1681
  if (typeof value === "object") {
1726
- // Add to visited before recursing
1727
1682
  visited.add(value);
1728
1683
  return (async () => {
1729
1684
  let result = "{";
1730
1685
  let first = true;
1731
1686
 
1732
- // Handle arrays
1733
1687
  if (Array.isArray(value)) {
1734
1688
  for (const val of value) {
1735
1689
  if (first) {
@@ -1737,14 +1691,12 @@ export function luaToString(
1737
1691
  } else {
1738
1692
  result += ", ";
1739
1693
  }
1740
- // Recursively stringify the value, passing the visited set
1741
1694
  const strVal = await luaToString(val, visited);
1742
1695
  result += strVal;
1743
1696
  }
1744
1697
  return `${result}}`;
1745
1698
  }
1746
1699
 
1747
- // Handle objects
1748
1700
  for (const [key, val] of Object.entries(value)) {
1749
1701
  if (first) {
1750
1702
  first = false;
@@ -1756,7 +1708,6 @@ export function luaToString(
1756
1708
  } else {
1757
1709
  result += `["${key}"] = `;
1758
1710
  }
1759
- // Recursively stringify the value, passing the visited set
1760
1711
  const strVal = await luaToString(val, visited);
1761
1712
  result += strVal;
1762
1713
  }
@@ -1777,7 +1728,6 @@ export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1777
1728
  if (n === 0) {
1778
1729
  return 1 / n === -Infinity ? "-0.0" : "0.0";
1779
1730
  }
1780
- // Delegate to luaFormat for `%.14g`
1781
1731
  const s = luaFormat("%.14g", n);
1782
1732
  // Guarantee `.01 suffix for integer-valued floats
1783
1733
  if (s.indexOf(".") === -1 && s.indexOf("e") === -1) {
@@ -1834,7 +1784,6 @@ export function jsToLuaValue(value: any): any {
1834
1784
  return value;
1835
1785
  }
1836
1786
  if (Array.isArray(value) && "index" in value && "input" in value) {
1837
- // This is a RegExpMatchArray
1838
1787
  const regexMatch = value as RegExpMatchArray;
1839
1788
  const regexMatchTable = new LuaTable();
1840
1789
  for (let i = 0; i < regexMatch.length; i++) {
@@ -1865,7 +1814,6 @@ export function jsToLuaValue(value: any): any {
1865
1814
  return value;
1866
1815
  }
1867
1816
 
1868
- // Inverse of jsToLuaValue
1869
1817
  export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1870
1818
  if (isPromise(value)) {
1871
1819
  return (value as Promise<any>).then((v) => luaValueToJS(v, sf));
@@ -1878,16 +1826,55 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1878
1826
  value instanceof LuaFunction ||
1879
1827
  value instanceof LuaBuiltinFunction
1880
1828
  ) {
1881
- 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
+ const fn = (...args: any[]) => {
1830
+ // A converted closure captures one frame forever; without a fresh
1831
+ // thread state per call, every invocation would share one budget.
1832
+ const boundaryBudget = boundaryBudgetFactory?.();
1833
+ const callSf =
1834
+ boundaryBudget === undefined
1835
+ ? sf
1836
+ : new LuaStackFrame(sf.threadLocal, sf.astCtx, undefined, undefined, {
1837
+ closeStack: undefined,
1838
+ budget: boundaryBudget,
1839
+ });
1840
+ // Marks this call's own budget (if any) done, so a Stop click that
1841
+ // arrives after the call has already returned is a no-op rather than
1842
+ // quarantining/disabling something that isn't running anymore.
1843
+ const markFinished = () => {
1844
+ if (boundaryBudget !== undefined) {
1845
+ boundaryBudget.finished = true;
1846
+ }
1847
+ };
1848
+ try {
1849
+ const jsArgs = rpAll(args.map((v) => luaValueToJS(v, callSf)));
1850
+ if (isPromise(jsArgs)) {
1851
+ return luaValueToJS(
1852
+ jsArgs
1853
+ .then((jsArgs) => (value as ILuaFunction).call(callSf, ...jsArgs))
1854
+ .finally(markFinished),
1855
+ callSf,
1856
+ );
1857
+ }
1858
+ const result = (value as ILuaFunction).call(callSf, ...jsArgs);
1859
+ if (isPromise(result)) {
1860
+ return luaValueToJS(
1861
+ (result as Promise<any>).finally(markFinished),
1862
+ callSf,
1863
+ );
1864
+ }
1865
+ markFinished();
1866
+ return luaValueToJS(result, callSf);
1867
+ } catch (e) {
1868
+ markFinished();
1869
+ throw e;
1888
1870
  }
1889
- return luaValueToJS((value as ILuaFunction).call(sf, ...jsArgs), sf);
1890
1871
  };
1872
+ if (value instanceof LuaFunction) {
1873
+ (fn as typeof fn & { [luaFunctionInfo]: LuaFunctionInfo })[
1874
+ luaFunctionInfo
1875
+ ] = value.info;
1876
+ }
1877
+ return fn;
1891
1878
  }
1892
1879
  if (isTaggedFloat(value)) {
1893
1880
  return value.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) {