@silverbulletmd/silverbullet 2.4.2 → 2.6.1

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 (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -0,0 +1,490 @@
1
+ import {
2
+ LuaBuiltinFunction,
3
+ LuaMultiRes,
4
+ LuaRuntimeError,
5
+ } from "../runtime.ts";
6
+
7
+ import { isTaggedFloat } from "../numeric.ts";
8
+
9
+ function untagN(x: any): number {
10
+ if (typeof x === "number") return x;
11
+ if (isTaggedFloat(x)) return x.value;
12
+ return Number(x);
13
+ }
14
+
15
+ const NATIVE_LITTLE = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
16
+ const NATIVE_MAXALIGN = 8; // JS doubles are 8-byte aligned
17
+
18
+ type KOption =
19
+ | "int"
20
+ | "uint"
21
+ | "float"
22
+ | "double"
23
+ | "number"
24
+ | "char"
25
+ | "string"
26
+ | "zstr"
27
+ | "padding"
28
+ | "paddalign"
29
+ | "nop";
30
+
31
+ interface ParsedOption {
32
+ opt: KOption;
33
+ size: number; // byte width
34
+ ntoalign: number; // padding bytes before this field
35
+ }
36
+
37
+ interface Header {
38
+ islittle: boolean;
39
+ maxalign: number;
40
+ }
41
+
42
+ function makeHeader(): Header {
43
+ return { islittle: NATIVE_LITTLE, maxalign: NATIVE_MAXALIGN };
44
+ }
45
+
46
+ // Read digits from fmt starting at pos; return [value, newPos]
47
+ function readNum(fmt: string, pos: number, dflt: number): [number, number] {
48
+ if (pos >= fmt.length || fmt[pos] < "0" || fmt[pos] > "9") return [dflt, pos];
49
+ let v = 0;
50
+ while (pos < fmt.length && fmt[pos] >= "0" && fmt[pos] <= "9") {
51
+ v = v * 10 + (fmt.charCodeAt(pos) - 48);
52
+ pos++;
53
+ }
54
+ return [v, pos];
55
+ }
56
+
57
+ function numLimit(
58
+ fmt: string,
59
+ pos: number,
60
+ dflt: number,
61
+ src: string,
62
+ ): [number, number] {
63
+ const [sz, np] = readNum(fmt, pos, dflt);
64
+ if (sz < 1 || sz > 16) {
65
+ throw new Error(`integral size (${sz}) out of limits [1,16] in '${src}'`);
66
+ }
67
+ return [sz, np];
68
+ }
69
+
70
+ // Parse one option from fmt[pos], return [parsed, newPos]
71
+ // Modifies header in place for '<', '>', '=', '!'
72
+ function getOption(
73
+ fmt: string,
74
+ pos: number,
75
+ h: Header,
76
+ ): [KOption, number, number] {
77
+ // [opt, size, newPos]
78
+ const c = fmt[pos++];
79
+ switch (c) {
80
+ case "b":
81
+ return ["int", 1, pos];
82
+ case "B":
83
+ return ["uint", 1, pos];
84
+ case "h":
85
+ return ["int", 2, pos];
86
+ case "H":
87
+ return ["uint", 2, pos];
88
+ case "l":
89
+ return ["int", 8, pos];
90
+ case "L":
91
+ return ["uint", 8, pos];
92
+ case "j":
93
+ return ["int", 8, pos];
94
+ case "J":
95
+ return ["uint", 8, pos];
96
+ case "T":
97
+ return ["uint", 8, pos];
98
+ case "f":
99
+ return ["float", 4, pos];
100
+ case "n":
101
+ return ["number", 8, pos];
102
+ case "d":
103
+ return ["double", 8, pos];
104
+ case "i": {
105
+ const [sz, np] = numLimit(fmt, pos, 4, "i");
106
+ return ["int", sz, np];
107
+ }
108
+ case "I": {
109
+ const [sz, np] = numLimit(fmt, pos, 4, "I");
110
+ return ["uint", sz, np];
111
+ }
112
+ case "s": {
113
+ const [sz, np] = numLimit(fmt, pos, 8, "s");
114
+ return ["string", sz, np];
115
+ }
116
+ case "c": {
117
+ const [sz, np] = readNum(fmt, pos, -1);
118
+ if (sz === -1) throw new Error("missing size for format option 'c'");
119
+ return ["char", sz, np];
120
+ }
121
+ case "z":
122
+ return ["zstr", 0, pos];
123
+ case "x":
124
+ return ["padding", 1, pos];
125
+ case "X":
126
+ return ["paddalign", 0, pos];
127
+ case " ":
128
+ return ["nop", 0, pos];
129
+ case "<":
130
+ h.islittle = true;
131
+ return ["nop", 0, pos];
132
+ case ">":
133
+ h.islittle = false;
134
+ return ["nop", 0, pos];
135
+ case "=":
136
+ h.islittle = NATIVE_LITTLE;
137
+ return ["nop", 0, pos];
138
+ case "!": {
139
+ const [sz, np] = readNum(fmt, pos, NATIVE_MAXALIGN);
140
+ h.maxalign = sz;
141
+ return ["nop", 0, np];
142
+ }
143
+ default:
144
+ throw new Error(`invalid format option '${c}'`);
145
+ }
146
+ }
147
+
148
+ // Compute alignment padding
149
+ function getDetails(
150
+ fmt: string,
151
+ pos: number,
152
+ h: Header,
153
+ totalsize: number,
154
+ ): [ParsedOption, number] {
155
+ let opt: KOption, size: number;
156
+ [opt, size, pos] = getOption(fmt, pos, h);
157
+
158
+ let align = size;
159
+
160
+ if (opt === "paddalign") {
161
+ if (pos >= fmt.length) {
162
+ throw new Error("invalid next option for option 'X'");
163
+ }
164
+ const hCopy = { ...h };
165
+ let nextOpt: KOption, nextSize: number;
166
+ [nextOpt, nextSize, pos] = getOption(fmt, pos, hCopy);
167
+ if (nextOpt === "char" || nextSize === 0) {
168
+ throw new Error("invalid next option for option 'X'");
169
+ }
170
+ align = nextSize;
171
+ }
172
+
173
+ let ntoalign = 0;
174
+ if (
175
+ opt !== "char" &&
176
+ opt !== "nop" &&
177
+ opt !== "padding" &&
178
+ opt !== "paddalign"
179
+ ) {
180
+ const realign = Math.min(align, h.maxalign);
181
+ if (realign > 0) {
182
+ ntoalign = (realign - (totalsize % realign)) % realign;
183
+ }
184
+ }
185
+
186
+ return [{ opt, size, ntoalign }, pos];
187
+ }
188
+
189
+ function packInt(v: bigint, size: number, islittle: boolean): Uint8Array {
190
+ const buf = new Uint8Array(size);
191
+ let val = v;
192
+ // Two's complement mask
193
+ const mask = (1n << BigInt(size * 8)) - 1n;
194
+ val = ((val % (mask + 1n)) + (mask + 1n)) & mask; // normalise to unsigned
195
+ for (let i = 0; i < size; i++) {
196
+ buf[islittle ? i : size - 1 - i] = Number(val & 0xffn);
197
+ val >>= 8n;
198
+ }
199
+ return buf;
200
+ }
201
+
202
+ function unpackInt(
203
+ buf: Uint8Array,
204
+ pos: number,
205
+ size: number,
206
+ islittle: boolean,
207
+ issigned: boolean,
208
+ ): bigint {
209
+ let res = 0n;
210
+ const limit = Math.min(size, 8);
211
+ for (let i = limit - 1; i >= 0; i--) {
212
+ res = (res << 8n) | BigInt(buf[pos + (islittle ? i : size - 1 - i)]);
213
+ }
214
+ if (issigned && size <= 8) {
215
+ const mask = 1n << BigInt(size * 8 - 1);
216
+ if (res & mask) res -= mask << 1n;
217
+ }
218
+ return res;
219
+ }
220
+
221
+ function packFloat32(v: number, islittle: boolean): Uint8Array {
222
+ const buf = new ArrayBuffer(4);
223
+ new DataView(buf).setFloat32(0, v, islittle);
224
+ return new Uint8Array(buf);
225
+ }
226
+
227
+ function unpackFloat32(
228
+ buf: Uint8Array,
229
+ pos: number,
230
+ islittle: boolean,
231
+ ): number {
232
+ return new DataView(buf.buffer, buf.byteOffset + pos, 4).getFloat32(
233
+ 0,
234
+ islittle,
235
+ );
236
+ }
237
+
238
+ function packFloat64(v: number, islittle: boolean): Uint8Array {
239
+ const buf = new ArrayBuffer(8);
240
+ new DataView(buf).setFloat64(0, v, islittle);
241
+ return new Uint8Array(buf);
242
+ }
243
+
244
+ function unpackFloat64(
245
+ buf: Uint8Array,
246
+ pos: number,
247
+ islittle: boolean,
248
+ ): number {
249
+ return new DataView(buf.buffer, buf.byteOffset + pos, 8).getFloat64(
250
+ 0,
251
+ islittle,
252
+ );
253
+ }
254
+
255
+ export const strPackFn = new LuaBuiltinFunction(
256
+ (sf, fmt: string, ...args: any[]) => {
257
+ const h = makeHeader();
258
+ const parts: Uint8Array[] = [];
259
+ let totalsize = 0;
260
+ let argIdx = 0;
261
+
262
+ let pos = 0;
263
+ while (pos < fmt.length) {
264
+ let opt: ParsedOption;
265
+ [opt, pos] = getDetails(fmt, pos, h, totalsize);
266
+
267
+ // alignment padding
268
+ if (opt.ntoalign > 0) {
269
+ parts.push(new Uint8Array(opt.ntoalign));
270
+ totalsize += opt.ntoalign;
271
+ }
272
+
273
+ switch (opt.opt) {
274
+ case "nop":
275
+ case "paddalign":
276
+ break;
277
+
278
+ case "padding":
279
+ parts.push(new Uint8Array(1)); // LUAL_PACKPADBYTE = 0
280
+ totalsize += 1;
281
+ break;
282
+
283
+ case "int":
284
+ case "uint": {
285
+ const v = args[argIdx++];
286
+ if (v === undefined || v === null) {
287
+ throw new LuaRuntimeError(
288
+ `bad argument #${argIdx} to 'pack' (value expected)`,
289
+ sf,
290
+ );
291
+ }
292
+ let bi: bigint;
293
+ if (typeof v === "bigint") bi = v;
294
+ else bi = BigInt(Math.trunc(untagN(v)));
295
+ parts.push(packInt(bi, opt.size, h.islittle));
296
+ totalsize += opt.size;
297
+ break;
298
+ }
299
+
300
+ case "float": {
301
+ const v = untagN(args[argIdx++]);
302
+ parts.push(packFloat32(v, h.islittle));
303
+ totalsize += 4;
304
+ break;
305
+ }
306
+
307
+ case "double":
308
+ case "number": {
309
+ const v = untagN(args[argIdx++]);
310
+ parts.push(packFloat64(v, h.islittle));
311
+ totalsize += 8;
312
+ break;
313
+ }
314
+
315
+ case "char": {
316
+ const s: string = String(args[argIdx++]);
317
+ const enc = new TextEncoder().encode(s);
318
+ const buf = new Uint8Array(opt.size);
319
+ buf.set(enc.subarray(0, opt.size));
320
+ parts.push(buf);
321
+ totalsize += opt.size;
322
+ break;
323
+ }
324
+
325
+ case "string": {
326
+ const s: string = String(args[argIdx++]);
327
+ const enc = new TextEncoder().encode(s);
328
+ const lenBuf = packInt(BigInt(enc.length), opt.size, h.islittle);
329
+ parts.push(lenBuf);
330
+ parts.push(enc);
331
+ totalsize += opt.size + enc.length;
332
+ break;
333
+ }
334
+
335
+ case "zstr": {
336
+ const s: string = String(args[argIdx++]);
337
+ if (s.includes("\0")) {
338
+ throw new LuaRuntimeError(
339
+ "string contains zeros for format 'z'",
340
+ sf,
341
+ );
342
+ }
343
+ const enc = new TextEncoder().encode(s);
344
+ parts.push(enc);
345
+ parts.push(new Uint8Array(1)); // null terminator
346
+ totalsize += enc.length + 1;
347
+ break;
348
+ }
349
+ }
350
+ }
351
+
352
+ // Concatenate all parts into one binary string (latin-1 encoding)
353
+ let total = 0;
354
+ for (const p of parts) total += p.length;
355
+ const out = new Uint8Array(total);
356
+ let off = 0;
357
+ for (const p of parts) {
358
+ out.set(p, off);
359
+ off += p.length;
360
+ }
361
+
362
+ // Return as a Lua binary string (each byte is a char code 0-255)
363
+ let result = "";
364
+ for (let i = 0; i < out.length; i++) result += String.fromCharCode(out[i]);
365
+ return result;
366
+ },
367
+ );
368
+
369
+ export const strUnpackFn = new LuaBuiltinFunction(
370
+ (sf, fmt: string, data: string, init?: number) => {
371
+ const h = makeHeader();
372
+
373
+ const buf = new Uint8Array(data.length);
374
+ for (let i = 0; i < data.length; i++) {
375
+ buf[i] = data.charCodeAt(i) & 0xff;
376
+ }
377
+
378
+ let pos = (init !== undefined && init !== null ? init : 1) - 1;
379
+ const results: any[] = [];
380
+
381
+ let fmtPos = 0;
382
+ while (fmtPos < fmt.length) {
383
+ let opt: ParsedOption;
384
+ [opt, fmtPos] = getDetails(fmt, fmtPos, h, pos);
385
+
386
+ if (opt.ntoalign + opt.size > buf.length - pos) {
387
+ if (
388
+ opt.opt !== "nop" &&
389
+ opt.opt !== "paddalign" &&
390
+ opt.opt !== "padding"
391
+ ) {
392
+ throw new LuaRuntimeError("data string too short", sf);
393
+ }
394
+ }
395
+
396
+ pos += opt.ntoalign; // skip alignment padding
397
+
398
+ switch (opt.opt) {
399
+ case "nop":
400
+ case "paddalign":
401
+ break;
402
+
403
+ case "padding":
404
+ pos += 1;
405
+ break;
406
+
407
+ case "int": {
408
+ const v = unpackInt(buf, pos, opt.size, h.islittle, true);
409
+ const n = Number(v);
410
+ results.push(Number.isSafeInteger(n) ? n : v);
411
+ pos += opt.size;
412
+ break;
413
+ }
414
+
415
+ case "uint": {
416
+ const v = unpackInt(buf, pos, opt.size, h.islittle, false);
417
+ const n = Number(v);
418
+ results.push(Number.isSafeInteger(n) ? n : v);
419
+ pos += opt.size;
420
+ break;
421
+ }
422
+
423
+ case "float": {
424
+ results.push(unpackFloat32(buf, pos, h.islittle));
425
+ pos += 4;
426
+ break;
427
+ }
428
+
429
+ case "double":
430
+ case "number": {
431
+ results.push(unpackFloat64(buf, pos, h.islittle));
432
+ pos += 8;
433
+ break;
434
+ }
435
+
436
+ case "char": {
437
+ const s = String.fromCharCode(...buf.subarray(pos, pos + opt.size));
438
+ results.push(s);
439
+ pos += opt.size;
440
+ break;
441
+ }
442
+
443
+ case "string": {
444
+ const len = Number(unpackInt(buf, pos, opt.size, h.islittle, false));
445
+ if (len > buf.length - pos - opt.size) {
446
+ throw new LuaRuntimeError("data string too short", sf);
447
+ }
448
+ pos += opt.size;
449
+ const s = new TextDecoder().decode(buf.subarray(pos, pos + len));
450
+ results.push(s);
451
+ pos += len;
452
+ break;
453
+ }
454
+
455
+ case "zstr": {
456
+ let end = pos;
457
+ while (end < buf.length && buf[end] !== 0) end++;
458
+ if (end >= buf.length) {
459
+ throw new LuaRuntimeError("unfinished string for format 'z'", sf);
460
+ }
461
+ results.push(new TextDecoder().decode(buf.subarray(pos, end)));
462
+ pos = end + 1;
463
+ break;
464
+ }
465
+ }
466
+ }
467
+
468
+ results.push(pos + 1);
469
+ return new LuaMultiRes(results);
470
+ },
471
+ );
472
+
473
+ export const strPackSizeFn = new LuaBuiltinFunction((_sf, fmt: string) => {
474
+ const h = makeHeader();
475
+ let totalsize = 0;
476
+ let pos = 0;
477
+
478
+ while (pos < fmt.length) {
479
+ let opt: ParsedOption;
480
+ [opt, pos] = getDetails(fmt, pos, h, totalsize);
481
+
482
+ if (opt.opt === "string" || opt.opt === "zstr") {
483
+ throw new LuaRuntimeError("variable-length format", _sf);
484
+ }
485
+
486
+ totalsize += opt.ntoalign + opt.size;
487
+ }
488
+
489
+ return totalsize;
490
+ });
@@ -5,6 +5,7 @@ import {
5
5
  luaCall,
6
6
  type LuaEnv,
7
7
  luaEquals,
8
+ luaFormatNumber,
8
9
  luaGet,
9
10
  LuaMultiRes,
10
11
  LuaRuntimeError,
@@ -77,15 +78,14 @@ export const tableApi = new LuaTable({
77
78
  return v;
78
79
  }
79
80
  if (typeof v === "number") {
80
- return String(v);
81
+ return luaFormatNumber(v);
81
82
  }
82
83
  if (isTaggedFloat(v)) {
83
- return String(v.value);
84
+ return luaFormatNumber(v.value, "float");
84
85
  }
85
86
 
86
- const ty = typeof v === "object" && v instanceof LuaTable
87
- ? "table"
88
- : typeof v;
87
+ const ty =
88
+ typeof v === "object" && v instanceof LuaTable ? "table" : typeof v;
89
89
  throw new LuaRuntimeError(
90
90
  `invalid value (${ty}) at index ${idx} in table for 'concat'`,
91
91
  sf,
@@ -204,6 +204,56 @@ export const tableApi = new LuaTable({
204
204
  },
205
205
  ),
206
206
 
207
+ /**
208
+ * Moves elements from table a1 into table a2 (defaults to a1).
209
+ * Equivalent to: `for i = f, e do a2[t+(i-f)] = a1[i] end`
210
+ * Handles overlapping ranges within the same table correctly.
211
+ * @param a1 - Source table.
212
+ * @param f - First source index (inclusive).
213
+ * @param e - Last source index (inclusive).
214
+ * @param t - Destination start index.
215
+ * @param a2 - Destination table (defaults to a1).
216
+ * @returns a2.
217
+ */
218
+ move: new LuaBuiltinFunction(
219
+ async (
220
+ sf,
221
+ a1: LuaTable | any[],
222
+ f: number,
223
+ e: number,
224
+ t: number,
225
+ a2?: LuaTable | any[],
226
+ ) => {
227
+ // a2 defaults to a1
228
+ if (a2 === undefined || a2 === null) {
229
+ a2 = a1;
230
+ }
231
+
232
+ // Empty range: nothing to do, return destination
233
+ if (e < f) {
234
+ return a2;
235
+ }
236
+
237
+ const count = e - f + 1;
238
+
239
+ // When source and destination overlap and destination is ahead of
240
+ // source then copy backwards to avoid clobbering unread values.
241
+ if (t > f && a2 === a1) {
242
+ for (let i = count - 1; i >= 0; i--) {
243
+ const v = await luaGet(a1, f + i, sf.astCtx ?? null, sf);
244
+ await luaSet(a2, t + i, v, sf);
245
+ }
246
+ } else {
247
+ for (let i = 0; i < count; i++) {
248
+ const v = await luaGet(a1, f + i, sf.astCtx ?? null, sf);
249
+ await luaSet(a2, t + i, v, sf);
250
+ }
251
+ }
252
+
253
+ return a2;
254
+ },
255
+ ),
256
+
207
257
  /**
208
258
  * Sorts a table.
209
259
  * @param tbl - The table to sort.
@@ -215,7 +265,7 @@ export const tableApi = new LuaTable({
215
265
  if (Array.isArray(tbl)) {
216
266
  return await asyncQuickSort(tbl, async (a, b) => {
217
267
  if (comp) {
218
- return (await comp.call(sf, a, b)) ? -1 : 1;
268
+ return (await comp.call(sf, a, b)) ? -1 : 0;
219
269
  }
220
270
  return (a as any) < (b as any) ? -1 : 1;
221
271
  });
@@ -235,7 +285,7 @@ export const tableApi = new LuaTable({
235
285
  const cmp = async (a: any, b: any): Promise<number> => {
236
286
  if (comp) {
237
287
  const r = await luaCall(comp, [a, b], sf.astCtx ?? {}, sf);
238
- return r ? -1 : 1;
288
+ return r ? -1 : 0;
239
289
  }
240
290
 
241
291
  const av = isTaggedFloat(a) ? a.value : a;
@@ -250,10 +300,7 @@ export const tableApi = new LuaTable({
250
300
 
251
301
  const ta = typeof av;
252
302
  const tb = typeof bv;
253
- throw new LuaRuntimeError(
254
- `attempt to compare ${ta} with ${tb}`,
255
- sf,
256
- );
303
+ throw new LuaRuntimeError(`attempt to compare ${ta} with ${tb}`, sf);
257
304
  };
258
305
 
259
306
  const sorted = await asyncQuickSort(values, cmp);
@@ -338,24 +385,37 @@ export const tableApi = new LuaTable({
338
385
  },
339
386
  ),
340
387
 
341
- pack: new LuaBuiltinFunction((_sf, ...args: any[]) => {
388
+ /**
389
+ * Returns a new table with all arguments stored in keys 1, 2, ..., n
390
+ * and t.n = n (the total number of arguments).
391
+ */
392
+ pack: new LuaBuiltinFunction(async (sf, ...args: any[]) => {
342
393
  const tbl = new LuaTable();
343
- for (let i = 0; i < args.length; i++) {
344
- tbl.set(i + 1, args[i]);
394
+ const n = args.length;
395
+ for (let i = 0; i < n; i++) {
396
+ await luaSet(tbl, i + 1, args[i], sf);
345
397
  }
346
- tbl.set("n", args.length);
398
+ void tbl.rawSet("n", n);
347
399
  return tbl;
348
400
  }),
349
401
 
402
+ /**
403
+ * Returns all values t[i], t[i+1], ..., t[j].
404
+ * i defaults to 1, j defaults to #t (honours __len).
405
+ * Empty range returns no values (null), not an empty multi-res.
406
+ */
350
407
  unpack: new LuaBuiltinFunction(
351
408
  async (sf, tbl: LuaTable | any[], i?: number, j?: number) => {
352
- i = i ?? 1;
409
+ i = i === undefined || i === null ? 1 : i;
353
410
  if (j === undefined || j === null) {
354
411
  j = Array.isArray(tbl)
355
412
  ? tbl.length
356
413
  : await luaLenForTableLibAsync(sf, tbl);
357
414
  }
358
415
 
416
+ if (i > j) {
417
+ return new LuaMultiRes([]);
418
+ }
359
419
  const result: LuaValue[] = [];
360
420
  for (let k = i; k <= j; k++) {
361
421
  const v = Array.isArray(tbl)