@silverbulletmd/silverbullet 2.4.2 → 2.5.3

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