@silverbulletmd/silverbullet 2.4.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 (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. package/plugs/builtin_plugs.ts +14 -0
@@ -0,0 +1,1702 @@
1
+ import type { ASTCtx, LuaFunctionBody, NumericType } from "./ast.ts";
2
+ import { evalStatement } from "./eval.ts";
3
+ import { asyncQuickSort } from "./util.ts";
4
+ import { isPromise, rpAll } from "./rp.ts";
5
+ import { isNegativeZero, isTaggedFloat } from "./numeric.ts";
6
+ import { luaFormat } from "./stdlib/format.ts";
7
+
8
+ export type LuaType =
9
+ | "nil"
10
+ | "boolean"
11
+ | "number"
12
+ | "string"
13
+ | "table"
14
+ | "function"
15
+ | "userdata"
16
+ | "thread";
17
+
18
+ // These types are for documentation only
19
+ export type LuaValue = any;
20
+ export type JSValue = any;
21
+
22
+ export interface ILuaFunction {
23
+ call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue;
24
+
25
+ asString(): string;
26
+ }
27
+
28
+ export interface ILuaSettable {
29
+ set(
30
+ key: LuaValue,
31
+ value: LuaValue,
32
+ sf?: LuaStackFrame,
33
+ numType?: NumericType,
34
+ ): void | Promise<void>;
35
+ }
36
+
37
+ export interface ILuaGettable {
38
+ get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null;
39
+ getNumericType?(key: LuaValue): NumericType | undefined;
40
+ }
41
+
42
+ // Small helpers for type safety/readability
43
+ export function isILuaFunction(v: unknown): v is ILuaFunction {
44
+ return !!v && typeof (v as any).call === "function";
45
+ }
46
+
47
+ export function isLuaTable(v: unknown): v is LuaTable {
48
+ return v instanceof LuaTable;
49
+ }
50
+
51
+ export function toNumKey(key: unknown): string | number {
52
+ if (isTaggedFloat(key)) {
53
+ return key.value;
54
+ }
55
+ if (typeof key === "number" || typeof key === "string") {
56
+ return key;
57
+ }
58
+ return key as unknown as string | number;
59
+ }
60
+
61
+ export function ctxOrNull(sf?: LuaStackFrame): ASTCtx | null {
62
+ return sf?.astCtx ?? null;
63
+ }
64
+
65
+ // Reuse a single empty context to avoid allocating `{}` in hot paths
66
+ const EMPTY_CTX = {} as ASTCtx;
67
+
68
+ const MAX_TAG_LOOP = 200;
69
+
70
+ // Close-stack support
71
+ export type LuaCloseEntry = { value: LuaValue; ctx: ASTCtx };
72
+
73
+ type LuaThreadState = {
74
+ closeStack?: LuaCloseEntry[];
75
+ };
76
+
77
+ function isLuaNumber(v: any): boolean {
78
+ return typeof v === "number" || isTaggedFloat(v);
79
+ }
80
+
81
+ export function luaTypeName(val: any): LuaType {
82
+ if (val === null || val === undefined) {
83
+ return "nil";
84
+ }
85
+
86
+ const t = luaTypeOf(val);
87
+
88
+ if (typeof t === "string") {
89
+ return t;
90
+ }
91
+
92
+ const ty = typeof val;
93
+ if (ty === "number") {
94
+ return "number";
95
+ }
96
+ if (ty === "string") {
97
+ return "string";
98
+ }
99
+ if (ty === "boolean") {
100
+ return "boolean";
101
+ }
102
+ if (ty === "function") {
103
+ return "function";
104
+ }
105
+ if (Array.isArray(val)) {
106
+ return "table";
107
+ }
108
+ if (ty === "object" && (val as any).constructor === Object) {
109
+ return "table";
110
+ }
111
+
112
+ return "userdata";
113
+ }
114
+
115
+ // Check whether a value is callable without invoking it.
116
+ export function luaIsCallable(
117
+ v: LuaValue,
118
+ sf: LuaStackFrame,
119
+ ): boolean {
120
+ if (v === null || v === undefined) {
121
+ return false;
122
+ }
123
+ if (typeof v === "function") {
124
+ return true;
125
+ }
126
+ if (isILuaFunction(v)) {
127
+ return true;
128
+ }
129
+ if (v instanceof LuaTable) {
130
+ const mt = getMetatable(v, sf);
131
+ if (mt && mt.has("__call")) {
132
+ const mm = mt.get("__call", sf);
133
+ return !!mm && (typeof mm === "function" || isILuaFunction(mm));
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ // In Lua, `__close` must be a function (no `__call` fallback).
140
+ function luaIsCloseMethod(
141
+ v: LuaValue,
142
+ ): boolean {
143
+ return typeof v === "function" || isILuaFunction(v);
144
+ }
145
+
146
+ export function luaEnsureCloseStack(sf: LuaStackFrame): LuaCloseEntry[] {
147
+ if (!sf.threadState.closeStack) {
148
+ sf.threadState.closeStack = [];
149
+ }
150
+ return sf.threadState.closeStack as LuaCloseEntry[];
151
+ }
152
+
153
+ export function luaMarkToBeClosed(
154
+ sf: LuaStackFrame,
155
+ value: LuaValue,
156
+ ctx: ASTCtx,
157
+ ): void {
158
+ const closeStack = luaEnsureCloseStack(sf);
159
+
160
+ // In Lua, `nil` is not closed.
161
+ if (value === null) {
162
+ return;
163
+ }
164
+
165
+ const mt = getMetatable(value, sf);
166
+ if (!mt || !mt.has("__close")) {
167
+ throw new LuaRuntimeError(
168
+ "variable got a non-closable value",
169
+ sf.withCtx(ctx),
170
+ );
171
+ }
172
+
173
+ const mm = mt.get("__close");
174
+ if (!luaIsCloseMethod(mm)) {
175
+ throw new LuaRuntimeError(
176
+ "variable got a non-closable value",
177
+ sf.withCtx(ctx),
178
+ );
179
+ }
180
+
181
+ closeStack.push({ value, ctx });
182
+ }
183
+
184
+ // Close entries from a mark (LIFO) and shrink stack back to mark. This
185
+ // is the core semantic for block exits and protected call boundaries.
186
+ export function luaCloseFromMark(
187
+ sf: LuaStackFrame,
188
+ mark: number,
189
+ errObj: LuaValue | null,
190
+ ): Promise<void> | void {
191
+ const closeStack = sf.threadState?.closeStack as LuaCloseEntry[] | undefined;
192
+ if (!closeStack) {
193
+ return;
194
+ }
195
+ if (closeStack.length <= mark) {
196
+ return;
197
+ }
198
+
199
+ const callClose = (entry: LuaCloseEntry): LuaValue | Promise<LuaValue> => {
200
+ const mt = getMetatable(entry.value, sf);
201
+ const mm = mt ? mt.get("__close", sf) : null;
202
+ if (!luaIsCloseMethod(mm)) {
203
+ throw new LuaRuntimeError(
204
+ "metamethod '__close' is not callable",
205
+ sf.withCtx(entry.ctx),
206
+ );
207
+ }
208
+ if (errObj === null) {
209
+ return luaCall(mm, [entry.value], entry.ctx, sf);
210
+ }
211
+ return luaCall(mm, [entry.value, errObj], entry.ctx, sf);
212
+ };
213
+
214
+ // Close all to-be-closed variables (LIFO) even if one close errors.
215
+ // The reported error should be the first close error encountered.
216
+ const runFrom = (i: number): void | Promise<void> => {
217
+ let firstErr: unknown | null = null;
218
+
219
+ const recordErr = (e: unknown) => {
220
+ if (firstErr === null) {
221
+ firstErr = e;
222
+ }
223
+ };
224
+
225
+ const next = (idx: number): void | Promise<void> => {
226
+ for (let j = idx; j >= mark; j--) {
227
+ let r: LuaValue | Promise<LuaValue>;
228
+ try {
229
+ r = callClose(closeStack[j]);
230
+ } catch (e) {
231
+ recordErr(e);
232
+ continue;
233
+ }
234
+
235
+ if (isPromise(r)) {
236
+ return (r as Promise<any>).then(
237
+ () => next(j - 1),
238
+ (e: any) => {
239
+ recordErr(e);
240
+ return next(j - 1);
241
+ },
242
+ );
243
+ }
244
+ }
245
+
246
+ closeStack.length = mark;
247
+ if (firstErr !== null) {
248
+ throw firstErr;
249
+ }
250
+ };
251
+
252
+ return next(i);
253
+ };
254
+
255
+ return runFrom(closeStack.length - 1);
256
+ }
257
+
258
+ export class LuaEnv implements ILuaSettable, ILuaGettable {
259
+ variables = new Map<string, LuaValue>();
260
+
261
+ private readonly consts = new Set<string>();
262
+ private readonly numericTypes = new Map<string, NumericType>();
263
+
264
+ constructor(readonly parent?: LuaEnv) {
265
+ }
266
+
267
+ setLocal(name: string, value: LuaValue, numType?: NumericType) {
268
+ this.variables.set(name, value);
269
+ if (isLuaNumber(value) && numType) {
270
+ this.numericTypes.set(name, numType);
271
+ } else {
272
+ this.numericTypes.delete(name);
273
+ }
274
+ }
275
+
276
+ setLocalConst(name: string, value: LuaValue, numType?: NumericType) {
277
+ this.variables.set(name, value);
278
+ this.consts.add(name);
279
+ if (isLuaNumber(value) && numType) {
280
+ this.numericTypes.set(name, numType);
281
+ } else {
282
+ this.numericTypes.delete(name);
283
+ }
284
+ }
285
+
286
+ set(
287
+ key: string,
288
+ value: LuaValue,
289
+ sf?: LuaStackFrame,
290
+ numType?: NumericType,
291
+ ): void {
292
+ if (this.variables.has(key) || !this.parent) {
293
+ if (this.consts.has(key)) {
294
+ throw new LuaRuntimeError(
295
+ `attempt to assign to const variable '${key}'`,
296
+ sf || LuaStackFrame.lostFrame,
297
+ );
298
+ }
299
+ this.variables.set(key, value);
300
+ if (isLuaNumber(value) && numType) {
301
+ this.numericTypes.set(key, numType);
302
+ } else {
303
+ this.numericTypes.delete(key);
304
+ }
305
+ } else {
306
+ this.parent.set(key, value, sf, numType);
307
+ }
308
+ }
309
+
310
+ getNumericType(name: string): NumericType | undefined {
311
+ if (this.numericTypes.has(name)) {
312
+ return this.numericTypes.get(name);
313
+ }
314
+ if (this.parent) {
315
+ return this.parent.getNumericType(name);
316
+ }
317
+ return undefined;
318
+ }
319
+
320
+ has(key: string): boolean {
321
+ if (this.variables.has(key)) {
322
+ return true;
323
+ }
324
+ if (this.parent) {
325
+ return this.parent.has(key);
326
+ }
327
+ return false;
328
+ }
329
+
330
+ get(
331
+ name: string,
332
+ _sf?: LuaStackFrame,
333
+ ): Promise<LuaValue> | LuaValue | null {
334
+ if (this.variables.has(name)) {
335
+ return this.variables.get(name);
336
+ }
337
+ if (this.parent) {
338
+ return this.parent.get(name, _sf);
339
+ }
340
+ return null;
341
+ }
342
+
343
+ /**
344
+ * Lists all keys in the environment including its parents
345
+ */
346
+ keys(): string[] {
347
+ const keys = Array.from(this.variables.keys());
348
+ if (this.parent) {
349
+ return keys.concat(this.parent.keys());
350
+ }
351
+ return keys;
352
+ }
353
+
354
+ toJSON(omitKeys: string[] = []): Record<string, any> {
355
+ const result: Record<string, any> = {};
356
+ for (const key of this.keys()) {
357
+ if (omitKeys.includes(key)) {
358
+ continue;
359
+ }
360
+ result[key] = luaValueToJS(this.get(key), LuaStackFrame.lostFrame);
361
+ }
362
+ return result;
363
+ }
364
+ }
365
+
366
+ export class LuaStackFrame {
367
+ // Must not share mutable per-thread state across calls/tests. This is
368
+ // a getter that returns a fresh frame each time.
369
+ static get lostFrame(): LuaStackFrame {
370
+ return new LuaStackFrame(new LuaEnv(), null, undefined, undefined, {
371
+ closeStack: undefined,
372
+ });
373
+ }
374
+
375
+ constructor(
376
+ readonly threadLocal: LuaEnv,
377
+ readonly astCtx: ASTCtx | null,
378
+ readonly parent?: LuaStackFrame,
379
+ readonly currentFunction?: LuaFunction,
380
+ readonly threadState: LuaThreadState = { closeStack: undefined },
381
+ ) {
382
+ }
383
+
384
+ static createWithGlobalEnv(
385
+ globalEnv: LuaEnv,
386
+ ctx: ASTCtx | null = null,
387
+ ): LuaStackFrame {
388
+ const env = new LuaEnv();
389
+ env.setLocal("_GLOBAL", globalEnv);
390
+ return new LuaStackFrame(env, ctx, undefined, undefined, {
391
+ closeStack: undefined,
392
+ });
393
+ }
394
+
395
+ withCtx(ctx: ASTCtx): LuaStackFrame {
396
+ return new LuaStackFrame(
397
+ this.threadLocal,
398
+ ctx,
399
+ this,
400
+ this.currentFunction,
401
+ this.threadState,
402
+ );
403
+ }
404
+
405
+ withFunction(fn: LuaFunction): LuaStackFrame {
406
+ return new LuaStackFrame(
407
+ this.threadLocal,
408
+ this.astCtx,
409
+ this.parent,
410
+ fn,
411
+ this.threadState,
412
+ );
413
+ }
414
+ }
415
+
416
+ export class LuaMultiRes {
417
+ values: any[];
418
+
419
+ constructor(values: LuaValue[] | LuaValue) {
420
+ if (values instanceof LuaMultiRes) {
421
+ this.values = values.values;
422
+ } else {
423
+ this.values = Array.isArray(values) ? values : [values];
424
+ }
425
+ }
426
+
427
+ unwrap(): any {
428
+ if (this.values.length === 0) {
429
+ return null;
430
+ }
431
+ return this.values[0];
432
+ }
433
+
434
+ // Takes an array of either LuaMultiRes or LuaValue and flattens them into a single LuaMultiRes
435
+ flatten(): LuaMultiRes {
436
+ const result: any[] = [];
437
+
438
+ for (const value of this.values) {
439
+ if (value instanceof LuaMultiRes) {
440
+ result.push(...value.values);
441
+ } else {
442
+ result.push(value);
443
+ }
444
+ }
445
+
446
+ return new LuaMultiRes(result);
447
+ }
448
+ }
449
+
450
+ export function singleResult(value: any): any {
451
+ if (value instanceof LuaMultiRes) {
452
+ return value.unwrap();
453
+ }
454
+ return value;
455
+ }
456
+
457
+ export class LuaFunction implements ILuaFunction {
458
+ private capturedEnv: LuaEnv;
459
+ funcHasGotos?: boolean;
460
+
461
+ constructor(readonly body: LuaFunctionBody, closure: LuaEnv) {
462
+ this.capturedEnv = closure;
463
+ }
464
+
465
+ call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
466
+ // Create a new environment that chains to the captured environment
467
+ const env = new LuaEnv(this.capturedEnv);
468
+ if (!sf) {
469
+ console.trace(sf);
470
+ }
471
+ // Set _CTX to the thread local environment from the stack frame
472
+ env.setLocal("_CTX", sf.threadLocal);
473
+
474
+ // Eval using a stack frame that knows the current function
475
+ const sfWithFn = sf.currentFunction === this ? sf : sf.withFunction(this);
476
+
477
+ // Resolve args (sync-first)
478
+ const argsRP = rpAll(args as any[]);
479
+ const resolveArgs = (resolvedArgs: any[]) => {
480
+ // Assign parameter values to variable names in env
481
+ let varargs: LuaValue[] = [];
482
+ for (let i = 0; i < this.body.parameters.length; i++) {
483
+ const paramName = this.body.parameters[i];
484
+ if (paramName === "...") {
485
+ // Vararg parameter, let's collect the remainder of the resolved args into the varargs array
486
+ varargs = resolvedArgs.slice(i);
487
+ // Done, break out of this loop
488
+ break;
489
+ }
490
+ env.setLocal(paramName, resolvedArgs[i] ?? null);
491
+ }
492
+ env.setLocal("...", new LuaMultiRes(varargs));
493
+
494
+ // Evaluate the function body with returnOnReturn set to true
495
+ const r = evalStatement(this.body.block, env, sfWithFn, true);
496
+
497
+ const map = (val: any) => {
498
+ if (val === undefined) {
499
+ return;
500
+ }
501
+ if (val && typeof val === "object" && val.ctrl === "return") {
502
+ return mapFunctionReturnValue(val.values);
503
+ }
504
+ if (val && typeof val === "object" && val.ctrl === "break") {
505
+ throw new LuaRuntimeError(
506
+ "break outside loop",
507
+ sfWithFn.withCtx(this.body.block.ctx),
508
+ );
509
+ }
510
+ if (val && typeof val === "object" && val.ctrl === "goto") {
511
+ throw new LuaRuntimeError(
512
+ "unexpected goto signal",
513
+ sfWithFn.withCtx(this.body.block.ctx),
514
+ );
515
+ }
516
+ };
517
+
518
+ if (isPromise(r)) {
519
+ return r.then(map);
520
+ }
521
+ return map(r);
522
+ };
523
+
524
+ if (isPromise(argsRP)) {
525
+ return argsRP.then(resolveArgs);
526
+ }
527
+ return resolveArgs(argsRP);
528
+ }
529
+
530
+ asString(): string {
531
+ return `<lua function(${this.body.parameters.join(", ")})>`;
532
+ }
533
+
534
+ toString(): string {
535
+ return this.asString();
536
+ }
537
+ }
538
+
539
+ function mapFunctionReturnValue(values: any[]): any {
540
+ if (values.length === 0) {
541
+ return;
542
+ }
543
+
544
+ if (values.length === 1) {
545
+ return values[0];
546
+ }
547
+
548
+ return new LuaMultiRes(values);
549
+ }
550
+
551
+ export class LuaNativeJSFunction implements ILuaFunction {
552
+ constructor(readonly fn: (...args: JSValue[]) => JSValue) {
553
+ }
554
+
555
+ // Performs automatic conversion between Lua and JS values for arguments, but not for return values
556
+ call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
557
+ const jsArgsRP = args.map((v) => luaValueToJS(v, sf));
558
+ const resolved = rpAll(jsArgsRP);
559
+ if (isPromise(resolved)) {
560
+ return resolved.then((jsArgs) => this.fn(...jsArgs));
561
+ }
562
+ return this.fn(...resolved);
563
+ }
564
+
565
+ asString(): string {
566
+ return `<native js function: ${this.fn.name}>`;
567
+ }
568
+
569
+ toString(): string {
570
+ return this.asString();
571
+ }
572
+ }
573
+
574
+ export class LuaBuiltinFunction implements ILuaFunction {
575
+ constructor(
576
+ readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue,
577
+ ) {
578
+ }
579
+
580
+ call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
581
+ // _CTX is already available via the stack frame
582
+ return this.fn(sf, ...args);
583
+ }
584
+
585
+ asString(): string {
586
+ return `<builtin lua function>`;
587
+ }
588
+
589
+ toString(): string {
590
+ return this.asString();
591
+ }
592
+ }
593
+
594
+ export class LuaTable implements ILuaSettable, ILuaGettable {
595
+ // To optimize the table implementation we use a combination of different data structures
596
+ public metatable: LuaTable | null;
597
+
598
+ // When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
599
+ private stringKeys: Record<string, any>;
600
+ // Other keys we can support using a Map as a fallback
601
+ private otherKeys: Map<any, any> | null;
602
+ // When tables are used as arrays, we use a native JavaScript array for that
603
+ private arrayPart: any[];
604
+
605
+ // Numeric type metadata at storage boundaries
606
+ private readonly stringKeyTypes = new Map<string, NumericType>();
607
+ private otherKeyTypes: Map<any, NumericType> | null = null;
608
+ private readonly arrayTypes: (NumericType | undefined)[] = [];
609
+
610
+ constructor(init?: any[] | Record<string, any>) {
611
+ // For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
612
+ this.arrayPart = Array.isArray(init) ? init : [];
613
+ this.stringKeys = init && !Array.isArray(init) ? init : {};
614
+
615
+ if (init && !Array.isArray(init)) {
616
+ for (const k in init) {
617
+ if (Object.prototype.hasOwnProperty.call(init, k)) {
618
+ this.stringKeys[k] = (init as any)[k];
619
+ }
620
+ }
621
+ }
622
+ this.otherKeys = null; // Only create this when needed
623
+ this.metatable = null;
624
+ }
625
+
626
+ private static numKeyValue(key: any): number | null {
627
+ if (isTaggedFloat(key)) {
628
+ return key.value;
629
+ }
630
+ if (typeof key === "number") {
631
+ return key;
632
+ }
633
+ return null;
634
+ }
635
+
636
+ // Normalize numeric keys for table indexing:
637
+ // * negative zero becomes positive zero,
638
+ // * integer-valued floats become plain integers,
639
+ // * non-integer floats stay as-is.
640
+ static normalizeNumericKey(key: any): any {
641
+ if (typeof key === "string") {
642
+ return key;
643
+ }
644
+
645
+ const numVal = LuaTable.numKeyValue(key);
646
+ if (numVal !== null) {
647
+ // Normalize -0 to +0
648
+ if (isNegativeZero(numVal)) {
649
+ return 0;
650
+ }
651
+ // Integer-valued numbers normalize to integers
652
+ if (Number.isInteger(numVal)) {
653
+ return numVal;
654
+ }
655
+ // Non-integer floats
656
+ return numVal;
657
+ }
658
+ return key;
659
+ }
660
+
661
+ private static isIntegerKey(key: any): boolean {
662
+ const norm = LuaTable.normalizeNumericKey(key);
663
+ return typeof norm === "number" && Number.isInteger(norm) && norm >= 1;
664
+ }
665
+
666
+ private static toIndex(key: any): number {
667
+ const norm = LuaTable.normalizeNumericKey(key);
668
+ const k = typeof norm === "number" ? norm : (norm as number);
669
+ return k - 1;
670
+ }
671
+
672
+ get rawLength(): number {
673
+ return this.arrayPart.length;
674
+ }
675
+
676
+ get length(): number {
677
+ let n = this.arrayPart.length;
678
+ while (n > 0) {
679
+ const v = this.arrayPart[n - 1];
680
+ if (v === null || v === undefined) {
681
+ n--;
682
+ continue;
683
+ }
684
+ break;
685
+ }
686
+ return n;
687
+ }
688
+
689
+ keys(): any[] {
690
+ const keys: any[] = [];
691
+ for (const k in this.stringKeys) {
692
+ if (Object.prototype.hasOwnProperty.call(this.stringKeys, k)) {
693
+ keys.push(k);
694
+ }
695
+ }
696
+ for (let i = 0; i < this.arrayPart.length; i++) {
697
+ keys.push(i + 1);
698
+ }
699
+ if (this.otherKeys) {
700
+ for (const key of this.otherKeys.keys()) {
701
+ keys.push(key);
702
+ }
703
+ }
704
+ return keys;
705
+ }
706
+
707
+ empty(): boolean {
708
+ for (const k in this.stringKeys) {
709
+ if (Object.prototype.hasOwnProperty.call(this.stringKeys, k)) {
710
+ return false;
711
+ }
712
+ }
713
+ if (this.arrayPart.length !== 0) {
714
+ return false;
715
+ }
716
+ if (this.otherKeys && this.otherKeys.size !== 0) {
717
+ return false;
718
+ }
719
+ return true;
720
+ }
721
+
722
+ has(key: LuaValue) {
723
+ if (typeof key === "string") {
724
+ return this.stringKeys[key] !== undefined;
725
+ }
726
+
727
+ const normalizedKey = LuaTable.normalizeNumericKey(key);
728
+
729
+ if (
730
+ typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
731
+ normalizedKey >= 1
732
+ ) {
733
+ const idx = normalizedKey - 1;
734
+ const v = this.arrayPart[idx];
735
+ if (v !== undefined) {
736
+ return true;
737
+ }
738
+ return this.otherKeys ? this.otherKeys.has(normalizedKey) : false;
739
+ }
740
+ if (typeof normalizedKey === "string") {
741
+ return this.stringKeys[normalizedKey] !== undefined;
742
+ }
743
+ if (this.otherKeys) {
744
+ return this.otherKeys.has(normalizedKey);
745
+ }
746
+
747
+ return false;
748
+ }
749
+
750
+ // Used by table constructors to preserve positional semantics
751
+ // including nils and ensure the array part grows to the final
752
+ // constructor size.
753
+ rawSetArrayIndex(
754
+ index1: number,
755
+ value: LuaValue,
756
+ numType?: NumericType,
757
+ ): void {
758
+ const idx = index1 - 1;
759
+
760
+ this.arrayPart[idx] = value;
761
+ if (isLuaNumber(value) && numType) {
762
+ this.arrayTypes[idx] = numType;
763
+ } else {
764
+ this.arrayTypes[idx] = undefined;
765
+ }
766
+ }
767
+
768
+ private promoteIntegerKeysFromHash(): void {
769
+ if (!this.otherKeys) return;
770
+
771
+ while (true) {
772
+ const nextIndex1 = this.arrayPart.length + 1;
773
+ if (!this.otherKeys.has(nextIndex1)) {
774
+ break;
775
+ }
776
+
777
+ const v = this.otherKeys.get(nextIndex1);
778
+ const nt = this.otherKeyTypes
779
+ ? this.otherKeyTypes.get(nextIndex1)
780
+ : undefined;
781
+
782
+ this.otherKeys.delete(nextIndex1);
783
+ if (this.otherKeyTypes) {
784
+ this.otherKeyTypes.delete(nextIndex1);
785
+ }
786
+
787
+ this.arrayPart.push(v);
788
+ this.arrayTypes.push(nt);
789
+ }
790
+ }
791
+
792
+ rawSet(
793
+ key: LuaValue,
794
+ value: LuaValue,
795
+ numType?: NumericType,
796
+ ): void | Promise<void> {
797
+ if (isPromise(key)) {
798
+ return key.then((key) => this.rawSet(key, value, numType));
799
+ }
800
+ if (isPromise(value)) {
801
+ return value.then((v) => this.rawSet(key, v, numType));
802
+ }
803
+
804
+ // Fast path: string keys (the dominant case)
805
+ if (typeof key === "string") {
806
+ if (value === null || value === undefined) {
807
+ delete this.stringKeys[key];
808
+ this.stringKeyTypes.delete(key);
809
+ } else {
810
+ this.stringKeys[key] = value;
811
+ if (isLuaNumber(value) && numType) {
812
+ this.stringKeyTypes.set(key, numType);
813
+ } else {
814
+ this.stringKeyTypes.delete(key);
815
+ }
816
+ }
817
+ return;
818
+ }
819
+
820
+ const normalizedKey = LuaTable.normalizeNumericKey(key);
821
+
822
+ if (typeof normalizedKey === "string") {
823
+ if (value === null || value === undefined) {
824
+ delete this.stringKeys[normalizedKey];
825
+ this.stringKeyTypes.delete(normalizedKey);
826
+ } else {
827
+ this.stringKeys[normalizedKey] = value;
828
+ if (isLuaNumber(value) && numType) {
829
+ this.stringKeyTypes.set(normalizedKey, numType);
830
+ } else {
831
+ this.stringKeyTypes.delete(normalizedKey);
832
+ }
833
+ }
834
+ return;
835
+ }
836
+
837
+ if (
838
+ typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
839
+ normalizedKey >= 1
840
+ ) {
841
+ const idx = normalizedKey - 1;
842
+
843
+ // Sparse writes (e.g. `a[7]=4` when length is 3) go to the hash
844
+ // part so that `#a` does not jump across holes.
845
+ if (idx <= this.arrayPart.length) {
846
+ this.arrayPart[idx] = value;
847
+ if (isLuaNumber(value) && numType) {
848
+ this.arrayTypes[idx] = numType;
849
+ } else {
850
+ this.arrayTypes[idx] = undefined;
851
+ }
852
+
853
+ // If we extended the array by appending, we may now be able to
854
+ // promote subsequent integer keys from the hash part.
855
+ if (idx === this.arrayPart.length - 1) {
856
+ this.promoteIntegerKeysFromHash();
857
+ }
858
+
859
+ // Trailing nil shrink
860
+ if (value === null || value === undefined) {
861
+ let n = this.arrayPart.length;
862
+ while (n > 0) {
863
+ const v = this.arrayPart[n - 1];
864
+ if (v === null || v === undefined) {
865
+ n--;
866
+ continue;
867
+ }
868
+ break;
869
+ }
870
+ if (n !== this.arrayPart.length) {
871
+ this.arrayPart.length = n;
872
+ this.arrayTypes.length = n;
873
+ }
874
+ }
875
+
876
+ return;
877
+ }
878
+
879
+ // Sparse numeric key
880
+ if (!this.otherKeys) {
881
+ this.otherKeys = new Map();
882
+ }
883
+ if (!this.otherKeyTypes) {
884
+ this.otherKeyTypes = new Map();
885
+ }
886
+
887
+ if (value === null || value === undefined) {
888
+ this.otherKeys.delete(normalizedKey);
889
+ this.otherKeyTypes.delete(normalizedKey);
890
+ } else {
891
+ this.otherKeys.set(normalizedKey, value);
892
+ if (isLuaNumber(value) && numType) {
893
+ this.otherKeyTypes.set(normalizedKey, numType);
894
+ } else {
895
+ this.otherKeyTypes.delete(normalizedKey);
896
+ }
897
+ }
898
+ return;
899
+ }
900
+
901
+ if (!this.otherKeys) {
902
+ this.otherKeys = new Map();
903
+ }
904
+ if (!this.otherKeyTypes) {
905
+ this.otherKeyTypes = new Map();
906
+ }
907
+
908
+ if (value === null || value === undefined) {
909
+ this.otherKeys.delete(normalizedKey);
910
+ this.otherKeyTypes.delete(normalizedKey);
911
+ } else {
912
+ this.otherKeys.set(normalizedKey, value);
913
+ if (isLuaNumber(value) && numType) {
914
+ this.otherKeyTypes.set(normalizedKey, numType);
915
+ } else {
916
+ this.otherKeyTypes.delete(normalizedKey);
917
+ }
918
+ }
919
+ }
920
+
921
+ set(
922
+ key: LuaValue,
923
+ value: LuaValue,
924
+ sf?: LuaStackFrame,
925
+ numType?: NumericType,
926
+ ): Promise<void> | void {
927
+ const errSf = sf || LuaStackFrame.lostFrame;
928
+ const ctx = sf?.astCtx ?? EMPTY_CTX;
929
+
930
+ if (this.has(key)) {
931
+ return this.rawSet(key, value, numType);
932
+ }
933
+
934
+ if (this.metatable === null) {
935
+ return this.rawSet(key, value, numType);
936
+ }
937
+
938
+ const newIndexMM = this.metatable.rawGet("__newindex");
939
+
940
+ if (newIndexMM === undefined || newIndexMM === null) {
941
+ return this.rawSet(key, value, numType);
942
+ }
943
+
944
+ const k: LuaValue = key;
945
+ const v: LuaValue = value;
946
+ const nt: NumericType | undefined = numType;
947
+
948
+ let target: LuaValue | null = null;
949
+
950
+ for (let loop = 0; loop < MAX_TAG_LOOP; loop++) {
951
+ const t = target === null ? this : target;
952
+
953
+ if (t instanceof LuaTable) {
954
+ if (t.has(k)) {
955
+ return t.rawSet(k, v, nt);
956
+ }
957
+
958
+ const mt = t.metatable;
959
+ if (!mt) {
960
+ return t.rawSet(k, v, nt);
961
+ }
962
+
963
+ const mm = mt.rawGet("__newindex");
964
+ const hasMM = !(mm === undefined || mm === null);
965
+
966
+ if (!hasMM) {
967
+ return t.rawSet(k, v, nt);
968
+ }
969
+
970
+ // Function metamethod: call and stop
971
+ if (typeof mm === "function" || isILuaFunction(mm)) {
972
+ return luaCall(mm, [t, k, v], ctx, errSf);
973
+ }
974
+
975
+ // Table/env metamethod: forward assignment
976
+ if (mm instanceof LuaTable || mm instanceof LuaEnv) {
977
+ target = mm;
978
+ continue;
979
+ }
980
+
981
+ const ty = luaTypeOf(mm) as any as string;
982
+ throw new LuaRuntimeError(
983
+ `attempt to index a ${ty} value`,
984
+ errSf.withCtx(ctx),
985
+ );
986
+ }
987
+
988
+ const ty = luaTypeOf(t) as any as string;
989
+ throw new LuaRuntimeError(
990
+ `attempt to index a ${ty} value`,
991
+ errSf.withCtx(ctx),
992
+ );
993
+ }
994
+
995
+ throw new LuaRuntimeError(
996
+ "'__newindex' chain too long; possible loop",
997
+ errSf.withCtx(ctx),
998
+ );
999
+ }
1000
+
1001
+ getNumericType(key: LuaValue): NumericType | undefined {
1002
+ if (typeof key === "string") {
1003
+ return this.stringKeyTypes.get(key);
1004
+ }
1005
+ if (LuaTable.isIntegerKey(key)) {
1006
+ return this.arrayTypes[LuaTable.toIndex(key)];
1007
+ }
1008
+ if (this.otherKeyTypes) {
1009
+ return this.otherKeyTypes.get(key);
1010
+ }
1011
+ return undefined;
1012
+ }
1013
+
1014
+ rawGet(key: LuaValue): LuaValue | null {
1015
+ if (typeof key === "string") {
1016
+ return this.stringKeys[key];
1017
+ }
1018
+
1019
+ const normalizedKey = LuaTable.normalizeNumericKey(key);
1020
+
1021
+ if (typeof normalizedKey === "string") {
1022
+ return this.stringKeys[normalizedKey];
1023
+ }
1024
+
1025
+ if (
1026
+ typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
1027
+ normalizedKey >= 1
1028
+ ) {
1029
+ const idx = normalizedKey - 1;
1030
+ const v = this.arrayPart[idx];
1031
+ if (v !== undefined) {
1032
+ return v;
1033
+ }
1034
+ // Sparse integer keys can live in the hash part.
1035
+ if (this.otherKeys) {
1036
+ return this.otherKeys.get(normalizedKey);
1037
+ }
1038
+ return undefined;
1039
+ }
1040
+
1041
+ if (this.otherKeys) {
1042
+ return this.otherKeys.get(normalizedKey);
1043
+ }
1044
+ }
1045
+
1046
+ get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null {
1047
+ return luaIndexValue(this, key, sf);
1048
+ }
1049
+
1050
+ insert(value: LuaValue, pos: number) {
1051
+ this.arrayPart.splice(pos - 1, 0, value);
1052
+ }
1053
+
1054
+ remove(pos: number) {
1055
+ this.arrayPart.splice(pos - 1, 1);
1056
+ }
1057
+
1058
+ async sort(fn?: ILuaFunction, sf?: LuaStackFrame) {
1059
+ if (fn && sf) {
1060
+ this.arrayPart = await asyncQuickSort(this.arrayPart, async (a, b) => {
1061
+ return (await fn.call(sf, a, b)) ? -1 : 1;
1062
+ });
1063
+ } else {
1064
+ this.arrayPart.sort();
1065
+ }
1066
+ }
1067
+
1068
+ toJSObject(sf = LuaStackFrame.lostFrame): Record<string, any> {
1069
+ const result: Record<string, any> = {};
1070
+ for (const key of this.keys()) {
1071
+ result[key] = luaValueToJS(this.get(key, sf), sf);
1072
+ }
1073
+ return result;
1074
+ }
1075
+
1076
+ toJSArray(sf = LuaStackFrame.lostFrame): any[] {
1077
+ return this.arrayPart.map((v) => luaValueToJS(v, sf));
1078
+ }
1079
+
1080
+ toJS(sf = LuaStackFrame.lostFrame): Record<string, any> | any[] {
1081
+ if (this.length > 0) {
1082
+ return this.toJSArray(sf);
1083
+ }
1084
+ return this.toJSObject(sf);
1085
+ }
1086
+
1087
+ async toStringAsync(): Promise<string> {
1088
+ const metatable = getMetatable(this);
1089
+ if (metatable) {
1090
+ const mm = metatable.rawGet("__tostring");
1091
+ if (!(mm === undefined || mm === null)) {
1092
+ const ctx = EMPTY_CTX;
1093
+ const sf = LuaStackFrame.lostFrame.withCtx(ctx);
1094
+
1095
+ const r = luaCall(mm, [this], ctx, sf);
1096
+ const v = isPromise(r) ? await r : r;
1097
+
1098
+ const s = singleResult(v);
1099
+ if (typeof s !== "string") {
1100
+ throw new LuaRuntimeError(
1101
+ "'__tostring' must return a string",
1102
+ sf,
1103
+ );
1104
+ }
1105
+ return s;
1106
+ }
1107
+ }
1108
+
1109
+ let result = "{";
1110
+ let first = true;
1111
+ for (const key of this.keys()) {
1112
+ if (first) {
1113
+ first = false;
1114
+ } else {
1115
+ result += ", ";
1116
+ }
1117
+ if (typeof key === "number") {
1118
+ result += await luaToString(this.get(key));
1119
+ continue;
1120
+ }
1121
+ if (typeof key === "string") {
1122
+ result += key;
1123
+ } else {
1124
+ result += "[" + key + "]";
1125
+ }
1126
+ result += " = " + await luaToString(this.get(key));
1127
+ }
1128
+ result += "}";
1129
+ return result;
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Lookup a key in a table or a metatable
1135
+ */
1136
+ export function luaIndexValue(
1137
+ value: LuaValue,
1138
+ key: LuaValue,
1139
+ sf?: LuaStackFrame,
1140
+ ): LuaValue | Promise<LuaValue> | null {
1141
+ // `nil` handling is done by luaGet() which has better context;
1142
+ // keep this defensive for direct callers.
1143
+ if (value === null || value === undefined) {
1144
+ return null;
1145
+ }
1146
+
1147
+ const errSf = sf || LuaStackFrame.lostFrame;
1148
+ const ctx = sf?.astCtx ?? EMPTY_CTX;
1149
+
1150
+ let t: LuaValue = value;
1151
+
1152
+ for (let loop = 0; loop < MAX_TAG_LOOP; loop++) {
1153
+ // Primitive get when table
1154
+ if (t instanceof LuaTable) {
1155
+ const raw = t.rawGet(key);
1156
+ if (raw !== undefined) {
1157
+ return raw;
1158
+ }
1159
+ // If no metatable, raw miss => nil
1160
+ if (t.metatable === null) {
1161
+ return null;
1162
+ }
1163
+ }
1164
+
1165
+ const mt = getMetatable(t, errSf);
1166
+ const mm = mt ? mt.rawGet("__index") : null;
1167
+
1168
+ if (mm === undefined || mm === null) {
1169
+ // Strict Lua: only tables are indexable without a metamethod.
1170
+ // For a table, raw miss yields nil; for non-table, it's a type error.
1171
+ if (t instanceof LuaTable) {
1172
+ return null;
1173
+ }
1174
+ const ty = luaTypeOf(t) as any as string;
1175
+ throw new LuaRuntimeError(
1176
+ `attempt to index a ${ty} value`,
1177
+ errSf.withCtx(ctx),
1178
+ );
1179
+ }
1180
+
1181
+ // Function metamethod
1182
+ if (typeof mm === "function" || isILuaFunction(mm)) {
1183
+ return luaCall(mm, [t, key], ctx, errSf);
1184
+ }
1185
+
1186
+ // Table/metatable delegation: repeat with mm as new "t"
1187
+ if (mm instanceof LuaTable || mm instanceof LuaEnv) {
1188
+ t = mm;
1189
+ continue;
1190
+ }
1191
+
1192
+ // Bad metamethod type: make it a Lua-like type error
1193
+ const ty = luaTypeOf(mm) as any as string;
1194
+ throw new LuaRuntimeError(
1195
+ `attempt to index a ${ty} value`,
1196
+ errSf.withCtx(ctx),
1197
+ );
1198
+ }
1199
+
1200
+ throw new LuaRuntimeError(
1201
+ "'__index' chain too long; possible loop",
1202
+ errSf.withCtx(ctx),
1203
+ );
1204
+ }
1205
+
1206
+ export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
1207
+
1208
+ export async function luaSet(
1209
+ obj: any,
1210
+ key: any,
1211
+ value: any,
1212
+ sf: LuaStackFrame,
1213
+ numType?: NumericType,
1214
+ ): Promise<void> {
1215
+ if (!obj) {
1216
+ throw new LuaRuntimeError(
1217
+ `Not a settable object: nil`,
1218
+ sf,
1219
+ );
1220
+ }
1221
+
1222
+ const normKey = isTaggedFloat(key) ? key.value : key;
1223
+
1224
+ if (obj instanceof LuaTable || obj instanceof LuaEnv) {
1225
+ await obj.set(normKey, value, sf, numType);
1226
+ } else {
1227
+ const k = toNumKey(normKey);
1228
+ (obj as Record<string | number, any>)[k] = await luaValueToJS(value, sf);
1229
+ }
1230
+ }
1231
+
1232
+ export function luaGet(
1233
+ obj: any,
1234
+ key: any,
1235
+ ctx: ASTCtx | null,
1236
+ sf: LuaStackFrame,
1237
+ ): Promise<any> | any {
1238
+ const errSf = ctx ? sf.withCtx(ctx) : sf;
1239
+
1240
+ if (obj === null || obj === undefined) {
1241
+ throw new LuaRuntimeError(
1242
+ `attempt to index a nil value`,
1243
+ errSf,
1244
+ );
1245
+ }
1246
+ if (key === null || key === undefined) {
1247
+ throw new LuaRuntimeError(
1248
+ `attempt to index with a nil key`,
1249
+ errSf,
1250
+ );
1251
+ }
1252
+
1253
+ if (obj instanceof LuaTable || obj instanceof LuaEnv) {
1254
+ return obj.get(key, sf);
1255
+ }
1256
+ if (typeof key === "number") {
1257
+ return (obj as any[])[key - 1];
1258
+ }
1259
+ if (isTaggedFloat(key)) {
1260
+ return (obj as any[])[key.value - 1];
1261
+ }
1262
+ // Native JS object
1263
+ const k = toNumKey(key);
1264
+ const val = (obj as Record<string | number, any>)[k];
1265
+ if (typeof val === "function") {
1266
+ // Automatically bind the function to the object
1267
+ return val.bind(obj);
1268
+ }
1269
+ if (val === undefined) {
1270
+ return null;
1271
+ }
1272
+ return val;
1273
+ }
1274
+
1275
+ export function luaLen(
1276
+ obj: any,
1277
+ sf?: LuaStackFrame,
1278
+ ): number {
1279
+ if (typeof obj === "string") {
1280
+ return obj.length;
1281
+ }
1282
+ if (Array.isArray(obj)) {
1283
+ return obj.length;
1284
+ }
1285
+ if (obj instanceof LuaTable) {
1286
+ return obj.rawLength;
1287
+ }
1288
+
1289
+ const t = luaTypeOf(obj) as LuaType;
1290
+ throw new LuaRuntimeError(
1291
+ `bad argument #1 to 'rawlen' (table or string expected, got ${t})`,
1292
+ sf || LuaStackFrame.lostFrame,
1293
+ );
1294
+ }
1295
+
1296
+ export function luaCall(
1297
+ callee: any,
1298
+ args: any[],
1299
+ ctx: ASTCtx,
1300
+ sf?: LuaStackFrame,
1301
+ ): any {
1302
+ if (!callee) {
1303
+ throw new LuaRuntimeError(
1304
+ `attempt to call a nil value`,
1305
+ (sf || LuaStackFrame.lostFrame).withCtx(ctx),
1306
+ );
1307
+ }
1308
+
1309
+ // Fast path: native JS function
1310
+ if (typeof callee === "function") {
1311
+ const jsArgs = rpAll(
1312
+ args.map((v) => luaValueToJS(v, sf || LuaStackFrame.lostFrame)),
1313
+ );
1314
+
1315
+ if (isPromise(jsArgs)) {
1316
+ return jsArgs.then((resolved) =>
1317
+ (callee as (...a: any[]) => any)(...resolved)
1318
+ );
1319
+ }
1320
+ return (callee as (...a: any[]) => any)(...jsArgs);
1321
+ }
1322
+
1323
+ // Lua table: may be callable via __call metamethod
1324
+ if (callee instanceof LuaTable) {
1325
+ const metatable = getMetatable(callee, sf);
1326
+
1327
+ // Metamethod lookup must be raw (no __index involvement).
1328
+ const mm = metatable ? metatable.rawGet("__call") : null;
1329
+
1330
+ if (!(mm === undefined || mm === null)) {
1331
+ const isCallable = (v: any): boolean => {
1332
+ if (v === null || v === undefined) return false;
1333
+ if (typeof v === "function") return true;
1334
+ if (isILuaFunction(v)) return true;
1335
+ if (v instanceof LuaTable) {
1336
+ const mt2 = getMetatable(v, sf);
1337
+ const mm2 = mt2 ? mt2.rawGet("__call") : null;
1338
+ return !(mm2 === undefined || mm2 === null);
1339
+ }
1340
+ return false;
1341
+ };
1342
+
1343
+ if (!isCallable(mm)) {
1344
+ throw new LuaRuntimeError(
1345
+ `attempt to call a ${luaTypeOf(mm)} value`,
1346
+ (sf || LuaStackFrame.lostFrame).withCtx(ctx),
1347
+ );
1348
+ }
1349
+
1350
+ return luaCall(mm, [callee, ...args], ctx, sf);
1351
+ }
1352
+ }
1353
+
1354
+ // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1355
+ if (isILuaFunction(callee)) {
1356
+ const base = (sf || LuaStackFrame.lostFrame).withCtx(ctx);
1357
+ const frameForCall = callee instanceof LuaFunction
1358
+ ? base.withFunction(callee)
1359
+ : base;
1360
+ return callee.call(
1361
+ frameForCall,
1362
+ ...args,
1363
+ );
1364
+ }
1365
+
1366
+ throw new LuaRuntimeError(
1367
+ `attempt to call a non-callable value of type: ${luaTypeOf(callee)}`,
1368
+ (sf || LuaStackFrame.lostFrame).withCtx(ctx),
1369
+ );
1370
+ }
1371
+
1372
+ export function luaEquals(a: any, b: any): boolean {
1373
+ const an = isTaggedFloat(a) ? a.value : a;
1374
+ const bn = isTaggedFloat(b) ? b.value : b;
1375
+ return an === bn;
1376
+ }
1377
+
1378
+ export function luaKeys(val: any): any[] {
1379
+ if (val instanceof LuaTable) {
1380
+ return val.keys();
1381
+ }
1382
+ if (Array.isArray(val)) {
1383
+ return val.map((_, i) => i + 1);
1384
+ }
1385
+ return Object.keys(val);
1386
+ }
1387
+
1388
+ export function luaTypeOf(val: any): LuaType | Promise<LuaType> {
1389
+ if (val === null || val === undefined) {
1390
+ return "nil";
1391
+ }
1392
+ if (isPromise(val)) {
1393
+ return (val as Promise<any>).then((v) => luaTypeOf(v));
1394
+ }
1395
+ if (typeof val === "boolean") {
1396
+ return "boolean";
1397
+ }
1398
+ if (typeof val === "number") {
1399
+ return "number";
1400
+ }
1401
+ if (isTaggedFloat(val)) {
1402
+ return "number";
1403
+ }
1404
+ if (typeof val === "string") {
1405
+ return "string";
1406
+ }
1407
+ if (val instanceof LuaTable) {
1408
+ return "table";
1409
+ }
1410
+ if (Array.isArray(val)) {
1411
+ return "table";
1412
+ }
1413
+ if (typeof val === "function" || isILuaFunction(val)) {
1414
+ return "function";
1415
+ }
1416
+ if (typeof val === "object" && (val as any).constructor === Object) {
1417
+ return "table";
1418
+ }
1419
+ return "userdata";
1420
+ }
1421
+
1422
+ export class LuaRuntimeError extends Error {
1423
+ constructor(
1424
+ override readonly message: string,
1425
+ public sf: LuaStackFrame,
1426
+ cause?: Error,
1427
+ ) {
1428
+ super(message, cause);
1429
+ }
1430
+
1431
+ toPrettyString(code: string): string {
1432
+ if (!this.sf || !this.sf.astCtx?.from || !this.sf.astCtx?.to) {
1433
+ return this.toString();
1434
+ }
1435
+ let traceStr = "";
1436
+ let current: LuaStackFrame | undefined = this.sf;
1437
+ while (current) {
1438
+ const ctx = current.astCtx;
1439
+ if (!ctx || !ctx.from || !ctx.to) {
1440
+ break;
1441
+ }
1442
+ // Find the line and column
1443
+ let line = 1;
1444
+ let column = 0;
1445
+ let lastNewline = -1;
1446
+ for (let i = 0; i < ctx.from; i++) {
1447
+ if (code[i] === "\n") {
1448
+ line++;
1449
+ lastNewline = i;
1450
+ column = 0;
1451
+ } else {
1452
+ column++;
1453
+ }
1454
+ }
1455
+
1456
+ // Get the full line of code for context
1457
+ const lineStart = lastNewline + 1;
1458
+ const lineEnd = code.indexOf("\n", ctx.from);
1459
+ const codeLine = code.substring(
1460
+ lineStart,
1461
+ lineEnd === -1 ? undefined : lineEnd,
1462
+ );
1463
+
1464
+ // Add position indicator
1465
+ const pointer = " ".repeat(column) + "^";
1466
+
1467
+ traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
1468
+ ` ${codeLine}\n` +
1469
+ ` ${pointer}\n`;
1470
+ current = current.parent;
1471
+ }
1472
+
1473
+ return `LuaRuntimeError: ${this.message}\nStack trace:\n${traceStr}`;
1474
+ }
1475
+
1476
+ override toString() {
1477
+ return `LuaRuntimeError: ${this.message} at ${this.sf.astCtx?.from}, ${this.sf.astCtx?.to}`;
1478
+ }
1479
+ }
1480
+
1481
+ export function luaTruthy(value: any): boolean {
1482
+ if (value === undefined || value === null || value === false) {
1483
+ return false;
1484
+ }
1485
+
1486
+ if (typeof value === "object" && value instanceof LuaMultiRes) {
1487
+ // for multi-return values, only the first result determines truthiness
1488
+ const first = value.unwrap();
1489
+ return !(first === null || first === undefined || first === false);
1490
+ }
1491
+
1492
+ // all non-`nil`/non-`false` values are truthy (including empty tables)
1493
+ return true;
1494
+ }
1495
+
1496
+ export function luaToString(
1497
+ value: any,
1498
+ visited: Set<any> = new Set(),
1499
+ ): string | Promise<string> {
1500
+ if (value === null || value === undefined) {
1501
+ return "nil";
1502
+ }
1503
+ if (isPromise(value)) {
1504
+ return (value as Promise<any>).then((v) => luaToString(v, visited));
1505
+ }
1506
+
1507
+ if (isTaggedFloat(value)) {
1508
+ return luaFormatNumber(value.value, "float");
1509
+ }
1510
+
1511
+ if (typeof value === "number") {
1512
+ return luaFormatNumber(value);
1513
+ }
1514
+
1515
+ // Check for circular references
1516
+ if (typeof value === "object" && visited.has(value)) {
1517
+ return "<circular reference>";
1518
+ }
1519
+ if ((value as any).toStringAsync) {
1520
+ // Add to visited before recursing
1521
+ visited.add(value);
1522
+ return (value as any).toStringAsync();
1523
+ }
1524
+ if ((value as any).asString) {
1525
+ visited.add(value);
1526
+ return (value as any).asString();
1527
+ }
1528
+ if (value instanceof LuaFunction) {
1529
+ // Don't recurse into the function body, just show the function signature
1530
+ return `<lua-function (${value.body.parameters.join(", ")})>`;
1531
+ }
1532
+ // Handle plain JavaScript objects in a Lua-like format
1533
+ if (typeof value === "object") {
1534
+ // Add to visited before recursing
1535
+ visited.add(value);
1536
+ return (async () => {
1537
+ let result = "{";
1538
+ let first = true;
1539
+
1540
+ // Handle arrays
1541
+ if (Array.isArray(value)) {
1542
+ for (const val of value) {
1543
+ if (first) {
1544
+ first = false;
1545
+ } else {
1546
+ result += ", ";
1547
+ }
1548
+ // Recursively stringify the value, passing the visited set
1549
+ const strVal = await luaToString(val, visited);
1550
+ result += strVal;
1551
+ }
1552
+ return result + "}";
1553
+ }
1554
+
1555
+ // Handle objects
1556
+ for (const [key, val] of Object.entries(value)) {
1557
+ if (first) {
1558
+ first = false;
1559
+ } else {
1560
+ result += ", ";
1561
+ }
1562
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
1563
+ result += `${key} = `;
1564
+ } else {
1565
+ result += `["${key}"] = `;
1566
+ }
1567
+ // Recursively stringify the value, passing the visited set
1568
+ const strVal = await luaToString(val, visited);
1569
+ result += strVal;
1570
+ }
1571
+ result += "}";
1572
+ return result;
1573
+ })();
1574
+ }
1575
+ return String(value);
1576
+ }
1577
+
1578
+ export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1579
+ if (kind !== "float" && Number.isInteger(n) && isFinite(n)) {
1580
+ return String(n);
1581
+ }
1582
+ if (n !== n) return "-nan";
1583
+ if (n === Infinity) return "inf";
1584
+ if (n === -Infinity) return "-inf";
1585
+ if (n === 0) {
1586
+ return (1 / n === -Infinity) ? "-0.0" : "0.0";
1587
+ }
1588
+ // Delegate to luaFormat for `%.14g`
1589
+ const s = luaFormat("%.14g", n);
1590
+ // Guarantee `.01 suffix for integer-valued floats
1591
+ if (s.indexOf(".") === -1 && s.indexOf("e") === -1) {
1592
+ return s + ".0";
1593
+ }
1594
+ return s;
1595
+ }
1596
+
1597
+ export function getMetatable(
1598
+ value: LuaValue,
1599
+ sf?: LuaStackFrame,
1600
+ ): LuaTable | null {
1601
+ if (value === null || value === undefined) {
1602
+ return null;
1603
+ }
1604
+ if (typeof value === "string") {
1605
+ // Prefer per-thread cached string metatable if `_GLOBAL` available
1606
+ const thread = sf?.threadLocal;
1607
+ const globalEnv = thread?.get("_GLOBAL") as LuaEnv | null | undefined;
1608
+
1609
+ if (thread && globalEnv instanceof LuaEnv) {
1610
+ const cached = thread.get("_STRING_MT") as LuaTable | undefined;
1611
+ if (cached instanceof LuaTable) {
1612
+ return cached;
1613
+ }
1614
+
1615
+ const stringMetatable = new LuaTable();
1616
+ stringMetatable.set("__index", (globalEnv as any).get("string"));
1617
+ thread.setLocal("_STRING_MT", stringMetatable);
1618
+
1619
+ return stringMetatable;
1620
+ }
1621
+
1622
+ return null;
1623
+ }
1624
+
1625
+ if ((value as any).metatable) {
1626
+ return (value as any).metatable as LuaTable;
1627
+ }
1628
+ return null;
1629
+ }
1630
+
1631
+ export function jsToLuaValue(value: any): any {
1632
+ if (isPromise(value)) {
1633
+ return (value as Promise<any>).then(jsToLuaValue);
1634
+ }
1635
+ if (value instanceof LuaTable) {
1636
+ return value;
1637
+ }
1638
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
1639
+ return value;
1640
+ }
1641
+ if (Array.isArray(value) && "index" in value && "input" in value) {
1642
+ // This is a RegExpMatchArray
1643
+ const regexMatch = value as RegExpMatchArray;
1644
+ const regexMatchTable = new LuaTable();
1645
+ for (let i = 0; i < regexMatch.length; i++) {
1646
+ regexMatchTable.set(i + 1, regexMatch[i]);
1647
+ }
1648
+ regexMatchTable.set("index", regexMatch.index);
1649
+ regexMatchTable.set("input", regexMatch.input);
1650
+ regexMatchTable.set("groups", regexMatch.groups);
1651
+ return regexMatchTable;
1652
+ }
1653
+ if (Array.isArray(value)) {
1654
+ const table = new LuaTable();
1655
+ for (let i = 0; i < value.length; i++) {
1656
+ table.set(i + 1, jsToLuaValue(value[i]));
1657
+ }
1658
+ return table;
1659
+ }
1660
+ if (typeof value === "object") {
1661
+ const table = new LuaTable();
1662
+ for (const key in value) {
1663
+ table.set(key, jsToLuaValue((value as any)[key]));
1664
+ }
1665
+ return table;
1666
+ }
1667
+ if (typeof value === "function") {
1668
+ return new LuaNativeJSFunction(value);
1669
+ }
1670
+ return value;
1671
+ }
1672
+
1673
+ // Inverse of jsToLuaValue
1674
+ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1675
+ if (isPromise(value)) {
1676
+ return (value as Promise<any>).then((v) => luaValueToJS(v, sf));
1677
+ }
1678
+ if (value instanceof LuaTable) {
1679
+ return value.toJS(sf);
1680
+ }
1681
+ if (
1682
+ value instanceof LuaNativeJSFunction || value instanceof LuaFunction ||
1683
+ value instanceof LuaBuiltinFunction
1684
+ ) {
1685
+ return (...args: any[]) => {
1686
+ const jsArgs = rpAll(
1687
+ args.map((v) => luaValueToJS(v, sf)),
1688
+ );
1689
+ if (isPromise(jsArgs)) {
1690
+ return luaValueToJS(
1691
+ jsArgs.then((jsArgs) => (value as ILuaFunction).call(sf, ...jsArgs)),
1692
+ sf,
1693
+ );
1694
+ }
1695
+ return luaValueToJS((value as ILuaFunction).call(sf, ...jsArgs), sf);
1696
+ };
1697
+ }
1698
+ if (isTaggedFloat(value)) {
1699
+ return value.value;
1700
+ }
1701
+ return value;
1702
+ }