@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.
- package/LICENSE.md +18 -0
- package/README.md +98 -0
- package/client/asset_bundle/bundle.ts +95 -0
- package/client/data/datastore.ts +85 -0
- package/client/data/kv_primitives.ts +25 -0
- package/client/markdown_parser/constants.ts +13 -0
- package/client/plugos/event.ts +36 -0
- package/client/plugos/eventhook.ts +8 -0
- package/client/plugos/hooks/code_widget.ts +59 -0
- package/client/plugos/hooks/command.ts +104 -0
- package/client/plugos/hooks/document_editor.ts +77 -0
- package/client/plugos/hooks/event.ts +187 -0
- package/client/plugos/hooks/mq.ts +154 -0
- package/client/plugos/hooks/plug_namespace.ts +85 -0
- package/client/plugos/hooks/slash_command.ts +192 -0
- package/client/plugos/hooks/syscall.ts +66 -0
- package/client/plugos/manifest_cache.ts +67 -0
- package/client/plugos/plug.ts +99 -0
- package/client/plugos/plug_compile.ts +202 -0
- package/client/plugos/protocol.ts +40 -0
- package/client/plugos/proxy_fetch.ts +53 -0
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
- package/client/plugos/sandboxes/sandbox.ts +14 -0
- package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
- package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
- package/client/plugos/syscalls/asset.ts +35 -0
- package/client/plugos/syscalls/clientStore.ts +21 -0
- package/client/plugos/syscalls/client_code_widget.ts +12 -0
- package/client/plugos/syscalls/code_widget.ts +24 -0
- package/client/plugos/syscalls/config.ts +46 -0
- package/client/plugos/syscalls/datastore.ts +89 -0
- package/client/plugos/syscalls/editor.ts +673 -0
- package/client/plugos/syscalls/event.ts +36 -0
- package/client/plugos/syscalls/fetch.ts +128 -0
- package/client/plugos/syscalls/index.ts +102 -0
- package/client/plugos/syscalls/jsonschema.ts +69 -0
- package/client/plugos/syscalls/language.ts +23 -0
- package/client/plugos/syscalls/lua.ts +58 -0
- package/client/plugos/syscalls/markdown.ts +84 -0
- package/client/plugos/syscalls/mq.ts +52 -0
- package/client/plugos/syscalls/service_registry.ts +43 -0
- package/client/plugos/syscalls/shell.ts +39 -0
- package/client/plugos/syscalls/space.ts +139 -0
- package/client/plugos/syscalls/sync.ts +77 -0
- package/client/plugos/syscalls/system.ts +150 -0
- package/client/plugos/system.ts +201 -0
- package/client/plugos/types.ts +60 -0
- package/client/plugos/util.ts +14 -0
- package/client/plugos/worker_runtime.ts +195 -0
- package/client/space_lua/ast.ts +328 -0
- package/client/space_lua/ast_narrow.ts +81 -0
- package/client/space_lua/eval.ts +2478 -0
- package/client/space_lua/labels.ts +416 -0
- package/client/space_lua/numeric.ts +240 -0
- package/client/space_lua/parse.ts +1522 -0
- package/client/space_lua/query_collection.ts +232 -0
- package/client/space_lua/rp.ts +27 -0
- package/client/space_lua/runtime.ts +1702 -0
- package/client/space_lua/stdlib/crypto.ts +10 -0
- package/client/space_lua/stdlib/encoding.ts +19 -0
- package/client/space_lua/stdlib/format.ts +770 -0
- package/client/space_lua/stdlib/js.ts +73 -0
- package/client/space_lua/stdlib/load.ts +52 -0
- package/client/space_lua/stdlib/math.ts +193 -0
- package/client/space_lua/stdlib/net.ts +113 -0
- package/client/space_lua/stdlib/os.ts +368 -0
- package/client/space_lua/stdlib/space_lua.ts +153 -0
- package/client/space_lua/stdlib/string.ts +286 -0
- package/client/space_lua/stdlib/table.ts +401 -0
- package/client/space_lua/stdlib.ts +489 -0
- package/client/space_lua/tonumber.ts +501 -0
- package/client/space_lua/util.ts +96 -0
- package/dist/plug-compile.js +1513 -0
- package/package.json +120 -0
- package/plug-api/constants.ts +42 -0
- package/plug-api/lib/async.ts +162 -0
- package/plug-api/lib/crypto.ts +202 -0
- package/plug-api/lib/dates.ts +13 -0
- package/plug-api/lib/json.ts +136 -0
- package/plug-api/lib/limited_map.ts +72 -0
- package/plug-api/lib/memory_cache.ts +21 -0
- package/plug-api/lib/native_fetch.ts +6 -0
- package/plug-api/lib/ref.ts +275 -0
- package/plug-api/lib/resolve.ts +90 -0
- package/plug-api/lib/tags.ts +15 -0
- package/plug-api/lib/transclusion.ts +122 -0
- package/plug-api/lib/tree.ts +232 -0
- package/plug-api/lib/yaml.ts +284 -0
- package/plug-api/syscall.ts +15 -0
- package/plug-api/syscalls/asset.ts +36 -0
- package/plug-api/syscalls/client_store.ts +33 -0
- package/plug-api/syscalls/code_widget.ts +8 -0
- package/plug-api/syscalls/config.ts +58 -0
- package/plug-api/syscalls/datastore.ts +96 -0
- package/plug-api/syscalls/editor.ts +517 -0
- package/plug-api/syscalls/event.ts +47 -0
- package/plug-api/syscalls/index.ts +77 -0
- package/plug-api/syscalls/jsonschema.ts +25 -0
- package/plug-api/syscalls/language.ts +23 -0
- package/plug-api/syscalls/lua.ts +20 -0
- package/plug-api/syscalls/markdown.ts +38 -0
- package/plug-api/syscalls/mq.ts +79 -0
- package/plug-api/syscalls/shell.ts +14 -0
- package/plug-api/syscalls/space.ts +212 -0
- package/plug-api/syscalls/sync.ts +28 -0
- package/plug-api/syscalls/system.ts +102 -0
- package/plug-api/syscalls/yaml.ts +28 -0
- package/plug-api/syscalls.ts +21 -0
- package/plug-api/system_mock.ts +89 -0
- package/plug-api/types/client.ts +116 -0
- package/plug-api/types/config.ts +22 -0
- package/plug-api/types/datastore.ts +28 -0
- package/plug-api/types/event.ts +27 -0
- package/plug-api/types/index.ts +56 -0
- package/plug-api/types/manifest.ts +98 -0
- package/plug-api/types/namespace.ts +6 -0
- 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
|
+
}
|