@silverbulletmd/silverbullet 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +38 -25
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +24 -14
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/asset.ts +61 -23
  15. package/client/plugos/syscalls/clientStore.ts +32 -6
  16. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  17. package/client/plugos/syscalls/code_widget.ts +26 -11
  18. package/client/plugos/syscalls/config.ts +146 -30
  19. package/client/plugos/syscalls/datastore.ts +146 -40
  20. package/client/plugos/syscalls/editor.ts +1518 -722
  21. package/client/plugos/syscalls/event.ts +44 -13
  22. package/client/plugos/syscalls/fetch.ts +120 -83
  23. package/client/plugos/syscalls/icon.ts +46 -0
  24. package/client/plugos/syscalls/index.ts +267 -121
  25. package/client/plugos/syscalls/jsonschema.ts +107 -9
  26. package/client/plugos/syscalls/language.ts +38 -12
  27. package/client/plugos/syscalls/markdown.ts +166 -50
  28. package/client/plugos/syscalls/mq.ts +92 -20
  29. package/client/plugos/syscalls/navigator.ts +143 -0
  30. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  31. package/client/plugos/syscalls/search.ts +44 -0
  32. package/client/plugos/syscalls/service_registry.ts +74 -20
  33. package/client/plugos/syscalls/shell.ts +51 -21
  34. package/client/plugos/syscalls/space.ts +412 -113
  35. package/client/plugos/syscalls/sync.ts +103 -20
  36. package/client/plugos/syscalls/system.ts +274 -139
  37. package/client/plugos/system.ts +28 -10
  38. package/client/plugos/worker_runtime.ts +0 -2
  39. package/client/space_lua/aggregates.ts +0 -16
  40. package/client/space_lua/api_documentation.ts +107 -0
  41. package/client/space_lua/ast.ts +13 -5
  42. package/client/space_lua/ast_narrow.ts +0 -5
  43. package/client/space_lua/budget.ts +113 -0
  44. package/client/space_lua/budget_ui.ts +49 -0
  45. package/client/space_lua/eval.ts +125 -92
  46. package/client/space_lua/labels.ts +0 -4
  47. package/client/space_lua/numeric.ts +0 -5
  48. package/client/space_lua/parse.ts +243 -125
  49. package/client/space_lua/pretty_print.ts +128 -54
  50. package/client/space_lua/quarantine.ts +127 -0
  51. package/client/space_lua/query_collection.ts +7 -51
  52. package/client/space_lua/query_env.ts +0 -2
  53. package/client/space_lua/render_lua_markdown.ts +2 -7
  54. package/client/space_lua/render_widget.ts +173 -0
  55. package/client/space_lua/runtime.ts +133 -92
  56. package/client/space_lua/stdlib/crypto.ts +8 -3
  57. package/client/space_lua/stdlib/encoding.ts +27 -9
  58. package/client/space_lua/stdlib/format.ts +0 -12
  59. package/client/space_lua/stdlib/js.ts +136 -23
  60. package/client/space_lua/stdlib/load.ts +24 -16
  61. package/client/space_lua/stdlib/math.ts +331 -133
  62. package/client/space_lua/stdlib/net.ts +57 -13
  63. package/client/space_lua/stdlib/os.ts +111 -54
  64. package/client/space_lua/stdlib/pattern.ts +10 -15
  65. package/client/space_lua/stdlib/space_lua.ts +344 -21
  66. package/client/space_lua/stdlib/string.ts +282 -104
  67. package/client/space_lua/stdlib/string_pack.ts +58 -22
  68. package/client/space_lua/stdlib/table.ts +195 -45
  69. package/client/space_lua/stdlib.ts +354 -125
  70. package/client/space_lua/syscalls.ts +270 -0
  71. package/client/space_lua/tonumber.ts +0 -9
  72. package/dist/plug-compile.js +2 -3
  73. package/package.json +15 -5
  74. package/plug-api/lib/async.ts +1 -6
  75. package/plug-api/lib/collation.ts +22 -0
  76. package/plug-api/lib/crypto.ts +16 -15
  77. package/plug-api/lib/dates.ts +33 -0
  78. package/plug-api/lib/fuzzy.ts +282 -0
  79. package/plug-api/lib/json.ts +0 -6
  80. package/plug-api/lib/limited_map.ts +0 -2
  81. package/plug-api/lib/link_write.ts +27 -0
  82. package/plug-api/{ui → lib}/panel_styles.ts +4 -3
  83. package/plug-api/lib/ref.ts +124 -5
  84. package/plug-api/lib/resolve.ts +0 -1
  85. package/plug-api/lib/resolve_path.ts +290 -0
  86. package/plug-api/lib/shortcut.ts +17 -1
  87. package/plug-api/lib/tags.ts +0 -3
  88. package/plug-api/lib/transclusion.ts +28 -1
  89. package/plug-api/lib/tree.ts +0 -5
  90. package/plug-api/lib/yaml.ts +3 -42
  91. package/plug-api/syscall.ts +1 -5
  92. package/plug-api/syscalls/config.ts +7 -5
  93. package/plug-api/syscalls/editor.ts +70 -208
  94. package/plug-api/syscalls/icon.ts +18 -0
  95. package/plug-api/syscalls/index.ts +39 -5
  96. package/plug-api/syscalls/jsonschema.ts +9 -0
  97. package/plug-api/syscalls/lua.ts +7 -0
  98. package/plug-api/syscalls/search.ts +11 -0
  99. package/plug-api/syscalls/shell.ts +0 -1
  100. package/plug-api/syscalls/space.ts +122 -5
  101. package/plug-api/syscalls/sync.ts +23 -2
  102. package/plug-api/syscalls/system.ts +53 -0
  103. package/plug-api/syscalls.ts +2 -0
  104. package/plug-api/system_mock.ts +15 -2
  105. package/plug-api/types/client.ts +4 -3
  106. package/plug-api/types/datastore.ts +11 -2
  107. package/plug-api/types/index.ts +62 -1
  108. package/plug-api/types/manifest.ts +22 -4
  109. package/plug-api/types/profile.ts +13 -0
  110. package/plug-api/types/revisions.ts +67 -0
  111. package/plug-api/ui/cx.ts +1 -3
  112. package/plug-api/ui/description.ts +64 -0
  113. package/plug-api/ui/hover.ts +69 -0
  114. package/plug-api/ui/index.ts +61 -2
  115. package/plug-api/ui/scroll.ts +30 -0
  116. package/plug-api/ui/slugify.ts +44 -0
  117. package/plug-api/ui/tree_model.ts +263 -0
  118. package/plug-api/ui/tree_types.ts +66 -0
  119. package/plug-api/ui/use_fit_collapse.ts +67 -0
  120. package/plugs/builtin_plugs.ts +0 -1
  121. package/plugs/index/types.ts +9 -0
  122. package/client/plugos/syscalls/lua.ts +0 -81
  123. package/plug-api/lib/memory_cache.ts +0 -21
@@ -1,10 +1,23 @@
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";
5
6
  import { isNegativeZero, isTaggedFloat } from "./numeric.ts";
6
7
  import { isSqlNull } from "./sliq_null.ts";
7
8
  import { luaFormat } from "./stdlib/format.ts";
9
+ import type {
10
+ LuaFunctionDocumentation,
11
+ LuaFunctionInfo,
12
+ } from "../../plug-api/types/index.ts";
13
+
14
+ let boundaryBudgetFactory: (() => LuaBudget) | undefined;
15
+
16
+ export function setBoundaryBudgetFactory(
17
+ f: (() => LuaBudget) | undefined,
18
+ ): void {
19
+ boundaryBudgetFactory = f;
20
+ }
8
21
 
9
22
  export type LuaType =
10
23
  | "nil"
@@ -16,14 +29,22 @@ export type LuaType =
16
29
  | "userdata"
17
30
  | "thread";
18
31
 
19
- // These types are for documentation only
20
32
  export type LuaValue = any;
21
33
  export type JSValue = any;
22
34
 
35
+ type LuaFunctionDefinitionMetadata = LuaFunctionDocumentation &
36
+ Partial<Pick<LuaFunctionInfo, "kind" | "name" | "source">>;
37
+
38
+ export type LuaFunctionDefinition<Callback> = {
39
+ callback: Callback;
40
+ } & LuaFunctionDefinitionMetadata;
41
+
23
42
  export interface ILuaFunction {
24
43
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue;
25
44
 
26
45
  asString(): string;
46
+
47
+ info?: LuaFunctionInfo;
27
48
  }
28
49
 
29
50
  export interface ILuaSettable {
@@ -40,7 +61,6 @@ export interface ILuaGettable {
40
61
  getNumericType?(key: LuaValue): NumericType | undefined;
41
62
  }
42
63
 
43
- // Small helpers for type safety/readability
44
64
  export function isILuaFunction(v: unknown): v is ILuaFunction {
45
65
  return !!v && typeof (v as any).call === "function";
46
66
  }
@@ -68,11 +88,11 @@ const EMPTY_CTX = {} as ASTCtx;
68
88
 
69
89
  const MAX_TAG_LOOP = 200;
70
90
 
71
- // Close-stack support
72
91
  export type LuaCloseEntry = { value: LuaValue; ctx: ASTCtx };
73
92
 
74
93
  type LuaThreadState = {
75
94
  closeStack?: LuaCloseEntry[];
95
+ budget?: LuaBudget;
76
96
  };
77
97
 
78
98
  function isLuaNumber(v: any): boolean {
@@ -323,14 +343,10 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
323
343
  }
324
344
 
325
345
  get(name: string, _sf?: LuaStackFrame): Promise<LuaValue> | LuaValue | null {
326
- // Fast path: single Map.get() instead of has() + get() for the common case
327
- // where the variable exists and has a non-undefined value (null = Lua nil is fine).
328
346
  const v = this.variables.get(name);
329
347
  if (v !== undefined) {
330
348
  return v;
331
349
  }
332
- // Variable set to null (Lua nil) returns null from Map.get() which is !== undefined,
333
- // so it's handled above. Only fall through when the key is truly absent.
334
350
  if (this.parent) {
335
351
  return this.parent.get(name, _sf);
336
352
  }
@@ -453,51 +469,53 @@ export function singleResult(value: any): any {
453
469
  export class LuaFunction implements ILuaFunction {
454
470
  private capturedEnv: LuaEnv;
455
471
  funcHasGotos?: boolean;
472
+ info: LuaFunctionInfo;
456
473
 
457
474
  constructor(
458
475
  readonly body: LuaFunctionBody,
459
476
  closure: LuaEnv,
477
+ info: Partial<LuaFunctionInfo> = {},
460
478
  ) {
461
479
  this.capturedEnv = closure;
480
+ const documentation = body.documentation ?? {};
481
+ this.info = {
482
+ kind: "lua",
483
+ source: { ...body.ctx },
484
+ ...documentation,
485
+ ...info,
486
+ parameters:
487
+ info.parameters ??
488
+ documentation.parameters ??
489
+ body.parameters.map((name) => ({ name })),
490
+ };
462
491
  }
463
492
 
464
493
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
465
- // Create a new environment that chains to the captured environment
466
494
  const env = new LuaEnv(this.capturedEnv);
467
- // Set _CTX to the thread local environment from the stack frame
468
495
  env.setLocal("_CTX", sf.threadLocal);
469
496
 
470
- // Eval using a stack frame that knows the current function
471
497
  const sfWithFn = sf.currentFunction === this ? sf : sf.withFunction(this);
472
498
 
473
499
  const params = this.body.parameters;
474
500
  const hasVarargs = params.length > 0 && params[params.length - 1] === "...";
475
501
 
476
- // Resolve args (sync-first)
477
502
  const argsRP = rpAll(args as any[]);
478
503
  const resolveArgs = (resolvedArgs: any[]) => {
479
504
  if (hasVarargs) {
480
- // Variadic function: bind named params, collect rest into varargs
481
505
  const varargStart = params.length - 1;
482
506
  for (let i = 0; i < varargStart; i++) {
483
507
  env.setLocal(params[i], resolvedArgs[i] ?? null);
484
508
  }
485
- env.setLocal(
486
- "...",
487
- new LuaMultiRes(resolvedArgs.slice(varargStart)),
488
- );
509
+ env.setLocal("...", new LuaMultiRes(resolvedArgs.slice(varargStart)));
489
510
  } else {
490
- // Non-variadic: bind all named params directly
491
511
  for (let i = 0; i < params.length; i++) {
492
512
  env.setLocal(params[i], resolvedArgs[i] ?? null);
493
513
  }
494
514
  }
495
515
 
496
- // Evaluate the function body with returnOnReturn set to true
497
516
  const r = evalStatement(this.body.block, env, sfWithFn, true);
498
517
 
499
518
  if (!isPromise(r)) {
500
- // Fast path: synchronous result
501
519
  if (r === undefined) return;
502
520
  if (r && typeof r === "object" && r.ctrl === "return") {
503
521
  return mapFunctionReturnValue(r.values);
@@ -565,7 +583,26 @@ function mapFunctionReturnValue(values: any[]): any {
565
583
  }
566
584
 
567
585
  export class LuaNativeJSFunction implements ILuaFunction {
568
- constructor(readonly fn: (...args: JSValue[]) => JSValue) {}
586
+ readonly fn: (...args: JSValue[]) => JSValue;
587
+ public info: LuaFunctionInfo;
588
+
589
+ constructor(
590
+ definition:
591
+ | ((...args: JSValue[]) => JSValue)
592
+ | LuaFunctionDefinition<(...args: JSValue[]) => JSValue>,
593
+ ) {
594
+ if (typeof definition === "function") {
595
+ this.fn = definition;
596
+ this.info = { kind: "builtin" };
597
+ } else {
598
+ const { callback, ...info } = definition;
599
+ this.fn = callback;
600
+ this.info = {
601
+ kind: "builtin",
602
+ ...info,
603
+ };
604
+ }
605
+ }
569
606
 
570
607
  // Performs automatic conversion between Lua and JS values for arguments, but not for return values
571
608
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
@@ -595,9 +632,28 @@ export class LuaNativeJSFunction implements ILuaFunction {
595
632
  }
596
633
 
597
634
  export class LuaBuiltinFunction implements ILuaFunction {
635
+ readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue;
636
+ public info: LuaFunctionInfo;
637
+
598
638
  constructor(
599
- readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue,
600
- ) {}
639
+ definition:
640
+ | ((sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue)
641
+ | LuaFunctionDefinition<
642
+ (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue
643
+ >,
644
+ ) {
645
+ if (typeof definition === "function") {
646
+ this.fn = definition;
647
+ this.info = { kind: "builtin" };
648
+ } else {
649
+ const { callback, ...info } = definition;
650
+ this.fn = callback;
651
+ this.info = {
652
+ kind: "builtin",
653
+ ...info,
654
+ };
655
+ }
656
+ }
601
657
 
602
658
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
603
659
  // _CTX is already available via the stack frame
@@ -614,14 +670,10 @@ export class LuaBuiltinFunction implements ILuaFunction {
614
670
  }
615
671
 
616
672
  export class LuaTable implements ILuaSettable, ILuaGettable {
617
- // To optimize the table implementation we use a combination of different data structures
618
673
  public metatable: LuaTable | null;
619
674
 
620
- // When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
621
675
  private stringKeys: Record<string, any>;
622
- // Other keys we can support using a Map as a fallback
623
676
  private otherKeys: Map<any, any> | null;
624
- // When tables are used as arrays, we use a native JavaScript array for that
625
677
  private arrayPart: any[];
626
678
 
627
679
  // Numeric type metadata at storage boundaries (lazily allocated)
@@ -658,15 +710,12 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
658
710
 
659
711
  const numVal = LuaTable.numKeyValue(key);
660
712
  if (numVal !== null) {
661
- // Normalize -0 to +0
662
713
  if (isNegativeZero(numVal)) {
663
714
  return 0;
664
715
  }
665
- // Integer-valued numbers normalize to integers
666
716
  if (Number.isInteger(numVal)) {
667
717
  return numVal;
668
718
  }
669
- // Non-integer floats
670
719
  return numVal;
671
720
  }
672
721
  return key;
@@ -738,7 +787,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
738
787
  return this.stringKeys[key] !== undefined;
739
788
  }
740
789
 
741
- // Fast path for plain integer keys (common in for loops)
742
790
  if (typeof key === "number") {
743
791
  if (key >= 1 && (key | 0) === key) {
744
792
  if (this.arrayPart[key - 1] !== undefined) return true;
@@ -831,7 +879,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
831
879
  return value.then((v) => this.rawSet(key, v, numType));
832
880
  }
833
881
 
834
- // Fast path: string keys (the dominant case)
835
882
  if (typeof key === "string") {
836
883
  if (value === null || value === undefined) {
837
884
  delete this.stringKeys[key];
@@ -890,7 +937,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
890
937
  this.promoteIntegerKeysFromHash();
891
938
  }
892
939
 
893
- // Trailing nil shrink
894
940
  if (value === null || value === undefined) {
895
941
  let n = this.arrayPart.length;
896
942
  while (n > 0) {
@@ -910,7 +956,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
910
956
  return;
911
957
  }
912
958
 
913
- // Sparse numeric key
914
959
  if (!this.otherKeys) {
915
960
  this.otherKeys = new Map();
916
961
  }
@@ -972,12 +1017,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
972
1017
  );
973
1018
  }
974
1019
 
975
- // Fast path: no metatable — skip has() check and metamethod machinery
976
1020
  if (this.metatable === null) {
977
1021
  return this.rawSet(key, value, numType);
978
1022
  }
979
1023
 
980
- // Key exists — rawSet directly, no metamethod needed
981
1024
  if (this.has(key)) {
982
1025
  return this.rawSet(key, value, numType);
983
1026
  }
@@ -988,7 +1031,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
988
1031
  return this.rawSet(key, value, numType);
989
1032
  }
990
1033
 
991
- // Slow path: __newindex metamethod chain (rare)
992
1034
  const errSf = sf || LuaStackFrame.lostFrame;
993
1035
  const ctx = sf?.astCtx ?? EMPTY_CTX;
994
1036
  const k: LuaValue = key;
@@ -1017,12 +1059,10 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1017
1059
  return t.rawSet(k, v, nt);
1018
1060
  }
1019
1061
 
1020
- // Function metamethod: call and stop
1021
1062
  if (typeof mm === "function" || isILuaFunction(mm)) {
1022
1063
  return luaCall(mm, [t, k, v], ctx, errSf);
1023
1064
  }
1024
1065
 
1025
- // Table/env metamethod: forward assignment
1026
1066
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1027
1067
  target = mm;
1028
1068
  continue;
@@ -1052,7 +1092,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1052
1092
  if (typeof key === "string") {
1053
1093
  return this.stringKeyTypes ? this.stringKeyTypes.get(key) : undefined;
1054
1094
  }
1055
- // Fast path for plain integer keys
1056
1095
  if (typeof key === "number") {
1057
1096
  if (key >= 1 && (key | 0) === key) {
1058
1097
  return this.arrayTypes ? this.arrayTypes[key - 1] : undefined;
@@ -1075,7 +1114,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1075
1114
  return this.stringKeys[key];
1076
1115
  }
1077
1116
 
1078
- // Fast path for plain integer keys (common in for loops)
1079
1117
  if (typeof key === "number") {
1080
1118
  if (key >= 1 && (key | 0) === key) {
1081
1119
  const v = this.arrayPart[key - 1];
@@ -1083,7 +1121,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1083
1121
  if (this.otherKeys) return this.otherKeys.get(key);
1084
1122
  return undefined;
1085
1123
  }
1086
- // Non-integer or zero/negative number keys
1087
1124
  if (this.otherKeys) return this.otherKeys.get(key);
1088
1125
  return undefined;
1089
1126
  }
@@ -1117,7 +1154,6 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1117
1154
  }
1118
1155
 
1119
1156
  get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null {
1120
- // Fast path: no metatable means rawGet is the final answer (most tables in SilverBullet)
1121
1157
  if (this.metatable === null) {
1122
1158
  const raw = this.rawGet(key);
1123
1159
  return raw !== undefined ? raw : null;
@@ -1151,11 +1187,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1151
1187
  result[k] = luaValueToJS(this.stringKeys[k], sf);
1152
1188
  }
1153
1189
  }
1154
- // Include array part with 1-based keys
1155
1190
  for (let i = 0; i < this.arrayPart.length; i++) {
1156
1191
  result[i + 1] = luaValueToJS(this.arrayPart[i], sf);
1157
1192
  }
1158
- // Include other keys
1159
1193
  if (this.otherKeys) {
1160
1194
  for (const [key, val] of this.otherKeys) {
1161
1195
  result[key] = luaValueToJS(val, sf);
@@ -1238,14 +1272,12 @@ export function luaIndexValue(
1238
1272
  let t: LuaValue = value;
1239
1273
 
1240
1274
  for (let loop = 0; loop < MAX_TAG_LOOP; loop++) {
1241
- // Primitive get when table
1242
1275
  if (t instanceof LuaTable) {
1243
1276
  const raw = t.rawGet(key);
1244
1277
  if (raw !== undefined) {
1245
1278
  if (isSqlNull(raw)) return null;
1246
1279
  return raw;
1247
1280
  }
1248
- // If no metatable, raw miss => nil
1249
1281
  if (t.metatable === null) {
1250
1282
  return null;
1251
1283
  }
@@ -1267,18 +1299,15 @@ export function luaIndexValue(
1267
1299
  );
1268
1300
  }
1269
1301
 
1270
- // Function metamethod
1271
1302
  if (typeof mm === "function" || isILuaFunction(mm)) {
1272
1303
  return luaCall(mm, [t, key], ctx, errSf);
1273
1304
  }
1274
1305
 
1275
- // Table/metatable delegation: repeat with mm as new "t"
1276
1306
  if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1277
1307
  t = mm;
1278
1308
  continue;
1279
1309
  }
1280
1310
 
1281
- // Bad metamethod type: make it a Lua-like type error
1282
1311
  const ty = luaTypeOf(mm) as any as string;
1283
1312
  throw new LuaRuntimeError(
1284
1313
  `attempt to index a ${ty} value`,
@@ -1313,11 +1342,9 @@ export function luaSet(
1313
1342
  const k = toNumKey(normKey);
1314
1343
  const jsVal = luaValueToJS(value, sf);
1315
1344
  if (isPromise(jsVal)) {
1316
- return (jsVal as Promise<any>).then(
1317
- (v) => {
1318
- (obj as Record<string | number, any>)[k] = v;
1319
- },
1320
- );
1345
+ return (jsVal as Promise<any>).then((v) => {
1346
+ (obj as Record<string | number, any>)[k] = v;
1347
+ });
1321
1348
  }
1322
1349
  (obj as Record<string | number, any>)[k] = jsVal;
1323
1350
  }
@@ -1354,11 +1381,9 @@ export function luaGet(
1354
1381
  if (isTaggedFloat(key)) {
1355
1382
  return (obj as any[])[key.value - 1] ?? null;
1356
1383
  }
1357
- // Native JS object
1358
1384
  const k = toNumKey(key);
1359
1385
  const val = (obj as Record<string | number, any>)[k];
1360
1386
  if (typeof val === "function") {
1361
- // Automatically bind the function to the object
1362
1387
  return val.bind(obj);
1363
1388
  }
1364
1389
  if (val === undefined) {
@@ -1379,7 +1404,6 @@ export function luaLen(
1379
1404
  return obj.length;
1380
1405
  }
1381
1406
  if (obj instanceof LuaTable) {
1382
- // Check __len metamethod unless raw access is requested
1383
1407
  if (!raw) {
1384
1408
  const mt = getMetatable(obj, sf || LuaStackFrame.lostFrame);
1385
1409
  const mm = mt ? mt.rawGet("__len") : null;
@@ -1414,7 +1438,6 @@ export function luaCall(
1414
1438
  );
1415
1439
  }
1416
1440
 
1417
- // Fast path: native JS function
1418
1441
  if (typeof callee === "function") {
1419
1442
  const baseSf = sf || LuaStackFrame.lostFrame;
1420
1443
  const len = args.length;
@@ -1435,7 +1458,6 @@ export function luaCall(
1435
1458
  return (callee as (...a: any[]) => any)(...args);
1436
1459
  }
1437
1460
 
1438
- // Lua table: may be callable via __call metamethod
1439
1461
  if (callee instanceof LuaTable) {
1440
1462
  const metatable = getMetatable(callee, sf);
1441
1463
 
@@ -1466,7 +1488,6 @@ export function luaCall(
1466
1488
  }
1467
1489
  }
1468
1490
 
1469
- // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1470
1491
  if (isILuaFunction(callee)) {
1471
1492
  // Fast path for builtins: skip withFunction, defer withCtx
1472
1493
  if (callee instanceof LuaBuiltinFunction) {
@@ -1491,17 +1512,18 @@ export function luaCall(
1491
1512
  }
1492
1513
 
1493
1514
  export function luaEquals(a: any, b: any): boolean {
1494
- // Normalize nil variants (null, undefined, SQL NULL) to null
1495
- const an = (a === null || a === undefined || isSqlNull(a))
1496
- ? null
1497
- : isTaggedFloat(a)
1498
- ? a.value
1499
- : a;
1500
- const bn = (b === null || b === undefined || isSqlNull(b))
1501
- ? null
1502
- : isTaggedFloat(b)
1503
- ? b.value
1504
- : b;
1515
+ const an =
1516
+ a === null || a === undefined || isSqlNull(a)
1517
+ ? null
1518
+ : isTaggedFloat(a)
1519
+ ? a.value
1520
+ : a;
1521
+ const bn =
1522
+ b === null || b === undefined || isSqlNull(b)
1523
+ ? null
1524
+ : isTaggedFloat(b)
1525
+ ? b.value
1526
+ : b;
1505
1527
  return an === bn;
1506
1528
  }
1507
1529
 
@@ -1569,7 +1591,6 @@ export class LuaRuntimeError extends Error {
1569
1591
  if (!ctx || !ctx.from || !ctx.to) {
1570
1592
  break;
1571
1593
  }
1572
- // Find the line and column
1573
1594
  let line = 1;
1574
1595
  let column = 0;
1575
1596
  let lastNewline = -1;
@@ -1583,7 +1604,6 @@ export class LuaRuntimeError extends Error {
1583
1604
  }
1584
1605
  }
1585
1606
 
1586
- // Get the full line of code for context
1587
1607
  const lineStart = lastNewline + 1;
1588
1608
  const lineEnd = code.indexOf("\n", ctx.from);
1589
1609
  const codeLine = code.substring(
@@ -1591,7 +1611,6 @@ export class LuaRuntimeError extends Error {
1591
1611
  lineEnd === -1 ? undefined : lineEnd,
1592
1612
  );
1593
1613
 
1594
- // Add position indicator
1595
1614
  const pointer = `${" ".repeat(column)}^`;
1596
1615
 
1597
1616
  traceStr +=
@@ -1643,12 +1662,10 @@ export function luaToString(
1643
1662
  return luaFormatNumber(value);
1644
1663
  }
1645
1664
 
1646
- // Check for circular references
1647
1665
  if (typeof value === "object" && visited.has(value)) {
1648
1666
  return "<circular reference>";
1649
1667
  }
1650
1668
  if ((value as any).toStringAsync) {
1651
- // Add to visited before recursing
1652
1669
  visited.add(value);
1653
1670
  return (value as any).toStringAsync();
1654
1671
  }
@@ -1660,15 +1677,12 @@ export function luaToString(
1660
1677
  // Don't recurse into the function body, just show the function signature
1661
1678
  return `<lua-function (${value.body.parameters.join(", ")})>`;
1662
1679
  }
1663
- // Handle plain JavaScript objects in a Lua-like format
1664
1680
  if (typeof value === "object") {
1665
- // Add to visited before recursing
1666
1681
  visited.add(value);
1667
1682
  return (async () => {
1668
1683
  let result = "{";
1669
1684
  let first = true;
1670
1685
 
1671
- // Handle arrays
1672
1686
  if (Array.isArray(value)) {
1673
1687
  for (const val of value) {
1674
1688
  if (first) {
@@ -1676,14 +1690,12 @@ export function luaToString(
1676
1690
  } else {
1677
1691
  result += ", ";
1678
1692
  }
1679
- // Recursively stringify the value, passing the visited set
1680
1693
  const strVal = await luaToString(val, visited);
1681
1694
  result += strVal;
1682
1695
  }
1683
1696
  return `${result}}`;
1684
1697
  }
1685
1698
 
1686
- // Handle objects
1687
1699
  for (const [key, val] of Object.entries(value)) {
1688
1700
  if (first) {
1689
1701
  first = false;
@@ -1695,7 +1707,6 @@ export function luaToString(
1695
1707
  } else {
1696
1708
  result += `["${key}"] = `;
1697
1709
  }
1698
- // Recursively stringify the value, passing the visited set
1699
1710
  const strVal = await luaToString(val, visited);
1700
1711
  result += strVal;
1701
1712
  }
@@ -1716,7 +1727,6 @@ export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1716
1727
  if (n === 0) {
1717
1728
  return 1 / n === -Infinity ? "-0.0" : "0.0";
1718
1729
  }
1719
- // Delegate to luaFormat for `%.14g`
1720
1730
  const s = luaFormat("%.14g", n);
1721
1731
  // Guarantee `.01 suffix for integer-valued floats
1722
1732
  if (s.indexOf(".") === -1 && s.indexOf("e") === -1) {
@@ -1773,7 +1783,6 @@ export function jsToLuaValue(value: any): any {
1773
1783
  return value;
1774
1784
  }
1775
1785
  if (Array.isArray(value) && "index" in value && "input" in value) {
1776
- // This is a RegExpMatchArray
1777
1786
  const regexMatch = value as RegExpMatchArray;
1778
1787
  const regexMatchTable = new LuaTable();
1779
1788
  for (let i = 0; i < regexMatch.length; i++) {
@@ -1804,7 +1813,6 @@ export function jsToLuaValue(value: any): any {
1804
1813
  return value;
1805
1814
  }
1806
1815
 
1807
- // Inverse of jsToLuaValue
1808
1816
  export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1809
1817
  if (isPromise(value)) {
1810
1818
  return (value as Promise<any>).then((v) => luaValueToJS(v, sf));
@@ -1818,14 +1826,47 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1818
1826
  value instanceof LuaBuiltinFunction
1819
1827
  ) {
1820
1828
  return (...args: any[]) => {
1821
- const jsArgs = rpAll(args.map((v) => luaValueToJS(v, sf)));
1822
- if (isPromise(jsArgs)) {
1823
- return luaValueToJS(
1824
- jsArgs.then((jsArgs) => (value as ILuaFunction).call(sf, ...jsArgs)),
1825
- sf,
1826
- );
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;
1827
1869
  }
1828
- return luaValueToJS((value as ILuaFunction).call(sf, ...jsArgs), sf);
1829
1870
  };
1830
1871
  }
1831
1872
  if (isTaggedFloat(value)) {
@@ -2,9 +2,14 @@ import { LuaBuiltinFunction, LuaTable } from "../runtime.ts";
2
2
  import { hashSHA256 } from "../../../plug-api/lib/crypto.ts";
3
3
 
4
4
  export const cryptoApi = new LuaTable({
5
- sha256: new LuaBuiltinFunction(
6
- (_sf, s: string | Uint8Array): Promise<string> => {
5
+ sha256: new LuaBuiltinFunction({
6
+ callback: (_sf, s: string | Uint8Array): Promise<string> => {
7
7
  return hashSHA256(s);
8
8
  },
9
- ),
9
+ description: "Computes the SHA-256 digest of a string or byte buffer.",
10
+ parameters: [
11
+ { name: "data", type: "string|bytes", description: "Data to hash." },
12
+ ],
13
+ returns: [{ type: "string", description: "Hexadecimal SHA-256 digest." }],
14
+ }),
10
15
  });
@@ -2,18 +2,36 @@ import { LuaBuiltinFunction, LuaTable } from "../runtime.ts";
2
2
  import { base64Decode, base64Encode } from "../../../plug-api/lib/crypto.ts";
3
3
 
4
4
  export const encodingApi = new LuaTable({
5
- base64Encode: new LuaBuiltinFunction(
6
- (_sf, s: string | Uint8Array): string => {
5
+ base64Encode: new LuaBuiltinFunction({
6
+ callback: (_sf, s: string | Uint8Array): string => {
7
7
  return base64Encode(s);
8
8
  },
9
- ),
10
- base64Decode: new LuaBuiltinFunction((_sf, s: string): Uint8Array => {
11
- return base64Decode(s);
9
+ description: "Encodes a string or byte buffer as Base64.",
10
+ parameters: [{ name: "data", type: "string|bytes" }],
11
+ returns: [{ type: "string", description: "Base64-encoded data." }],
12
12
  }),
13
- utf8Encode: new LuaBuiltinFunction((_sf, s: string): Uint8Array => {
14
- return new TextEncoder().encode(s);
13
+ base64Decode: new LuaBuiltinFunction({
14
+ callback: (_sf, s: string): Uint8Array => {
15
+ return base64Decode(s);
16
+ },
17
+ description: "Decodes a Base64 string into a byte buffer.",
18
+ parameters: [{ name: "encoded", type: "string" }],
19
+ returns: [{ type: "bytes", description: "Decoded bytes." }],
20
+ }),
21
+ utf8Encode: new LuaBuiltinFunction({
22
+ callback: (_sf, s: string): Uint8Array => {
23
+ return new TextEncoder().encode(s);
24
+ },
25
+ description: "Encodes a UTF-8 string into a byte buffer.",
26
+ parameters: [{ name: "value", type: "string" }],
27
+ returns: [{ type: "bytes", description: "UTF-8 encoded bytes." }],
15
28
  }),
16
- utf8Decode: new LuaBuiltinFunction((_sf, data: Uint8Array): string => {
17
- return new TextDecoder().decode(data);
29
+ utf8Decode: new LuaBuiltinFunction({
30
+ callback: (_sf, data: Uint8Array): string => {
31
+ return new TextDecoder().decode(data);
32
+ },
33
+ description: "Decodes a UTF-8 byte buffer into a string.",
34
+ parameters: [{ name: "data", type: "bytes" }],
35
+ returns: [{ type: "string", description: "Decoded text." }],
18
36
  }),
19
37
  });