@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,3 +1,4 @@
1
+ import { LuaBudgetStopped } from "./budget.ts";
1
2
  import {
2
3
  getMetatable,
3
4
  type ILuaFunction,
@@ -14,7 +15,7 @@ import {
14
15
  LuaMultiRes,
15
16
  LuaRuntimeError,
16
17
  type LuaStackFrame,
17
- type LuaTable,
18
+ LuaTable,
18
19
  luaToString,
19
20
  luaTypeOf,
20
21
  type LuaValue,
@@ -37,36 +38,70 @@ import { isTaggedFloat, makeLuaFloat } from "./numeric.ts";
37
38
  import { isPromise } from "./rp.ts";
38
39
  import { isSqlNull } from "./sliq_null.ts";
39
40
 
40
- const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => {
41
- console.log("[Lua]", ...(await Promise.all(args.map((v) => luaToString(v)))));
41
+ const printFunction = new LuaBuiltinFunction({
42
+ callback: async (_sf, ...args) => {
43
+ console.log(
44
+ "[Lua]",
45
+ ...(await Promise.all(args.map((v) => luaToString(v)))),
46
+ );
47
+ },
48
+ description:
49
+ "Prints string representations of its arguments to the runtime log.",
50
+ signatures: ["print(...)"],
51
+ parameters: [{ name: "...", description: "Values to print." }],
52
+ examples: [{ code: 'print("Hello, world!")' }],
42
53
  });
43
54
 
44
- const assertFunction = new LuaBuiltinFunction(
45
- async (sf, value: any, message?: string) => {
55
+ const assertFunction = new LuaBuiltinFunction({
56
+ callback: async (sf, value: any, message?: string) => {
46
57
  if (!(await value)) {
47
58
  throw new LuaRuntimeError(`Assertion failed: ${message}`, sf);
48
59
  }
49
60
  },
50
- );
61
+ description:
62
+ "Raises an error when a value is falsy; otherwise completes successfully.",
63
+ parameters: [
64
+ { name: "value", description: "Condition to test." },
65
+ {
66
+ name: "message",
67
+ type: "string",
68
+ description: "Error detail.",
69
+ optional: true,
70
+ },
71
+ ],
72
+ examples: [{ code: 'assert(user ~= nil, "user is required")' }],
73
+ });
51
74
 
52
- const ipairsFunction = new LuaBuiltinFunction((sf, t: LuaTable | any[]) => {
53
- let i = 0;
75
+ const ipairsFunction = new LuaBuiltinFunction({
76
+ callback: (sf, t: LuaTable | any[]) => {
77
+ let i = 0;
54
78
 
55
- return async () => {
56
- i = i + 1;
79
+ return async () => {
80
+ i = i + 1;
57
81
 
58
- const v = await luaGet(t, i, sf.astCtx ?? null, sf);
59
- if (v === null || v === undefined) {
60
- return;
61
- }
82
+ const v = await luaGet(t, i, sf.astCtx ?? null, sf);
83
+ if (v === null || v === undefined) {
84
+ return;
85
+ }
62
86
 
63
- return new LuaMultiRes([i, v]);
64
- };
87
+ return new LuaMultiRes([i, v]);
88
+ };
89
+ },
90
+ description:
91
+ "Returns an iterator over consecutive integer keys starting at 1 and stopping at the first `nil`.",
92
+ parameters: [{ name: "table", type: "table" }],
93
+ returns: [
94
+ { type: "function", description: "Iterator yielding index and value." },
95
+ ],
96
+ examples: [
97
+ {
98
+ code: 'for i, fruit in ipairs({"apple", "banana"}) do\n print(i, fruit)\nend',
99
+ },
100
+ ],
65
101
  });
66
102
 
67
- const pairsFunction = new LuaBuiltinFunction(
68
- (sf, t: LuaTable | any[] | Record<string, any>) => {
69
- // Respect `__pairs` metamethod for Lua tables
103
+ const pairsFunction = new LuaBuiltinFunction({
104
+ callback: (sf, t: LuaTable | any[] | Record<string, any>) => {
70
105
  if (isLuaTable(t)) {
71
106
  const mt = (t as any).metatable as LuaTable | null | undefined;
72
107
  if (mt) {
@@ -105,10 +140,24 @@ const pairsFunction = new LuaBuiltinFunction(
105
140
  // Must return (iter, state, control) for generic for
106
141
  return new LuaMultiRes([iter, t, null]);
107
142
  },
108
- );
143
+ description:
144
+ "Returns an iterator over all table key-value pairs, respecting `__pairs`.",
145
+ parameters: [{ name: "table", type: "table" }],
146
+ returns: [
147
+ {
148
+ type: "function",
149
+ description: "Iterator plus its state and initial control value.",
150
+ },
151
+ ],
152
+ examples: [
153
+ {
154
+ code: 'for key, value in pairs({name = "Ada", age = 36}) do\n print(key, value)\nend',
155
+ },
156
+ ],
157
+ });
109
158
 
110
- export const eachFunction = new LuaBuiltinFunction(
111
- (sf, ar: LuaTable | any[]) => {
159
+ export const eachFunction = new LuaBuiltinFunction({
160
+ callback: (sf, ar: LuaTable | any[]) => {
112
161
  let i = 1;
113
162
  const length = (ar as any).length;
114
163
  return async () => {
@@ -120,18 +169,30 @@ export const eachFunction = new LuaBuiltinFunction(
120
169
  return result;
121
170
  };
122
171
  },
123
- );
172
+ description:
173
+ "Returns a Space Lua iterator over array-like values without yielding indices.",
174
+ parameters: [{ name: "table", type: "table" }],
175
+ returns: [{ type: "function", description: "Iterator yielding values." }],
176
+ examples: [
177
+ {
178
+ code: 'for fruit in each({"apple", "banana"}) do\n print(fruit)\nend',
179
+ },
180
+ ],
181
+ });
124
182
 
125
- const typeFunction = new LuaBuiltinFunction(
126
- (_sf, value: LuaValue): string | Promise<string> => {
183
+ const typeFunction = new LuaBuiltinFunction({
184
+ callback: (_sf, value: LuaValue): string | Promise<string> => {
127
185
  return luaTypeOf(value);
128
186
  },
129
- );
187
+ description: "Returns the Lua type name of a value.",
188
+ parameters: [{ name: "value" }],
189
+ returns: [{ type: "string" }],
190
+ });
130
191
 
131
192
  // tostring() checks `__tostring` metamethod first (with live SF), then
132
193
  // falls back to the default `luaToString` representation.
133
- const tostringFunction = new LuaBuiltinFunction(
134
- (sf, value: any): string | Promise<string> => {
194
+ const tostringFunction = new LuaBuiltinFunction({
195
+ callback: (sf, value: any): string | Promise<string> => {
135
196
  const mt = getMetatable(value, sf);
136
197
  if (mt) {
137
198
  const mm = mt.rawGet("__tostring");
@@ -153,10 +214,14 @@ const tostringFunction = new LuaBuiltinFunction(
153
214
  }
154
215
  return luaToString(value);
155
216
  },
156
- );
217
+ description:
218
+ "Converts a value to a string, respecting its `__tostring` metamethod.",
219
+ parameters: [{ name: "value" }],
220
+ returns: [{ type: "string" }],
221
+ });
157
222
 
158
- const tonumberFunction = new LuaBuiltinFunction(
159
- (sf, value: LuaValue, base?: number) => {
223
+ const tonumberFunction = new LuaBuiltinFunction({
224
+ callback: (sf, value: LuaValue, base?: number) => {
160
225
  if (base !== undefined) {
161
226
  if (!(typeof base === "number" && base >= 2 && base <= 36)) {
162
227
  throw new LuaRuntimeError(
@@ -188,10 +253,26 @@ const tonumberFunction = new LuaBuiltinFunction(
188
253
 
189
254
  return result.value;
190
255
  },
191
- );
256
+ description:
257
+ "Converts a number or numeric string to a Lua number, optionally in a base from 2 through 36.",
258
+ signatures: [
259
+ "tonumber(value): number|nil",
260
+ "tonumber(value, base): integer|nil",
261
+ ],
262
+ parameters: [
263
+ { name: "value", type: "number|string" },
264
+ { name: "base", type: "integer", optional: true },
265
+ ],
266
+ returns: [{ type: "number|nil" }],
267
+ examples: [{ code: 'print(tonumber("2a", 16)) -- 42' }],
268
+ });
192
269
 
193
- const errorFunction = new LuaBuiltinFunction((sf, message: string) => {
194
- throw new LuaRuntimeError(message, sf);
270
+ const errorFunction = new LuaBuiltinFunction({
271
+ callback: (sf, message: string) => {
272
+ throw new LuaRuntimeError(message, sf);
273
+ },
274
+ description: "Raises a Lua runtime error with the supplied message.",
275
+ parameters: [{ name: "message", type: "string" }],
195
276
  });
196
277
 
197
278
  async function pcallBoundary(
@@ -214,38 +295,53 @@ async function pcallBoundary(
214
295
  const msg = errMsgOf(e);
215
296
  try {
216
297
  await luaCloseFromMark(sf, mark, msg);
217
- return { ok: false, message: msg };
218
298
  } catch (closeErr: any) {
219
- return { ok: false, message: errMsgOf(closeErr) };
299
+ if (!(e instanceof LuaBudgetStopped)) {
300
+ return { ok: false, message: errMsgOf(closeErr) };
301
+ }
220
302
  }
303
+ // A user-initiated stop is not a recoverable Lua error.
304
+ if (e instanceof LuaBudgetStopped) {
305
+ throw e;
306
+ }
307
+ return { ok: false, message: msg };
221
308
  }
222
309
  }
223
310
 
224
- const pcallFunction = new LuaBuiltinFunction(
225
- async (sf, fn: ILuaFunction, ...args) => {
226
- // To-be-closed variables must be closed when unwinding to the
227
- // protected call boundary. Space Lua uses a per-thread close
228
- // stack, so we snapshot its length and close anything pushed
229
- // after that.
230
- //
231
- // The protected call boundary must be established *before*
232
- // evaluating the function and its arguments. Otherwise, any
233
- // `<close>` locals created while evaluating `pcall`'s arguments
234
- // will be wrongly treated as "inside" the protected call, and
235
- // `pcall` may end up closing them (or affecting close ordering).
236
- //
237
- // `threadState` is read-only on the stack frame; do not reassign!
311
+ const pcallFunction = new LuaBuiltinFunction({
312
+ callback: async (sf, fn: ILuaFunction, ...args) => {
313
+ // Snapshot the close stack before evaluating the function and arguments,
314
+ // so argument-owned <close> locals stay outside the protected boundary.
315
+ // Do not reassign the frame’s read-only threadState.
238
316
  const res = await pcallBoundary(sf, fn, args);
239
317
  if (res.ok) {
240
318
  return new LuaMultiRes([true, ...res.values]);
241
319
  }
242
320
  return new LuaMultiRes([false, res.message]);
243
321
  },
244
- );
322
+ description:
323
+ "Calls a function in protected mode and returns a success flag followed by results or an error message.",
324
+ signatures: ["pcall(function, ...): boolean, ..."],
325
+ parameters: [
326
+ { name: "function", type: "function" },
327
+ { name: "...", description: "Arguments passed to the function." },
328
+ ],
329
+ returns: [
330
+ { type: "boolean", description: "Whether the call succeeded." },
331
+ { description: "Call results or error message." },
332
+ ],
333
+ examples: [
334
+ { code: "local ok, result = pcall(function() return mightFail() end)" },
335
+ ],
336
+ });
245
337
 
246
- const xpcallFunction = new LuaBuiltinFunction(
247
- async (sf, fn: ILuaFunction, errorHandler: ILuaFunction, ...args) => {
248
- // Same semantic as `pcall` (see comments there)
338
+ const xpcallFunction = new LuaBuiltinFunction({
339
+ callback: async (
340
+ sf,
341
+ fn: ILuaFunction,
342
+ errorHandler: ILuaFunction,
343
+ ...args
344
+ ) => {
249
345
  const res = await pcallBoundary(sf, fn, args);
250
346
  if (res.ok) {
251
347
  return new LuaMultiRes([true, ...res.values]);
@@ -254,30 +350,69 @@ const xpcallFunction = new LuaBuiltinFunction(
254
350
  const outVals = hr instanceof LuaMultiRes ? hr.flatten().values : [hr];
255
351
  return new LuaMultiRes([false, ...outVals]);
256
352
  },
257
- );
353
+ description:
354
+ "Calls a function in protected mode and transforms any error with an error handler.",
355
+ signatures: ["xpcall(function, errorHandler, ...): boolean, ..."],
356
+ parameters: [
357
+ { name: "function", type: "function" },
358
+ { name: "errorHandler", type: "function" },
359
+ { name: "...", description: "Arguments passed to the function." },
360
+ ],
361
+ returns: [
362
+ { type: "boolean", description: "Whether the call succeeded." },
363
+ { description: "Call results or handler results." },
364
+ ],
365
+ examples: [
366
+ {
367
+ code: 'local ok, message = xpcall(riskyOperation, function(err)\n return "Operation failed: " .. tostring(err)\nend)',
368
+ },
369
+ ],
370
+ });
258
371
 
259
- const setmetatableFunction = new LuaBuiltinFunction(
260
- (sf, table: LuaTable, metatable: LuaTable) => {
372
+ const setmetatableFunction = new LuaBuiltinFunction({
373
+ callback: (sf, table: LuaTable, metatable: LuaTable) => {
261
374
  if (!metatable) {
262
375
  throw new LuaRuntimeError("metatable cannot be set to nil", sf);
263
376
  }
264
377
  table.metatable = metatable;
265
378
  return table;
266
379
  },
267
- );
380
+ description: "Sets a table's metatable and returns the table.",
381
+ parameters: [
382
+ { name: "table", type: "table" },
383
+ { name: "metatable", type: "table" },
384
+ ],
385
+ returns: [{ type: "table" }],
386
+ });
268
387
 
269
- const rawlenFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
270
- return luaLen(value, _sf, true);
388
+ const rawlenFunction = new LuaBuiltinFunction({
389
+ callback: (_sf, value: LuaValue) => luaLen(value, _sf, true),
390
+ description: "Returns a string or table length without invoking `__len`.",
391
+ parameters: [{ name: "value", type: "string|table" }],
392
+ returns: [{ type: "integer" }],
271
393
  });
272
394
 
273
- const rawsetFunction = new LuaBuiltinFunction(
274
- (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
395
+ const rawsetFunction = new LuaBuiltinFunction({
396
+ callback: (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => {
275
397
  return (table as any).rawSet(key, value);
276
398
  },
277
- );
399
+ description:
400
+ "Sets a table key without invoking `__newindex` and returns the table.",
401
+ parameters: [
402
+ { name: "table", type: "table" },
403
+ { name: "key" },
404
+ { name: "value" },
405
+ ],
406
+ returns: [{ type: "table" }],
407
+ examples: [
408
+ {
409
+ code: 'local t = setmetatable({}, {__newindex = function() error("blocked") end})\nrawset(t, "name", "Ada")',
410
+ },
411
+ ],
412
+ });
278
413
 
279
- const rawgetFunction = new LuaBuiltinFunction(
280
- (_sf, table: any, key: LuaValue) => {
414
+ const rawgetFunction = new LuaBuiltinFunction({
415
+ callback: (_sf, table: any, key: LuaValue) => {
281
416
  const isArray = Array.isArray(table);
282
417
 
283
418
  const isPlainObj =
@@ -328,37 +463,64 @@ const rawgetFunction = new LuaBuiltinFunction(
328
463
  const v = (table as Record<string | number, any>)[k as any];
329
464
  return v === undefined ? null : v;
330
465
  },
331
- );
466
+ description: "Reads a table key without invoking `__index`.",
467
+ parameters: [{ name: "table", type: "table" }, { name: "key" }],
468
+ returns: [{ description: "Stored value or `nil`." }],
469
+ });
332
470
 
333
- const rawequalFunction = new LuaBuiltinFunction((_sf, a: any, b: any) => {
334
- const av = isTaggedFloat(a) ? a.value : a;
335
- const bv = isTaggedFloat(b) ? b.value : b;
336
- return av === bv;
471
+ const rawequalFunction = new LuaBuiltinFunction({
472
+ callback: (_sf, a: any, b: any) => {
473
+ const av = isTaggedFloat(a) ? a.value : a;
474
+ const bv = isTaggedFloat(b) ? b.value : b;
475
+ return av === bv;
476
+ },
477
+ description: "Tests two values for equality without invoking `__eq`.",
478
+ parameters: [{ name: "a" }, { name: "b" }],
479
+ returns: [{ type: "boolean" }],
337
480
  });
338
481
 
339
- const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
340
- return (table as any).metatable;
482
+ const getmetatableFunction = new LuaBuiltinFunction({
483
+ callback: (_sf, table: LuaTable) => (table as any).metatable,
484
+ description: "Returns a table's metatable, or `nil` when none is set.",
485
+ parameters: [{ name: "table", type: "table" }],
486
+ returns: [{ type: "table|nil" }],
341
487
  });
342
488
 
343
- const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => {
344
- const global = sf.threadLocal.get("_GLOBAL") as LuaEnv;
345
- const file = (await luaCall(
346
- (global.get("space") as any).get("readFile"),
347
- [filename],
348
- sf.astCtx!,
349
- sf,
350
- )) as Uint8Array;
351
- const code = new TextDecoder().decode(file);
352
- try {
353
- const parsedExpr = parseBlock(code);
354
- const env = new LuaEnv(global);
355
- await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx));
356
- } catch (e: any) {
357
- throw new LuaRuntimeError(
358
- `Error evaluating "${filename}": ${e.message}`,
489
+ const dofileFunction = new LuaBuiltinFunction({
490
+ callback: async (sf, filename: string) => {
491
+ const global = sf.threadLocal.get("_GLOBAL") as LuaEnv;
492
+ const file = (await luaCall(
493
+ (global.get("space") as any).get("readFile"),
494
+ [filename],
495
+ sf.astCtx!,
359
496
  sf,
360
- );
361
- }
497
+ )) as Uint8Array;
498
+ const code = new TextDecoder().decode(file);
499
+ try {
500
+ const parsedExpr = parseBlock(code);
501
+ const env = new LuaEnv(global);
502
+ await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx));
503
+ } catch (e: any) {
504
+ // A user-initiated stop must keep its identity so an enclosing
505
+ // pcall/xpcall still recognizes and rethrows it, rather than being
506
+ // rewrapped into a recoverable LuaRuntimeError.
507
+ if (e instanceof LuaBudgetStopped) {
508
+ throw e;
509
+ }
510
+ throw new LuaRuntimeError(
511
+ `Error evaluating "${filename}": ${e.message}`,
512
+ sf,
513
+ );
514
+ }
515
+ },
516
+ description: "Reads and executes a Lua source file from the current space.",
517
+ parameters: [
518
+ {
519
+ name: "path",
520
+ type: "string",
521
+ description: "Space-relative Lua file path.",
522
+ },
523
+ ],
362
524
  });
363
525
 
364
526
  /**
@@ -369,8 +531,8 @@ const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => {
369
531
  * argument). Otherwise, index must be the string "#", and select
370
532
  * returns the total number of extra arguments it received.
371
533
  */
372
- const selectFunction = new LuaBuiltinFunction(
373
- (_sf, index: number | "#", ...args: LuaValue[]) => {
534
+ const selectFunction = new LuaBuiltinFunction({
535
+ callback: (_sf, index: number | "#", ...args: LuaValue[]) => {
374
536
  if (index === "#") {
375
537
  return args.length;
376
538
  }
@@ -381,7 +543,19 @@ const selectFunction = new LuaBuiltinFunction(
381
543
  return new LuaMultiRes(args.slice(args.length + index));
382
544
  }
383
545
  },
384
- );
546
+ description:
547
+ "Returns the count of extra arguments or all arguments from a selected position onward.",
548
+ signatures: ['select("#", ...): integer', "select(index, ...): ..."],
549
+ parameters: [
550
+ {
551
+ name: "index",
552
+ type: "integer|string",
553
+ description: "One-based index, negative index from the end, or `#`.",
554
+ },
555
+ { name: "..." },
556
+ ],
557
+ returns: [{ description: "Argument count or selected argument values." }],
558
+ });
385
559
 
386
560
  /**
387
561
  * From the Lua docs:
@@ -403,65 +577,120 @@ const selectFunction = new LuaBuiltinFunction(
403
577
  * during its traversal. You may however modify existing fields. In
404
578
  * particular, you may set existing fields to nil.
405
579
  */
406
- const nextFunction = new LuaBuiltinFunction(
407
- (sf, table: LuaTable | Record<string, any>, index: number | null = null) => {
580
+ const nextFunction = new LuaBuiltinFunction({
581
+ callback: (
582
+ sf,
583
+ table: LuaTable | Record<string, any>,
584
+ index: number | null = null,
585
+ ) => {
408
586
  if (!table) {
409
- // When nil value
410
587
  return null;
411
588
  }
412
589
  const keys = luaKeys(table);
413
590
 
414
- // Empty table -> null return value
415
591
  if (keys.length === 0) {
416
592
  return null;
417
593
  }
418
594
 
419
595
  if (index === null) {
420
- // Return the first key, value
421
596
  const key = keys[0];
422
597
  return new LuaMultiRes([key, luaGet(table, key, sf.astCtx ?? null, sf)]);
423
598
  }
424
- // Find index in the key list
425
599
  const idx = keys.indexOf(index);
426
600
  if (idx === -1) {
427
- // Not found
428
601
  throw new LuaRuntimeError("invalid key to 'next': key not found", sf);
429
602
  }
430
603
  const key = keys[idx + 1];
431
604
  if (key === undefined) {
432
- // When called with the last key, should return nil
433
605
  return null;
434
606
  }
435
607
  return new LuaMultiRes([key, luaGet(table, key, sf.astCtx ?? null, sf)]);
436
608
  },
437
- );
438
-
439
- // Non-standard, but useful
440
- const someFunction = new LuaBuiltinFunction(async (_sf, value: any) => {
441
- switch (await luaTypeOf(value)) {
442
- case "number":
443
- if (!Number.isFinite(value)) return null;
444
- break;
445
- case "string":
446
- if (value.trim() === "") return null;
447
- break;
448
- case "table":
449
- if (luaKeys(value).length === 0) return null;
450
- }
451
- return value;
609
+ description:
610
+ "Returns the next table key and value after a given key, or the first pair when the key is omitted.",
611
+ parameters: [
612
+ { name: "table", type: "table" },
613
+ { name: "index", description: "Previous key.", optional: true },
614
+ ],
615
+ returns: [
616
+ { description: "Next key or `nil`." },
617
+ { description: "Value at the next key." },
618
+ ],
452
619
  });
453
620
 
454
- const loadFunction = new LuaBuiltinFunction((sf, s) => luaLoad(s, sf));
621
+ const someFunction = new LuaBuiltinFunction({
622
+ callback: async (_sf, value: any) => {
623
+ switch (await luaTypeOf(value)) {
624
+ case "number":
625
+ if (!Number.isFinite(value)) return null;
626
+ break;
627
+ case "string":
628
+ if (value.trim() === "") return null;
629
+ break;
630
+ case "table":
631
+ if (luaKeys(value).length === 0) return null;
632
+ }
633
+ return value;
634
+ },
635
+ description:
636
+ "Returns `nil` for empty Space Lua values and otherwise returns the value unchanged.",
637
+ parameters: [
638
+ {
639
+ name: "value",
640
+ description:
641
+ "Value to normalize; blank strings, empty tables, infinities, and NaN are empty.",
642
+ },
643
+ ],
644
+ returns: [{ description: "Original value or `nil`." }],
645
+ examples: [
646
+ {
647
+ code: 'print(some(" ") or "empty")\nprint(some({}) or "empty")\nprint(some(0))',
648
+ },
649
+ ],
650
+ });
651
+
652
+ const loadFunction = new LuaBuiltinFunction({
653
+ callback: (sf, s) => luaLoad(s, sf),
654
+ description:
655
+ "Compiles Lua source into a callable chunk without executing it.",
656
+ parameters: [
657
+ { name: "chunk", type: "string", description: "Lua source code." },
658
+ ],
659
+ returns: [
660
+ { type: "function|nil", description: "Compiled chunk or `nil`." },
661
+ { type: "string", description: "Compilation error when unsuccessful." },
662
+ ],
663
+ });
664
+
665
+ function annotateBuiltinApi(
666
+ value: unknown,
667
+ path: string,
668
+ page: string,
669
+ seen = new WeakSet<object>(),
670
+ ): void {
671
+ if (!value || typeof value !== "object" || seen.has(value)) return;
672
+ seen.add(value);
673
+ if (isILuaFunction(value)) {
674
+ value.info ??= { kind: "builtin" };
675
+ value.info.name ??= path;
676
+ value.info.see ??= page;
677
+ return;
678
+ }
679
+ if (value instanceof LuaTable) {
680
+ for (const key of value.keys()) {
681
+ if (typeof key !== "string") continue;
682
+ annotateBuiltinApi(value.rawGet(key), `${path}.${key}`, page, seen);
683
+ }
684
+ }
685
+ }
455
686
 
456
687
  export function luaBuildStandardEnv() {
457
688
  const env = new LuaEnv();
458
- // _G global
459
689
  env.set("_G", env);
460
690
  // Lua version string - for now it signals Lua 5.4 compatibility with
461
691
  // selective 5.5 features; kept non-standard so callers can distinguish
462
692
  // Space Lua from a plain Lua runtime.
463
693
  env.set("_VERSION", "Lua 5.4+");
464
- // Top-level builtins
465
694
  env.set("print", printFunction);
466
695
  env.set("assert", assertFunction);
467
696
  env.set("type", typeFunction);
@@ -469,10 +698,8 @@ export function luaBuildStandardEnv() {
469
698
  env.set("tonumber", tonumberFunction);
470
699
  env.set("select", selectFunction);
471
700
  env.set("next", nextFunction);
472
- // Iterators
473
701
  env.set("pairs", pairsFunction);
474
702
  env.set("ipairs", ipairsFunction);
475
- // meta table stuff
476
703
  env.set("setmetatable", setmetatableFunction);
477
704
  env.set("getmetatable", getmetatableFunction);
478
705
  env.set("rawlen", rawlenFunction);
@@ -480,24 +707,26 @@ export function luaBuildStandardEnv() {
480
707
  env.set("rawget", rawgetFunction);
481
708
  env.set("rawequal", rawequalFunction);
482
709
  env.set("dofile", dofileFunction);
483
- // Error handling
484
710
  env.set("error", errorFunction);
485
711
  env.set("pcall", pcallFunction);
486
712
  env.set("xpcall", xpcallFunction);
487
- // Evaluation
488
713
  env.set("load", loadFunction);
489
- // APIs
490
714
  env.set("string", stringApi);
491
715
  env.set("table", tableApi);
492
716
  env.set("os", osApi);
493
717
  env.set("js", jsApi);
494
718
  env.set("math", mathApi);
495
- // Non-standard
496
719
  env.set("each", eachFunction);
497
720
  env.set("spacelua", spaceluaApi);
498
721
  env.set("encoding", encodingApi);
499
722
  env.set("crypto", cryptoApi);
500
723
  env.set("net", netApi);
501
724
  env.set("some", someFunction);
725
+
726
+ for (const name of env.keys()) {
727
+ const value = env.get(name);
728
+ const page = value instanceof LuaTable ? `API/${name}` : "API/global";
729
+ annotateBuiltinApi(value, name, page);
730
+ }
502
731
  return env;
503
732
  }