@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
@@ -58,7 +58,8 @@ function parseSpec(
58
58
 
59
59
  // Parse width
60
60
  let width = 0;
61
- if (i < len && fmt.charCodeAt(i) === 42) { // '*'
61
+ if (i < len && fmt.charCodeAt(i) === 42) {
62
+ // '*'
62
63
  width = -1;
63
64
  i++;
64
65
  } else {
@@ -71,10 +72,12 @@ function parseSpec(
71
72
  // Parse precision
72
73
  let hasPrec = false;
73
74
  let prec = 0;
74
- if (i < len && fmt.charCodeAt(i) === 46) { // '.'
75
+ if (i < len && fmt.charCodeAt(i) === 46) {
76
+ // '.'
75
77
  hasPrec = true;
76
78
  i++;
77
- if (i < len && fmt.charCodeAt(i) === 42) { // '*'
79
+ if (i < len && fmt.charCodeAt(i) === 42) {
80
+ // '*'
78
81
  prec = -1;
79
82
  i++;
80
83
  } else {
@@ -109,9 +112,10 @@ function parseSpec(
109
112
  function pad(s: string, width: number, flags: number, numPad: boolean): string {
110
113
  if (width <= 0 || s.length >= width) return s;
111
114
  const n = width - s.length;
112
- if (numPad && (flags & FLAG_ZERO) && !(flags & FLAG_MINUS)) {
115
+ if (numPad && flags & FLAG_ZERO && !(flags & FLAG_MINUS)) {
113
116
  let signLen = 0;
114
- if (s.charCodeAt(0) === 45 || s.charCodeAt(0) === 43) { // '-' or '+'
117
+ if (s.charCodeAt(0) === 45 || s.charCodeAt(0) === 43) {
118
+ // '-' or '+'
115
119
  signLen = 1;
116
120
  } else if (
117
121
  s.charCodeAt(0) === 48 &&
@@ -128,8 +132,8 @@ function pad(s: string, width: number, flags: number, numPad: boolean): string {
128
132
  }
129
133
 
130
134
  function addSign(s: string, flags: number): string {
131
- if (flags & FLAG_PLUS) return "+" + s;
132
- if (flags & FLAG_SPACE) return " " + s;
135
+ if (flags & FLAG_PLUS) return `+${s}`;
136
+ if (flags & FLAG_SPACE) return ` ${s}`;
133
137
  return s;
134
138
  }
135
139
 
@@ -200,7 +204,7 @@ function formatInt(n: number, spec: FormatSpec): string {
200
204
 
201
205
  let result: string;
202
206
  if (neg) {
203
- result = "-" + prefix + digits;
207
+ result = `-${prefix}${digits}`;
204
208
  } else {
205
209
  result = addSign(prefix + digits, spec.flags);
206
210
  }
@@ -216,7 +220,7 @@ function formatFloat(n: number, spec: FormatSpec): string {
216
220
  const lower = code | 32; // to lowercase
217
221
 
218
222
  // Lua convention
219
- if (!isFinite(n)) {
223
+ if (!Number.isFinite(n)) {
220
224
  let s: string;
221
225
  if (n !== n) {
222
226
  s = upper ? "-NAN" : "-nan";
@@ -235,14 +239,17 @@ function formatFloat(n: number, spec: FormatSpec): string {
235
239
 
236
240
  let body: string;
237
241
 
238
- if (lower === 102) { // 'f'
242
+ if (lower === 102) {
243
+ // 'f'
239
244
  body = abs.toFixed(prec);
240
- } else if (lower === 101) { // 'e'
245
+ } else if (lower === 101) {
246
+ // 'e'
241
247
  body = abs.toExponential(prec);
242
248
  // Ensure exponent has at least 2 digits
243
249
  body = ensureExpTwoDigits(body);
244
- } else { // 'g'
245
- const gPrec = (prec === 0) ? 1 : prec;
250
+ } else {
251
+ // 'g'
252
+ const gPrec = prec === 0 ? 1 : prec;
246
253
  if (abs === 0) {
247
254
  body = "0";
248
255
  } else {
@@ -268,35 +275,35 @@ function formatFloat(n: number, spec: FormatSpec): string {
268
275
  }
269
276
 
270
277
  // Alt flag for 'f'/'e': ensure decimal point exists
271
- if ((spec.flags & FLAG_HASH) && lower !== 103) {
278
+ if (spec.flags & FLAG_HASH && lower !== 103) {
272
279
  if (body.indexOf(".") === -1) {
273
280
  // Insert dot before 'e' if present, else append
274
281
  const eIdx = body.indexOf("e");
275
282
  const EIdx = body.indexOf("E");
276
283
  const expIdx = eIdx !== -1 ? eIdx : EIdx;
277
284
  if (expIdx !== -1) {
278
- body = body.slice(0, expIdx) + "." + body.slice(expIdx);
285
+ body = `${body.slice(0, expIdx)}.${body.slice(expIdx)}`;
279
286
  } else {
280
- body = body + ".";
287
+ body = `${body}.`;
281
288
  }
282
289
  }
283
290
  }
284
291
 
285
292
  // Alt flag for 'g': keep trailing zeros but ensure decimal point
286
- if ((spec.flags & FLAG_HASH) && lower === 103) {
293
+ if (spec.flags & FLAG_HASH && lower === 103) {
287
294
  if (body.indexOf(".") === -1) {
288
295
  const expIdx = findExpIndex(body);
289
296
  if (expIdx !== -1) {
290
- body = body.slice(0, expIdx) + "." + body.slice(expIdx);
297
+ body = `${body.slice(0, expIdx)}.${body.slice(expIdx)}`;
291
298
  } else {
292
- body = body + ".";
299
+ body = `${body}.`;
293
300
  }
294
301
  }
295
302
  }
296
303
 
297
304
  let result: string;
298
305
  if (neg) {
299
- result = "-" + body;
306
+ result = `-${body}`;
300
307
  } else {
301
308
  result = addSign(body, spec.flags);
302
309
  }
@@ -322,7 +329,7 @@ function ensureExpTwoDigits(s: string): string {
322
329
  const digitStart = signIdx + 1;
323
330
  const expLen = s.length - digitStart;
324
331
  if (expLen < 2) {
325
- return s.slice(0, digitStart) + "0" + s.slice(digitStart);
332
+ return `${s.slice(0, digitStart)}0${s.slice(digitStart)}`;
326
333
  }
327
334
  return s;
328
335
  }
@@ -337,7 +344,8 @@ function stripTrailingZerosG(s: string): string {
337
344
  if (dotIdx === -1) return s; // nothing to strip
338
345
 
339
346
  let end = mantissa.length;
340
- while (end > dotIdx + 1 && mantissa.charCodeAt(end - 1) === 48) { // '0'
347
+ while (end > dotIdx + 1 && mantissa.charCodeAt(end - 1) === 48) {
348
+ // '0'
341
349
  end--;
342
350
  }
343
351
  // Remove dot if nothing after it
@@ -353,7 +361,7 @@ function formatHexFloat(n: number, spec: FormatSpec): string {
353
361
  const code = spec.spec;
354
362
  const upper = code === 65; // 'A'
355
363
 
356
- if (!isFinite(n)) {
364
+ if (!Number.isFinite(n)) {
357
365
  let s: string;
358
366
  if (n !== n) {
359
367
  s = upper ? "-NAN" : "-nan";
@@ -373,7 +381,7 @@ function formatHexFloat(n: number, spec: FormatSpec): string {
373
381
  if (abs === 0) {
374
382
  const prec = spec.hasPrec ? spec.prec : 0;
375
383
  if (prec > 0) {
376
- body = "0x0." + "0".repeat(prec) + "p+0";
384
+ body = `0x0.${"0".repeat(prec)}p+0`;
377
385
  } else {
378
386
  body = "0x0p+0";
379
387
  }
@@ -389,20 +397,21 @@ function formatHexFloat(n: number, spec: FormatSpec): string {
389
397
  if (pIdx !== -1) {
390
398
  let hasDot = false;
391
399
  for (let k = 0; k < pIdx; k++) {
392
- if (body.charCodeAt(k) === 46) { // '.'
400
+ if (body.charCodeAt(k) === 46) {
401
+ // '.'
393
402
  hasDot = true;
394
403
  break;
395
404
  }
396
405
  }
397
406
  if (!hasDot) {
398
- body = body.slice(0, pIdx) + "." + body.slice(pIdx);
407
+ body = `${body.slice(0, pIdx)}.${body.slice(pIdx)}`;
399
408
  }
400
409
  }
401
410
  }
402
411
 
403
412
  let result: string;
404
413
  if (neg) {
405
- result = "-" + body;
414
+ result = `-${body}`;
406
415
  } else {
407
416
  result = addSign(body, spec.flags);
408
417
  }
@@ -436,8 +445,8 @@ function hexFloatBody(abs: number, spec: FormatSpec): string {
436
445
  const view = new DataView(buf.buffer);
437
446
  view.setFloat64(0, abs);
438
447
  const bits = view.getBigUint64(0);
439
- const biasedExp = Number((bits >> 52n) & 0x7FFn);
440
- const frac = bits & 0xFFFFFFFFFFFFFn;
448
+ const biasedExp = Number((bits >> 52n) & 0x7ffn);
449
+ const frac = bits & 0xfffffffffffffn;
441
450
 
442
451
  let exponent: number;
443
452
  let mantBits: bigint;
@@ -474,9 +483,9 @@ function hexFloatBody(abs: number, spec: FormatSpec): string {
474
483
 
475
484
  const expSign = exponent >= 0 ? "+" : "";
476
485
  if (fracHex.length > 0) {
477
- return "0x" + firstDigit + "." + fracHex + "p" + expSign + exponent;
486
+ return `0x${firstDigit}.${fracHex}p${expSign}${exponent}`;
478
487
  }
479
- return "0x" + firstDigit + "p" + expSign + exponent;
488
+ return `0x${firstDigit}p${expSign}${exponent}`;
480
489
  }
481
490
 
482
491
  // Convert 52-bit value to 13 hex digits, zero-padded
@@ -557,7 +566,8 @@ function padHexRight(s: string, len: number): string {
557
566
 
558
567
  function stripHexTrailingZeros(s: string): string {
559
568
  let end = s.length;
560
- while (end > 0 && s.charCodeAt(end - 1) === 48) { // '0'
569
+ while (end > 0 && s.charCodeAt(end - 1) === 48) {
570
+ // '0'
561
571
  end--;
562
572
  }
563
573
  if (end === s.length) return s;
@@ -581,7 +591,7 @@ function quoteString(s: string): string {
581
591
  if (ds.length < 3) out += "0".repeat(3 - ds.length);
582
592
  out += ds;
583
593
  } else {
584
- out += "\\" + c.toString();
594
+ out += `\\${c.toString()}`;
585
595
  }
586
596
  } else {
587
597
  out += String.fromCharCode(c);
@@ -643,7 +653,7 @@ function toPointer(v: unknown): string {
643
653
  id = nextId++;
644
654
  stringIds.set(key, id);
645
655
  }
646
- return "0x" + id.toString(16).padStart(14, "0");
656
+ return `0x${id.toString(16).padStart(14, "0")}`;
647
657
  }
648
658
 
649
659
  const obj = v as object;
@@ -652,7 +662,7 @@ function toPointer(v: unknown): string {
652
662
  id = nextId++;
653
663
  objectIds.set(obj, id);
654
664
  }
655
- return "0x" + id.toString(16).padStart(14, "0");
665
+ return `0x${id.toString(16).padStart(14, "0")}`;
656
666
  }
657
667
 
658
668
  function formatPointer(v: unknown, spec: FormatSpec): string {
@@ -669,7 +679,8 @@ export function luaFormat(fmt: string, ...args: any[]): string {
669
679
 
670
680
  while (i < len) {
671
681
  const c = fmt.charCodeAt(i);
672
- if (c !== 37) { // not '%'
682
+ if (c !== 37) {
683
+ // not '%'
673
684
  // Fast path: scan for next '%' or end
674
685
  let j = i + 1;
675
686
  while (j < len && fmt.charCodeAt(j) !== 37) j++;
@@ -743,15 +754,18 @@ export function luaFormat(fmt: string, ...args: any[]): string {
743
754
  false,
744
755
  );
745
756
  break;
746
- case 112: { // 'p'
757
+ case 112: {
758
+ // 'p'
747
759
  out += formatPointer(args[ai++], spec);
748
760
  break;
749
761
  }
750
- case 113: { // 'q'
762
+ case 113: {
763
+ // 'q'
751
764
  out += formatQ(args[ai++]);
752
765
  break;
753
766
  }
754
- case 115: { // 's'
767
+ case 115: {
768
+ // 's'
755
769
  let s = String(args[ai++]);
756
770
  if (spec.hasPrec && s.length > spec.prec) {
757
771
  s = s.slice(0, spec.prec);
@@ -12,13 +12,9 @@ export const jsApi = new LuaTable({
12
12
  * @param args - The arguments to pass to the constructor.
13
13
  * @returns The new instance.
14
14
  */
15
- new: new LuaBuiltinFunction(
16
- (sf, constructorFn: any, ...args) => {
17
- return new constructorFn(
18
- ...args.map((v) => luaValueToJS(v, sf)),
19
- );
20
- },
21
- ),
15
+ new: new LuaBuiltinFunction((sf, constructorFn: any, ...args) => {
16
+ return new constructorFn(...args.map((v) => luaValueToJS(v, sf)));
17
+ }),
22
18
  /**
23
19
  * Imports a JavaScript module.
24
20
  * @param url - The URL of the module to import.
@@ -18,9 +18,7 @@ export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue {
18
18
 
19
19
  // Be vocal when no _GLOBAL is set
20
20
  if (!globalEnvMaybe) {
21
- console.warn(
22
- "load() called without _GLOBAL in thread-local environment",
23
- );
21
+ console.warn("load() called without _GLOBAL in thread-local environment");
24
22
  return new LuaMultiRes([null, "Global environment not set"]);
25
23
  }
26
24
 
@@ -4,7 +4,11 @@ import {
4
4
  LuaRuntimeError,
5
5
  LuaTable,
6
6
  } from "../runtime.ts";
7
- import { isNegativeZero, isTaggedFloat } from "../numeric.ts";
7
+ import { isNegativeZero, isTaggedFloat, makeLuaFloat } from "../numeric.ts";
8
+ import { LuaPRNG } from "./prng.ts";
9
+
10
+ // One PRNG per module load, auto-seeded at startup
11
+ const prng = new LuaPRNG();
8
12
 
9
13
  // Fast unwrap: avoids function call overhead for the common plain-number case
10
14
  function untagNumber(x: any): number {
@@ -40,6 +44,29 @@ export const mathApi = new LuaTable({
40
44
  }
41
45
  return null;
42
46
  }),
47
+
48
+ /**
49
+ * If the value x is representable as a Lua integer, returns an integer
50
+ * with that value. Otherwise returns nil.
51
+ * Strings are NOT accepted — only Lua number values.
52
+ */
53
+ tointeger: new LuaBuiltinFunction((_sf, x?: any) => {
54
+ if (typeof x === "number") {
55
+ return Number.isInteger(x) && Number.isFinite(x) ? x : null;
56
+ }
57
+ if (isTaggedFloat(x)) {
58
+ const n = x.value;
59
+ return Number.isInteger(n) && Number.isFinite(n) ? n : null;
60
+ }
61
+ if (typeof x === "string") {
62
+ const n = untagNumber(x); // Number(x) coerces the string
63
+ if (Number.isNaN(n) || !Number.isFinite(n) || !Number.isInteger(n))
64
+ return null;
65
+ return n;
66
+ }
67
+ return null;
68
+ }),
69
+
43
70
  /**
44
71
  * When called without arguments, returns a pseudo-random float with
45
72
  * uniform distribution in the range [0,1). When called with two
@@ -52,51 +79,21 @@ export const mathApi = new LuaTable({
52
79
  random: new LuaBuiltinFunction((_sf, m?: number, n?: number) => {
53
80
  if (m !== undefined) m = untagNumber(m);
54
81
  if (n !== undefined) n = untagNumber(n);
55
-
56
- if (m === undefined && n === undefined) {
57
- return Math.random();
58
- }
59
-
60
- if (!Number.isInteger(m)) {
61
- throw new LuaRuntimeError(
62
- "bad argument #1 to 'math.random' (integer expected)",
63
- _sf,
64
- );
65
- }
66
-
67
- if (n === undefined) {
68
- if (m! == 0) {
69
- const high = Math.floor(Math.random() * 0x100000000);
70
- const low = Math.floor(Math.random() * 0x100000000);
71
- let result = (BigInt(high) << 32n) | BigInt(low);
72
- if (result & (1n << 63n)) {
73
- result -= 1n << 64n;
74
- }
75
- return result;
76
- }
77
- if (m! < 1) {
78
- throw new LuaRuntimeError(
79
- "bad argument #1 to 'math.random' (interval is empty)",
80
- _sf,
81
- );
82
- }
83
- return Math.floor(Math.random() * m!) + 1;
82
+ try {
83
+ return prng.random(m, n);
84
+ } catch (e: any) {
85
+ throw new LuaRuntimeError(e.message, _sf);
84
86
  }
85
-
86
- if (!Number.isInteger(n!)) {
87
- throw new LuaRuntimeError(
88
- "bad argument #2 to 'math.random' (integer expected)",
89
- _sf,
90
- );
91
- }
92
-
93
- if (n! < m!) {
94
- throw new LuaRuntimeError(
95
- "bad argument #1 to 'math.random' (interval is empty)",
96
- _sf,
97
- );
98
- }
99
- return Math.floor(Math.random() * (n! - m! + 1)) + m!;
87
+ }),
88
+ /**
89
+ * Seeds the pseudo-random generator. With no arguments, uses a
90
+ * time-based seed. Returns the two seed integers used (Lua 5.4 contract).
91
+ */
92
+ randomseed: new LuaBuiltinFunction((_sf, x?: number, y?: number) => {
93
+ if (x !== undefined) x = untagNumber(x);
94
+ if (y !== undefined) y = untagNumber(y);
95
+ const [s1, s2] = prng.randomseed(x, y);
96
+ return new LuaMultiRes([s1, s2]);
100
97
  }),
101
98
 
102
99
  // Basic functions
@@ -104,23 +101,51 @@ export const mathApi = new LuaTable({
104
101
  ceil: new LuaBuiltinFunction((_sf, x: number) => Math.ceil(untagNumber(x))),
105
102
  floor: new LuaBuiltinFunction((_sf, x: number) => Math.floor(untagNumber(x))),
106
103
  max: new LuaBuiltinFunction((_sf, ...args: number[]) =>
107
- Math.max(...args.map(untagNumber))
104
+ Math.max(...args.map(untagNumber)),
108
105
  ),
109
106
  min: new LuaBuiltinFunction((_sf, ...args: number[]) =>
110
- Math.min(...args.map(untagNumber))
107
+ Math.min(...args.map(untagNumber)),
111
108
  ),
112
109
 
113
110
  // Rounding and remainder
114
- fmod: new LuaBuiltinFunction((_sf, x: number, y: number) =>
115
- untagNumber(x) % untagNumber(y)
111
+ fmod: new LuaBuiltinFunction(
112
+ (_sf, x: number, y: number) => untagNumber(x) % untagNumber(y),
116
113
  ),
117
114
  modf: new LuaBuiltinFunction((_sf, x: number) => {
118
115
  const xn = untagNumber(x);
119
116
  const int = Math.trunc(xn);
120
- const frac = xn - int;
117
+ // Guarantee that the `frac` part is always Lua float
118
+ const frac = makeLuaFloat(xn - int);
121
119
  return new LuaMultiRes([int, frac]);
122
120
  }),
123
121
 
122
+ // Returns m and e such that x = m * 2^e, 0.5 <= |m| < 1 (or m=0 when x=0).
123
+ // e is an integer. Mirrors C99/Lua.
124
+ // Special cases: frexp(0) = (0, 0); frexp(+-inf/nan) = (x, 0).
125
+ frexp: new LuaBuiltinFunction((_sf, x: number) => {
126
+ const xn = untagNumber(x);
127
+ if (xn === 0 || !Number.isFinite(xn) || Number.isNaN(xn)) {
128
+ return new LuaMultiRes([xn, 0]);
129
+ }
130
+ const abs = Math.abs(xn);
131
+ let e = Math.floor(Math.log2(abs)) + 1;
132
+ let m = xn / 2 ** e;
133
+ if (Math.abs(m) >= 1.0) {
134
+ e += 1;
135
+ m /= 2;
136
+ }
137
+ if (Math.abs(m) < 0.5) {
138
+ e -= 1;
139
+ m *= 2;
140
+ }
141
+ return new LuaMultiRes([m, e]);
142
+ }),
143
+
144
+ // Returns m * 2^e (the inverse of frexp). Mirrors C99/Lua.
145
+ ldexp: new LuaBuiltinFunction(
146
+ (_sf, m: number, e: number) => untagNumber(m) * 2 ** untagNumber(e),
147
+ ),
148
+
124
149
  // Power and logarithms
125
150
  exp: new LuaBuiltinFunction((_sf, x: number) => Math.exp(untagNumber(x))),
126
151
  log: new LuaBuiltinFunction((_sf, x: number, base?: number) => {
@@ -129,8 +154,9 @@ export const mathApi = new LuaTable({
129
154
  }
130
155
  return Math.log(untagNumber(x)) / Math.log(untagNumber(base));
131
156
  }),
132
- pow: new LuaBuiltinFunction((_sf, x: number, y: number) =>
133
- Math.pow(untagNumber(x), untagNumber(y))
157
+ // Power function (deprecated in Lua 5.4 but retained for compatibility)
158
+ pow: new LuaBuiltinFunction(
159
+ (_sf, x: number, y: number) => untagNumber(x) ** untagNumber(y),
134
160
  ),
135
161
  sqrt: new LuaBuiltinFunction((_sf, x: number) => Math.sqrt(untagNumber(x))),
136
162
 
@@ -147,20 +173,20 @@ export const mathApi = new LuaTable({
147
173
  return Math.atan2(untagNumber(y), untagNumber(x));
148
174
  }),
149
175
 
150
- // Hyperbolic functions
176
+ // Hyperbolic functions (deprecated in Lua 5.4 but retained for compatibility)
151
177
  cosh: new LuaBuiltinFunction((_sf, x: number) => Math.cosh(untagNumber(x))),
152
178
  sinh: new LuaBuiltinFunction((_sf, x: number) => Math.sinh(untagNumber(x))),
153
179
  tanh: new LuaBuiltinFunction((_sf, x: number) => Math.tanh(untagNumber(x))),
154
180
 
155
181
  // Additional utility
156
- deg: new LuaBuiltinFunction((_sf, x: number) =>
157
- untagNumber(x) * 180 / Math.PI
182
+ deg: new LuaBuiltinFunction(
183
+ (_sf, x: number) => (untagNumber(x) * 180) / Math.PI,
158
184
  ),
159
- rad: new LuaBuiltinFunction((_sf, x: number) =>
160
- untagNumber(x) * Math.PI / 180
185
+ rad: new LuaBuiltinFunction(
186
+ (_sf, x: number) => (untagNumber(x) * Math.PI) / 180,
161
187
  ),
162
188
  ult: new LuaBuiltinFunction((_sf, m: number, n: number) => {
163
- return (untagNumber(m) >>> 0) < (untagNumber(n) >>> 0);
189
+ return untagNumber(m) >>> 0 < untagNumber(n) >>> 0;
164
190
  }),
165
191
 
166
192
  // Keep the cosineSimilarity utility function
@@ -14,17 +14,18 @@ export const netApi = new LuaTable({
14
14
  ): Promise<ProxyFetchResponse> => {
15
15
  // JSONify any non-serializable body
16
16
  if (
17
- options?.body && typeof options.body !== "string" &&
17
+ options?.body &&
18
+ typeof options.body !== "string" &&
18
19
  !(options.body instanceof Uint8Array)
19
20
  ) {
20
21
  options.body = JSON.stringify(options.body);
21
22
  }
22
23
  const fetchOptions = options
23
24
  ? {
24
- method: options.method,
25
- headers: {} as Record<string, string>,
26
- body: options.body,
27
- }
25
+ method: options.method,
26
+ headers: {} as Record<string, string>,
27
+ body: options.body,
28
+ }
28
29
  : {};
29
30
  fetchOptions.headers = buildProxyHeaders(options.headers);
30
31
  const resp = await client.httpSpacePrimitives.authenticatedFetch(
@@ -40,19 +41,27 @@ export const netApi = new LuaTable({
40
41
  };
41
42
  }
42
43
  // Do sensible things with the body based on the content type
44
+ // Read as ArrayBuffer first to safely handle empty responses (e.g.
45
+ // PUT/DELETE returning 204 with Content-Type: application/json).
46
+ // resp.arrayBuffer() never throws on an empty body, whereas
47
+ // resp.json() would throw a SyntaxError.
48
+ const rawBytes = new Uint8Array(await resp.arrayBuffer());
43
49
  let body: any;
44
- const contentTypeHeader = options.responseEncoding ||
50
+ const contentTypeHeader =
51
+ options.responseEncoding ||
45
52
  resp.headers.get("x-proxy-header-content-type");
46
53
  const statusCode = +(resp.headers.get("x-proxy-status-code") || "200");
47
- if (contentTypeHeader?.startsWith("application/json")) {
48
- body = await resp.json();
54
+ if (rawBytes.length === 0) {
55
+ body = null;
56
+ } else if (contentTypeHeader?.startsWith("application/json")) {
57
+ body = JSON.parse(new TextDecoder().decode(rawBytes));
49
58
  } else if (
50
59
  contentTypeHeader?.startsWith("application/xml") ||
51
60
  contentTypeHeader?.startsWith("text/")
52
61
  ) {
53
- body = await resp.text();
62
+ body = new TextDecoder().decode(rawBytes);
54
63
  } else {
55
- body = new Uint8Array(await resp.arrayBuffer());
64
+ body = rawBytes;
56
65
  }
57
66
  return {
58
67
  ok: resp.ok,
@@ -66,7 +75,7 @@ export const netApi = new LuaTable({
66
75
  (uri: string, options: { uri?: string; encoding?: string } = {}) => {
67
76
  options.uri = uri;
68
77
  return client.clientSystem.serviceRegistry.invokeBestMatch(
69
- "net.readURI:" + uri,
78
+ `net.readURI:${uri}`,
70
79
  options,
71
80
  );
72
81
  },
@@ -74,7 +83,7 @@ export const netApi = new LuaTable({
74
83
  writeURI: new LuaNativeJSFunction(
75
84
  (uri: string, content: string | Uint8Array) => {
76
85
  return client.clientSystem.serviceRegistry.invokeBestMatch(
77
- "net.writeURI:" + uri,
86
+ `net.writeURI:${uri}`,
78
87
  { uri, content },
79
88
  );
80
89
  },
@@ -85,8 +94,11 @@ export const netApi = new LuaTable({
85
94
  function buildProxyUrl(client: Client, url: string) {
86
95
  url = url.replace(/^https?:\/\//, "");
87
96
  // Strip off the /.fs and replace with /.proxy
88
- return client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
89
- "/.proxy/" + url;
97
+ return (
98
+ client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
99
+ "/.proxy/" +
100
+ url
101
+ );
90
102
  }
91
103
 
92
104
  function buildProxyHeaders(headers?: Record<string, any>): Record<string, any> {
@@ -100,9 +112,7 @@ function buildProxyHeaders(headers?: Record<string, any>): Record<string, any> {
100
112
  return newHeaders;
101
113
  }
102
114
 
103
- function extractProxyHeaders(
104
- headers: Headers,
105
- ): Record<string, any> {
115
+ function extractProxyHeaders(headers: Headers): Record<string, any> {
106
116
  const newHeaders: Record<string, any> = {};
107
117
  for (const [key, value] of headers.entries()) {
108
118
  if (key.toLowerCase().startsWith("x-proxy-header-")) {