@loom-dev/luau 0.0.0-stage → 0.14.0

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.
@@ -0,0 +1,1444 @@
1
+ import { CFrame, Color3, ColorSequence, ColorSequenceKeypoint, Content, DateTime, Enum, EnumItem, Font, LoomSignal, NumberRange, NumberSequence, NumberSequenceKeypoint, Random, Rect, RobloxEnum, TweenInfo, UDim, UDim2, Vector2, Vector3, createInstance, enumTypeName, game, heartbeat, isLoomInstance, isLuaTuple, robloxEquals } from "@loom-dev/runtime";
2
+ import createLuauModule from "../wasm/luau-vm.mjs";
3
+ import { initLayout } from "@loom-dev/layout";
4
+ import { createWorld } from "@loom-dev/react";
5
+ //#region src/prelude.ts
6
+ /**
7
+ * `prelude.ts` — the Luau half of the globals.
8
+ *
9
+ * Runs once on the main state before the globals freeze, with a table of host
10
+ * primitives as its argument (see `install` in `bindings.ts`). What lives here
11
+ * is what is simplest to say in Luau itself: the `task` library on top of
12
+ * `coroutine` and the host's resume points, and the `print`/`warn` that
13
+ * `tostring` their arguments the way the engine does.
14
+ */
15
+ const PRELUDE = `
16
+ local host = ...
17
+
18
+ local function thread(f)
19
+ return if type(f) == "thread" then f else coroutine.create(f)
20
+ end
21
+
22
+ local task = {}
23
+
24
+ function task.spawn(f, ...)
25
+ local co = thread(f)
26
+ local ok, err = coroutine.resume(co, ...)
27
+ if not ok then
28
+ host.report(tostring(err), debug.traceback(co))
29
+ end
30
+ return co
31
+ end
32
+
33
+ function task.defer(f, ...)
34
+ local co = thread(f)
35
+ host.defer(co, ...)
36
+ return co
37
+ end
38
+
39
+ function task.delay(seconds, f, ...)
40
+ local co = thread(f)
41
+ host.delay(seconds or 0, co, ...)
42
+ return co
43
+ end
44
+
45
+ task.wait = host.wait
46
+
47
+ function task.cancel(co)
48
+ coroutine.close(co)
49
+ end
50
+
51
+ function task.synchronize() end
52
+ function task.desynchronize() end
53
+
54
+ _G.task = task
55
+
56
+ -- The engine's additions to \`debug\`: the MicroProfiler labels and the memory
57
+ -- categories. loom has neither profiler, so they record nothing. Ripple wraps
58
+ -- every Heartbeat step in a profile label.
59
+ function debug.profilebegin(_label) end
60
+ function debug.profileend() end
61
+ function debug.setmemorycategory(_tag) return "" end
62
+ function debug.resetmemorycategory() end
63
+ function debug.getmemorycategory() return "" end
64
+ wait = task.wait
65
+ spawn = task.defer
66
+ delay = task.delay
67
+ tick = host.tick
68
+ time = host.time
69
+ elapsedTime = host.time
70
+
71
+ local function join(...)
72
+ local n = select("#", ...)
73
+ local parts = table.create(n)
74
+ for i = 1, n do
75
+ parts[i] = tostring((select(i, ...)))
76
+ end
77
+ return table.concat(parts, " ")
78
+ end
79
+
80
+ function print(...)
81
+ host.print(join(...))
82
+ end
83
+
84
+ function warn(...)
85
+ host.warn(join(...))
86
+ end
87
+
88
+ function require(module)
89
+ -- A string path resolves from the requiring script: the caller's own
90
+ -- environment holds it as \`script\`.
91
+ local from = if type(module) == "string" then getfenv(2).script else nil
92
+ local ok, value = host.require(module, from)
93
+ if not ok then
94
+ error(value, 2)
95
+ end
96
+ return value
97
+ end
98
+ `;
99
+ //#endregion
100
+ //#region src/project.ts
101
+ /**
102
+ * `project.ts` — a Rojo project as live instances, and the client's startup.
103
+ *
104
+ * The preview plugin reads a project from disk (`default.project.json`, or the
105
+ * Rojo template's `src/{client,server,shared}` when there is none) and hands it
106
+ * over as a `LuauProjectNode` tree with every script's source inlined. Here it
107
+ * becomes real instances under the runtime's `game`: Folders, ModuleScripts,
108
+ * LocalScripts and Scripts in their services, with each script's source kept
109
+ * beside it — Roblox does not let a LocalScript read `Source`, and neither
110
+ * does loom.
111
+ *
112
+ * `startClient` then does what a joining client does: run the LocalScripts in
113
+ * ReplicatedFirst, copy StarterGui into PlayerGui and StarterPlayerScripts into
114
+ * PlayerScripts, and run the LocalScripts that land there. Server Scripts are
115
+ * instantiated (so `require` and `FindFirstChild` see them) but never run — a
116
+ * preview is one client.
117
+ */
118
+ const SOURCES = /* @__PURE__ */ new WeakMap();
119
+ /** Attach `source` to a script instance (what `require` and `startClient` run). */
120
+ function setScriptSource(script, source) {
121
+ SOURCES.set(script, source);
122
+ }
123
+ /** The source behind a script instance, if loom has one for it. */
124
+ function getScriptSource(script) {
125
+ return SOURCES.get(script);
126
+ }
127
+ /**
128
+ * `Clone`, keeping every script's source. The runtime copies instances but
129
+ * knows nothing about loom's source registry, so the two trees are walked side
130
+ * by side — `Clone` keeps child order and drops the non-Archivable ones.
131
+ */
132
+ function cloneWithSources(original) {
133
+ const copy = original.Clone();
134
+ if (copy !== void 0) copySources(original, copy);
135
+ return copy;
136
+ }
137
+ function copySources(from, to) {
138
+ const source = SOURCES.get(from);
139
+ if (source !== void 0) SOURCES.set(to, source);
140
+ const kept = from.GetChildren().filter((child) => child.Archivable !== false);
141
+ const copies = to.GetChildren();
142
+ kept.forEach((child, i) => {
143
+ const copy = copies[i];
144
+ if (copy !== void 0) copySources(child, copy);
145
+ });
146
+ }
147
+ const instantiated = /* @__PURE__ */ new WeakSet();
148
+ /**
149
+ * Build `project` into the runtime's `game`, once per project object. A node
150
+ * whose name and class match an instance that already exists (a service,
151
+ * `StarterPlayerScripts`) is merged into it rather than duplicated.
152
+ */
153
+ function instantiateProject(project) {
154
+ if (instantiated.has(project)) return;
155
+ instantiated.add(project);
156
+ for (const child of project.tree.children ?? []) populate(game.GetService(child.name), child);
157
+ }
158
+ function populate(parent, node) {
159
+ for (const child of node.children ?? []) {
160
+ const existing = parent.FindFirstChild(child.name);
161
+ const instance = existing?.ClassName === child.className ? existing : createInstance(child.className, child.name);
162
+ if (child.source !== void 0) setScriptSource(instance, child.source);
163
+ populate(instance, child);
164
+ instance.Parent = parent;
165
+ }
166
+ }
167
+ /**
168
+ * The client's startup, in Roblox order: ReplicatedFirst's LocalScripts, then
169
+ * StarterGui copied into PlayerGui, then StarterPlayerScripts copied into
170
+ * PlayerScripts. Returns every LocalScript that should run, in that order.
171
+ */
172
+ function startClient() {
173
+ const player = game.GetService("Players").LocalPlayer;
174
+ const playerGui = player.WaitForChild("PlayerGui");
175
+ let playerScripts = player.FindFirstChildOfClass("PlayerScripts");
176
+ if (playerScripts === void 0) {
177
+ playerScripts = createInstance("PlayerScripts", "PlayerScripts");
178
+ playerScripts.Parent = player;
179
+ }
180
+ const scripts = [];
181
+ collect(game.GetService("ReplicatedFirst"), scripts);
182
+ for (const child of game.GetService("StarterGui").GetChildren()) {
183
+ const copy = cloneWithSources(child);
184
+ if (copy === void 0) continue;
185
+ copy.Parent = playerGui;
186
+ collect(copy, scripts);
187
+ }
188
+ const starterScripts = game.GetService("StarterPlayer").FindFirstChild("StarterPlayerScripts");
189
+ for (const child of starterScripts?.GetChildren() ?? []) {
190
+ const copy = cloneWithSources(child);
191
+ if (copy === void 0) continue;
192
+ copy.Parent = playerScripts;
193
+ collect(copy, scripts);
194
+ }
195
+ return scripts;
196
+ }
197
+ /** `root` and its descendants' runnable LocalScripts, in tree order. */
198
+ function collect(root, into) {
199
+ for (const instance of [root, ...root.GetDescendants()]) {
200
+ if (instance.ClassName !== "LocalScript" || instance.Disabled === true) continue;
201
+ const source = SOURCES.get(instance);
202
+ if (source !== void 0) into.push({
203
+ script: instance,
204
+ source
205
+ });
206
+ }
207
+ }
208
+ //#endregion
209
+ //#region src/vm.ts
210
+ /**
211
+ * `vm.ts` — `LuauVM`, the Luau virtual machine (upstream Luau compiled to
212
+ * WASM, see `native/`) with a JavaScript-shaped API on top of its stack.
213
+ *
214
+ * The VM knows nothing about Roblox. It moves values across the boundary and
215
+ * runs threads; `bindings.ts`-style layers decide what Luau sees:
216
+ *
217
+ * - Luau → JS: nil is `undefined`, booleans/numbers/strings are themselves, a
218
+ * host userdata is the JS object it was made from, and tables, functions and
219
+ * threads arrive as `LuauTable`/`LuauFunction`/`LuauThread` references.
220
+ * - JS → Luau: a JS function becomes a host function; an object the
221
+ * classifier recognises becomes host userdata dispatched through its
222
+ * `HostClass` (the same object is always the same userdata, so `==` and
223
+ * table keys behave); plain arrays and objects are copied into new tables.
224
+ *
225
+ * Yielding is Roblox's model: a host function that returns a `Suspend` yields
226
+ * the calling thread, and the thread runs again when the `Suspend` resumes it —
227
+ * whoever resumed the thread in the first place.
228
+ */
229
+ const KIND_NIL = 0;
230
+ const KIND_BOOLEAN = 1;
231
+ const KIND_NUMBER = 2;
232
+ const KIND_STRING = 3;
233
+ const KIND_TABLE = 4;
234
+ const KIND_FUNCTION = 5;
235
+ const KIND_HOST = 6;
236
+ const KIND_THREAD = 7;
237
+ const LUA_OK = 0;
238
+ const LUA_YIELD = 1;
239
+ /** `LUA_GLOBALSINDEX`: `-LUAI_MAXCSTACK - 2002`, with Luau's default 8000. */
240
+ const LUA_GLOBALSINDEX = -10002;
241
+ const LUA_COSUS = 1;
242
+ const LB_ERROR = -1;
243
+ const LB_YIELD = -2;
244
+ /** An error raised in Luau, or by a host function on Luau's behalf. */
245
+ var LuauError = class extends Error {
246
+ value;
247
+ traceback;
248
+ name = "LuauError";
249
+ constructor(message, value = message, traceback) {
250
+ super(message);
251
+ this.value = value;
252
+ this.traceback = traceback;
253
+ }
254
+ };
255
+ /** Several return values from a host function. */
256
+ var Tuple = class {
257
+ values;
258
+ constructor(values) {
259
+ this.values = values;
260
+ }
261
+ };
262
+ /** Return several values from a host function. */
263
+ function tuple(...values) {
264
+ return new Tuple(values);
265
+ }
266
+ /**
267
+ * Returned by a host function to yield the thread that called it. `wait` runs
268
+ * once the thread has actually suspended and receives the function that
269
+ * resumes it with the host function's results.
270
+ */
271
+ var Suspend = class {
272
+ wait;
273
+ constructor(wait) {
274
+ this.wait = wait;
275
+ }
276
+ };
277
+ const METAMETHODS = [
278
+ "__index",
279
+ "__newindex",
280
+ "__namecall",
281
+ "__call",
282
+ "__tostring",
283
+ "__eq",
284
+ "__lt",
285
+ "__le",
286
+ "__len",
287
+ "__unm",
288
+ "__add",
289
+ "__sub",
290
+ "__mul",
291
+ "__div",
292
+ "__idiv",
293
+ "__mod",
294
+ "__pow"
295
+ ];
296
+ const encoder = new TextEncoder();
297
+ const decoder = new TextDecoder();
298
+ /** A reference that keeps a Luau value alive until the JS side is collected. */
299
+ var LuauRef = class {
300
+ vm;
301
+ ref;
302
+ constructor(vm, ref) {
303
+ this.vm = vm;
304
+ this.ref = ref;
305
+ vm.track(this, ref);
306
+ }
307
+ };
308
+ /** A Luau function. `call` runs it to completion; `spawn` gives it a thread. */
309
+ var LuauFunction = class extends LuauRef {
310
+ call(...args) {
311
+ return this.vm.call(this, args);
312
+ }
313
+ spawn(...args) {
314
+ return this.vm.spawn(this, args);
315
+ }
316
+ };
317
+ /** A Luau table, read and written raw (no metamethods). */
318
+ var LuauTable = class extends LuauRef {
319
+ get(key) {
320
+ return this.vm.tableGet(this, key);
321
+ }
322
+ set(key, value) {
323
+ this.vm.tableSet(this, key, value);
324
+ }
325
+ /** `#t`. */
326
+ get length() {
327
+ return this.vm.tableLength(this);
328
+ }
329
+ /** Every key/value pair, in `next` order. */
330
+ entries() {
331
+ return this.vm.tableEntries(this);
332
+ }
333
+ /** `t[1]` … `t[#t]`. */
334
+ array() {
335
+ const out = [];
336
+ const n = this.length;
337
+ for (let i = 1; i <= n; i++) out.push(this.get(i));
338
+ return out;
339
+ }
340
+ };
341
+ /** A Luau coroutine. */
342
+ var LuauThread = class extends LuauRef {};
343
+ /** A Luau value JS has no use for beyond handing it back (vector, buffer, …). */
344
+ var LuauOpaque = class extends LuauRef {
345
+ typeName;
346
+ constructor(vm, ref, typeName) {
347
+ super(vm, ref);
348
+ this.typeName = typeName;
349
+ }
350
+ };
351
+ var LuauVM = class LuauVM {
352
+ /** Instantiate the WASM module and open a fresh Luau state. */
353
+ static async create(options = {}) {
354
+ let vm;
355
+ const module = await createLuauModule({
356
+ lbHostCall: (L, id) => vm.hostCall(L, id),
357
+ lbHostRelease: (handle) => vm?.hostRelease(handle),
358
+ ...options.locateFile ? { locateFile: options.locateFile } : {},
359
+ ...options.wasmBinary ? { wasmBinary: options.wasmBinary } : {}
360
+ });
361
+ vm = new LuauVM(module, options);
362
+ return vm;
363
+ }
364
+ M;
365
+ L;
366
+ classify;
367
+ onError;
368
+ budgetMs;
369
+ sharedTables;
370
+ entries = [];
371
+ functionIds = /* @__PURE__ */ new Map();
372
+ debugNames = /* @__PURE__ */ new Map();
373
+ handles = /* @__PURE__ */ new Map();
374
+ handleOf = /* @__PURE__ */ new Map();
375
+ nextHandle = 1;
376
+ metatables = /* @__PURE__ */ new Map();
377
+ metamethodIds = /* @__PURE__ */ new Map();
378
+ refs = new FinalizationRegistry((ref) => {
379
+ if (!this.closed) this.M._lb_unref(this.L, ref);
380
+ });
381
+ scratch = 0;
382
+ scratchSize = 0;
383
+ lenPtr;
384
+ depth = 0;
385
+ frozen = false;
386
+ closed = false;
387
+ /** `run`'s `onFinish` for each thread still on its way to the end. */
388
+ finishers = /* @__PURE__ */ new Map();
389
+ constructor(module, options) {
390
+ this.M = module;
391
+ this.L = module._lb_open();
392
+ this.classify = options.classify ?? (() => void 0);
393
+ this.onError = options.onError ?? ((error) => console.error(error.message));
394
+ this.budgetMs = options.budgetMs ?? 0;
395
+ this.sharedTables = options.sharedTables ?? [];
396
+ this.lenPtr = module._malloc(4);
397
+ for (const name of METAMETHODS) this.metamethodIds.set(name, this.register(this.metamethod(name)));
398
+ }
399
+ /** Define a global. Only before the first script runs (globals freeze then). */
400
+ setGlobal(name, value) {
401
+ if (this.frozen) throw new Error(`loom luau: globals are frozen; cannot set ${name}`);
402
+ this.push(this.L, value);
403
+ this.M._lb_setglobal(this.L, this.cstr(name));
404
+ }
405
+ getGlobal(name) {
406
+ const base = this.M._lb_gettop(this.L);
407
+ this.M._lb_getglobal(this.L, this.cstr(name));
408
+ const value = this.read(this.L, -1);
409
+ this.M._lb_settop(this.L, base);
410
+ return value;
411
+ }
412
+ /**
413
+ * Freeze the globals and the standard libraries, Roblox style. Runs on its
414
+ * own before the first script; scripts then see the frozen set through
415
+ * their own writable environment.
416
+ */
417
+ freeze() {
418
+ if (this.frozen) return;
419
+ this.frozen = true;
420
+ this.M._lb_sandbox(this.L);
421
+ if (this.sharedTables.length === 0) return;
422
+ this.M._lb_setreadonly(this.L, LUA_GLOBALSINDEX, 0);
423
+ for (const name of this.sharedTables) {
424
+ this.M._lb_newtable(this.L, 0, 0);
425
+ this.M._lb_setglobal(this.L, this.cstr(name));
426
+ }
427
+ this.M._lb_setreadonly(this.L, LUA_GLOBALSINDEX, 1);
428
+ }
429
+ /** Compile `source` into a function, or throw the syntax error. */
430
+ load(source, chunkname = "=loom") {
431
+ const base = this.M._lb_gettop(this.L);
432
+ const status = this.loadOnto(this.L, source, chunkname);
433
+ const value = this.read(this.L, -1);
434
+ this.M._lb_settop(this.L, base);
435
+ if (status !== LUA_OK) throw new LuauError(String(value));
436
+ return value;
437
+ }
438
+ /**
439
+ * Run `source` as a script: its own thread with its own global environment
440
+ * (the frozen globals plus `globals`), the way Roblox runs a Script or
441
+ * LocalScript. Returns how the thread first came back; `onFinish` hears how
442
+ * it ended, after however many yields (`require` of a module that yields).
443
+ */
444
+ run(source, options = {}) {
445
+ this.freeze();
446
+ const co = this.M._lb_newthread(this.L);
447
+ const ref = this.M._lb_ref(this.L, -1);
448
+ this.M._lb_settop(this.L, -2);
449
+ try {
450
+ this.M._lb_sandboxthread(co);
451
+ for (const [name, value] of Object.entries(options.globals ?? {})) {
452
+ this.push(co, value);
453
+ this.M._lb_setglobal(co, this.cstr(name));
454
+ }
455
+ if (this.loadOnto(co, source, options.chunkname ?? "=loom") !== LUA_OK) {
456
+ const error = new LuauError(String(this.read(co, -1)));
457
+ this.M._lb_settop(co, 0);
458
+ this.onError(error);
459
+ const result = {
460
+ status: "error",
461
+ error
462
+ };
463
+ options.onFinish?.(result);
464
+ return result;
465
+ }
466
+ if (options.onFinish) this.finishers.set(co, options.onFinish);
467
+ return this.resume(co, 0);
468
+ } finally {
469
+ this.M._lb_unref(this.L, ref);
470
+ }
471
+ }
472
+ /**
473
+ * Call a Luau function to completion and return its results. A function
474
+ * that yields throws here — give it a thread with `spawn` instead.
475
+ */
476
+ call(fn, args = []) {
477
+ const L = this.L;
478
+ const base = this.M._lb_gettop(L);
479
+ this.ensureStack(L, args.length + 1);
480
+ this.M._lb_getref(L, fn.ref);
481
+ for (const arg of args) this.push(L, arg);
482
+ if (this.enter(() => this.M._lb_pcall(L, args.length, -1)) !== LUA_OK) {
483
+ const message = this.read(L, -1);
484
+ this.M._lb_settop(L, base);
485
+ throw new LuauError(String(message), message);
486
+ }
487
+ const results = this.readRange(L, base + 1, this.M._lb_gettop(L));
488
+ this.M._lb_settop(L, base);
489
+ return results;
490
+ }
491
+ /** Run a Luau function in a new thread (`task.spawn`). */
492
+ spawn(fn, args = []) {
493
+ const co = this.M._lb_newthread(this.L);
494
+ const ref = this.M._lb_ref(this.L, -1);
495
+ this.M._lb_settop(this.L, -2);
496
+ try {
497
+ this.ensureStack(co, args.length + 1);
498
+ this.M._lb_getref(co, fn.ref);
499
+ for (const arg of args) this.push(co, arg);
500
+ return this.resume(co, args.length);
501
+ } finally {
502
+ this.M._lb_unref(this.L, ref);
503
+ }
504
+ }
505
+ /**
506
+ * Resume a Luau thread (`task.defer`'s and `task.delay`'s, made with
507
+ * `coroutine.create`). A thread that cannot be resumed — finished, running,
508
+ * or closed by `task.cancel` — is left alone and gives `undefined`.
509
+ */
510
+ resumeThread(thread, args = []) {
511
+ const co = this.threadOf(thread);
512
+ if (this.M._lb_costatus(this.L, co) !== LUA_COSUS) return void 0;
513
+ this.ensureStack(co, args.length);
514
+ for (const arg of args) this.push(co, arg);
515
+ return this.resume(co, args.length);
516
+ }
517
+ /** Hand an error to `onError`, as if it had escaped a thread. */
518
+ report(error) {
519
+ this.onError(error);
520
+ }
521
+ /** Collect garbage now (tests use it to observe userdata release). */
522
+ collect() {
523
+ this.M._lb_gc(this.L);
524
+ }
525
+ /** Close the state. Every reference into it is dead afterwards. */
526
+ close() {
527
+ if (this.closed) return;
528
+ this.M._lb_close(this.L);
529
+ this.closed = true;
530
+ }
531
+ /** Whether `close` has run: every entry into Luau throws afterwards. */
532
+ get isClosed() {
533
+ return this.closed;
534
+ }
535
+ /** How many JS objects Luau currently holds as userdata. */
536
+ get liveHostObjects() {
537
+ return this.handles.size;
538
+ }
539
+ /** @internal */
540
+ track(owner, ref) {
541
+ this.refs.register(owner, ref);
542
+ }
543
+ /** @internal */
544
+ tableGet(table, key) {
545
+ const L = this.L;
546
+ const base = this.M._lb_gettop(L);
547
+ this.M._lb_getref(L, table.ref);
548
+ this.push(L, key);
549
+ this.M._lb_rawget(L, -2);
550
+ const value = this.read(L, -1);
551
+ this.M._lb_settop(L, base);
552
+ return value;
553
+ }
554
+ /** @internal */
555
+ tableSet(table, key, value) {
556
+ const L = this.L;
557
+ const base = this.M._lb_gettop(L);
558
+ this.M._lb_getref(L, table.ref);
559
+ this.push(L, key);
560
+ this.push(L, value);
561
+ this.M._lb_rawset(L, -3);
562
+ this.M._lb_settop(L, base);
563
+ }
564
+ /** @internal */
565
+ tableLength(table) {
566
+ const L = this.L;
567
+ this.M._lb_getref(L, table.ref);
568
+ const n = this.M._lb_objlen(L, -1);
569
+ this.M._lb_settop(L, -2);
570
+ return n;
571
+ }
572
+ /** @internal */
573
+ tableEntries(table) {
574
+ const L = this.L;
575
+ const base = this.M._lb_gettop(L);
576
+ this.M._lb_getref(L, table.ref);
577
+ this.M._lb_pushnil(L);
578
+ const out = [];
579
+ while (this.M._lb_next(L, base + 1) !== 0) {
580
+ out.push([this.read(L, -2), this.read(L, -1)]);
581
+ this.M._lb_settop(L, -2);
582
+ }
583
+ this.M._lb_settop(L, base);
584
+ return out;
585
+ }
586
+ /** Resume `co` with the top `nargs` values of its stack. */
587
+ resume(co, nargs) {
588
+ const result = this.resumeOnce(co, nargs);
589
+ if (result.status !== "yielded") {
590
+ const finish = this.finishers.get(co);
591
+ if (finish) {
592
+ this.finishers.delete(co);
593
+ finish(result);
594
+ }
595
+ }
596
+ return result;
597
+ }
598
+ resumeOnce(co, nargs) {
599
+ const status = this.enter(() => this.M._lb_resume(co, 0, nargs));
600
+ if (status === LUA_OK) {
601
+ const values = this.readRange(co, 1, this.M._lb_gettop(co));
602
+ this.M._lb_settop(co, 0);
603
+ return {
604
+ status: "done",
605
+ values
606
+ };
607
+ }
608
+ if (status === LUA_YIELD) {
609
+ this.M._lb_settop(co, 0);
610
+ return { status: "yielded" };
611
+ }
612
+ const value = this.read(co, -1);
613
+ const traceback = this.M.UTF8ToString(this.M._lb_debugtrace(co));
614
+ this.M._lb_settop(co, 0);
615
+ const error = new LuauError(String(value), value, traceback);
616
+ this.onError(error);
617
+ return {
618
+ status: "error",
619
+ error
620
+ };
621
+ }
622
+ /**
623
+ * A host function returned `suspend` on thread `co`. Keep the thread alive
624
+ * and hand `wait` its resume function once Luau has unwound — only then is
625
+ * the thread really suspended, whoever resumed it.
626
+ */
627
+ /** The `lua_State*` behind a thread reference. */
628
+ threadOf(thread) {
629
+ if (thread.vm !== this) throw new TypeError("loom luau: a thread from another VM");
630
+ this.M._lb_getref(this.L, thread.ref);
631
+ const co = this.M._lb_tothread(this.L, -1);
632
+ this.M._lb_settop(this.L, -2);
633
+ return co;
634
+ }
635
+ suspend(co, suspend) {
636
+ const ref = this.M._lb_refthread(co);
637
+ let resumed = false;
638
+ queueMicrotask(() => {
639
+ suspend.wait((...values) => {
640
+ if (resumed || this.closed) return;
641
+ resumed = true;
642
+ try {
643
+ if (this.M._lb_costatus(this.L, co) !== LUA_COSUS) return;
644
+ this.ensureStack(co, values.length);
645
+ for (const value of values) this.push(co, value);
646
+ this.resume(co, values.length);
647
+ } finally {
648
+ this.M._lb_unref(this.L, ref);
649
+ }
650
+ });
651
+ });
652
+ }
653
+ /** Run `fn` as one entry into Luau, under the run-time budget. */
654
+ enter(fn) {
655
+ if (this.closed) throw new Error("loom luau: the VM is closed");
656
+ if (this.depth === 0 && this.budgetMs > 0) this.M._lb_budget(this.budgetMs);
657
+ this.depth++;
658
+ try {
659
+ return fn();
660
+ } finally {
661
+ this.depth--;
662
+ if (this.depth === 0) this.M._lb_budget(0);
663
+ }
664
+ }
665
+ register(entry) {
666
+ this.entries.push(entry);
667
+ return this.entries.length - 1;
668
+ }
669
+ hostCall(L, id) {
670
+ const entry = this.entries[id];
671
+ const args = this.readRange(L, 1, this.M._lb_gettop(L));
672
+ try {
673
+ const result = entry(L, args);
674
+ if (result instanceof Suspend) {
675
+ this.suspend(L, result);
676
+ return LB_YIELD;
677
+ }
678
+ return this.pushResults(L, result);
679
+ } catch (error) {
680
+ this.push(L, errorValue(error));
681
+ return LB_ERROR;
682
+ }
683
+ }
684
+ pushResults(L, result) {
685
+ if (result === void 0) return 0;
686
+ if (result instanceof Tuple) {
687
+ this.ensureStack(L, result.values.length);
688
+ for (const value of result.values) this.push(L, value);
689
+ return result.values.length;
690
+ }
691
+ this.push(L, result);
692
+ return 1;
693
+ }
694
+ hostRelease(handle) {
695
+ const entry = this.handles.get(handle);
696
+ if (entry === void 0) return;
697
+ entry.count--;
698
+ if (entry.count > 0) return;
699
+ this.handles.delete(handle);
700
+ this.handleOf.delete(entry.value);
701
+ }
702
+ /** The host function behind metamethod `name`, shared by every class. */
703
+ metamethod(name) {
704
+ const self = (args) => {
705
+ const value = args[0];
706
+ return [value, this.classOf(value)];
707
+ };
708
+ const binary = (op) => (_, args) => {
709
+ const cls = this.classOf(args[0]) ?? this.classOf(args[1]);
710
+ const hook = cls?.[op];
711
+ if (hook === void 0) throw new LuauError(`attempt to perform arithmetic (${op}) on ${cls?.name ?? "userdata"}`);
712
+ return hook.call(cls, args[0], args[1]);
713
+ };
714
+ switch (name) {
715
+ case "__index": return (_, args) => {
716
+ const [value, cls] = self(args);
717
+ if (cls.index === void 0) throw new LuauError(`${String(args[1])} is not a valid member of ${cls.name}`);
718
+ return cls.index(value, args[1]);
719
+ };
720
+ case "__newindex": return (_, args) => {
721
+ const [value, cls] = self(args);
722
+ if (cls.newindex === void 0) throw new LuauError(`${String(args[1])} cannot be assigned to`);
723
+ cls.newindex(value, args[1], args[2]);
724
+ };
725
+ case "__namecall": return (L, args) => {
726
+ const method = this.M.UTF8ToString(this.M._lb_namecall(L));
727
+ const [value, cls] = self(args);
728
+ if (cls.namecall === void 0) throw new LuauError(`${method} is not a valid member of ${cls.name}`);
729
+ return cls.namecall(value, method, args.slice(1));
730
+ };
731
+ case "__call": return (_, args) => {
732
+ const [value, cls] = self(args);
733
+ if (cls.call === void 0) throw new LuauError(`attempt to call a ${cls.name} value`);
734
+ return cls.call(value, args.slice(1));
735
+ };
736
+ case "__tostring": return (_, args) => {
737
+ const [value, cls] = self(args);
738
+ return cls.tostring?.(value) ?? cls.name;
739
+ };
740
+ case "__eq": return (_, args) => {
741
+ const [value, cls] = self(args);
742
+ return cls.eq?.(value, args[1]) ?? value === args[1];
743
+ };
744
+ case "__len": return (_, args) => {
745
+ const [value, cls] = self(args);
746
+ if (cls.len === void 0) throw new LuauError(`attempt to get length of a ${cls.name} value`);
747
+ return cls.len(value);
748
+ };
749
+ case "__unm": return (_, args) => {
750
+ const [value, cls] = self(args);
751
+ if (cls.unm === void 0) throw new LuauError(`attempt to perform arithmetic (unm) on ${cls.name}`);
752
+ return cls.unm(value);
753
+ };
754
+ default: return binary(name.slice(2));
755
+ }
756
+ }
757
+ classOf(value) {
758
+ if (typeof value !== "object" || value === null) return void 0;
759
+ const handle = this.handleOf.get(value);
760
+ return handle === void 0 ? void 0 : this.handles.get(handle)?.cls;
761
+ }
762
+ metatableFor(cls) {
763
+ const existing = this.metatables.get(cls);
764
+ if (existing !== void 0) return existing;
765
+ const L = this.L;
766
+ this.M._lb_newtable(L, 0, METAMETHODS.length + 2);
767
+ for (const name of METAMETHODS) {
768
+ this.M._lb_pushfunction(L, this.metamethodIds.get(name), this.debugName(name));
769
+ this.M._lb_setfield(L, -2, this.cstr(name));
770
+ }
771
+ this.pushString(L, cls.name);
772
+ this.M._lb_setfield(L, -2, this.cstr("__type"));
773
+ this.pushString(L, "The metatable is locked");
774
+ this.M._lb_setfield(L, -2, this.cstr("__metatable"));
775
+ this.M._lb_setreadonly(L, -1, 1);
776
+ const ref = this.M._lb_ref(L, -1);
777
+ this.M._lb_settop(L, -2);
778
+ this.metatables.set(cls, ref);
779
+ return ref;
780
+ }
781
+ read(L, idx) {
782
+ switch (this.M._lb_kind(L, idx)) {
783
+ case KIND_NIL: return;
784
+ case KIND_BOOLEAN: return this.M._lb_toboolean(L, idx) !== 0;
785
+ case KIND_NUMBER: return this.M._lb_tonumber(L, idx);
786
+ case KIND_STRING: {
787
+ const ptr = this.M._lb_tostring(L, idx, this.lenPtr);
788
+ const len = this.M.HEAPU32[this.lenPtr >> 2];
789
+ return decoder.decode(this.M.HEAPU8.subarray(ptr, ptr + len));
790
+ }
791
+ case KIND_HOST: return this.handles.get(this.M._lb_tohost(L, idx))?.value;
792
+ case KIND_TABLE: return new LuauTable(this, this.M._lb_ref(L, idx));
793
+ case KIND_FUNCTION: return new LuauFunction(this, this.M._lb_ref(L, idx));
794
+ case KIND_THREAD: return new LuauThread(this, this.M._lb_ref(L, idx));
795
+ default: return new LuauOpaque(this, this.M._lb_ref(L, idx), this.M.UTF8ToString(this.M._lb_typeof(L, idx)));
796
+ }
797
+ }
798
+ readRange(L, from, to) {
799
+ const out = [];
800
+ for (let i = from; i <= to; i++) out.push(this.read(L, i));
801
+ return out;
802
+ }
803
+ push(L, value) {
804
+ this.ensureStack(L, 3);
805
+ switch (typeof value) {
806
+ case "undefined":
807
+ this.M._lb_pushnil(L);
808
+ return;
809
+ case "boolean":
810
+ this.M._lb_pushboolean(L, value ? 1 : 0);
811
+ return;
812
+ case "number":
813
+ this.M._lb_pushnumber(L, value);
814
+ return;
815
+ case "bigint":
816
+ this.M._lb_pushnumber(L, Number(value));
817
+ return;
818
+ case "string":
819
+ this.pushString(L, value);
820
+ return;
821
+ case "function":
822
+ this.M._lb_pushfunction(L, this.functionId(value), this.debugName(value.name || "host"));
823
+ return;
824
+ case "symbol": throw new TypeError("loom luau: a symbol cannot cross into Luau");
825
+ }
826
+ if (value === null) {
827
+ this.M._lb_pushnil(L);
828
+ return;
829
+ }
830
+ if (value instanceof LuauRef) {
831
+ if (value.vm !== this) throw new TypeError("loom luau: a value from another VM cannot cross into this one");
832
+ this.M._lb_getref(L, value.ref);
833
+ return;
834
+ }
835
+ const cls = this.classify(value);
836
+ if (cls !== void 0) {
837
+ this.pushHost(L, value, cls);
838
+ return;
839
+ }
840
+ if (Array.isArray(value)) {
841
+ this.M._lb_newtable(L, value.length, 0);
842
+ value.forEach((item, i) => {
843
+ this.push(L, item);
844
+ this.M._lb_rawseti(L, -2, i + 1);
845
+ });
846
+ return;
847
+ }
848
+ const proto = Object.getPrototypeOf(value);
849
+ if (proto === Object.prototype || proto === null) {
850
+ const fields = Object.entries(value);
851
+ this.M._lb_newtable(L, 0, fields.length);
852
+ for (const [key, item] of fields) {
853
+ this.pushString(L, key);
854
+ this.push(L, item);
855
+ this.M._lb_rawset(L, -3);
856
+ }
857
+ return;
858
+ }
859
+ throw new TypeError(`loom luau: no HostClass for ${proto?.constructor?.name ?? "object"}; cannot cross into Luau`);
860
+ }
861
+ pushHost(L, value, cls) {
862
+ let handle = this.handleOf.get(value);
863
+ if (handle === void 0) {
864
+ handle = this.nextHandle++;
865
+ this.handleOf.set(value, handle);
866
+ this.handles.set(handle, {
867
+ value,
868
+ cls,
869
+ count: 0
870
+ });
871
+ }
872
+ const entry = this.handles.get(handle);
873
+ entry.count++;
874
+ if (this.M._lb_pushhost(L, handle, this.metatableFor(cls)) === 0) entry.count--;
875
+ }
876
+ functionId(fn) {
877
+ let id = this.functionIds.get(fn);
878
+ if (id === void 0) {
879
+ id = this.register((_, args) => fn(...args));
880
+ this.functionIds.set(fn, id);
881
+ }
882
+ return id;
883
+ }
884
+ ensureStack(L, n) {
885
+ if (this.M._lb_checkstack(L, n) === 0) throw new LuauError("stack overflow");
886
+ }
887
+ loadOnto(L, source, chunkname) {
888
+ const name = encoder.encode(`${chunkname}\0`);
889
+ const namePtr = this.M._malloc(name.length);
890
+ this.M.HEAPU8.set(name, namePtr);
891
+ try {
892
+ const bytes = encoder.encode(source);
893
+ const ptr = this.reserve(bytes.length);
894
+ this.M.HEAPU8.set(bytes, ptr);
895
+ return this.M._lb_load(L, namePtr, ptr, bytes.length);
896
+ } finally {
897
+ this.M._free(namePtr);
898
+ }
899
+ }
900
+ pushString(L, value) {
901
+ const bytes = encoder.encode(value);
902
+ const ptr = this.reserve(bytes.length);
903
+ this.M.HEAPU8.set(bytes, ptr);
904
+ this.M._lb_pushstring(L, ptr, bytes.length);
905
+ }
906
+ /** A NUL-terminated copy of `value` in the scratch buffer (overwritten by the next use). */
907
+ cstr(value) {
908
+ const bytes = encoder.encode(`${value}\0`);
909
+ const ptr = this.reserve(bytes.length);
910
+ this.M.HEAPU8.set(bytes, ptr);
911
+ return ptr;
912
+ }
913
+ /** A permanent NUL-terminated string: Luau keeps C function debug names by pointer. */
914
+ debugName(name) {
915
+ let ptr = this.debugNames.get(name);
916
+ if (ptr === void 0) {
917
+ const bytes = encoder.encode(`${name}\0`);
918
+ ptr = this.M._malloc(bytes.length);
919
+ this.M.HEAPU8.set(bytes, ptr);
920
+ this.debugNames.set(name, ptr);
921
+ }
922
+ return ptr;
923
+ }
924
+ reserve(size) {
925
+ if (size > this.scratchSize) {
926
+ if (this.scratch !== 0) this.M._free(this.scratch);
927
+ this.scratchSize = Math.max(size, 256, this.scratchSize * 2);
928
+ this.scratch = this.M._malloc(this.scratchSize);
929
+ }
930
+ return this.scratch;
931
+ }
932
+ };
933
+ /** What a JS exception becomes when it is raised into Luau. */
934
+ function errorValue(error) {
935
+ if (error instanceof LuauError) return error.value;
936
+ if (error instanceof Error) return error.message;
937
+ return error;
938
+ }
939
+ //#endregion
940
+ //#region src/bindings.ts
941
+ /**
942
+ * `bindings.ts` — Roblox, as Luau sees it, over `@loom-dev/runtime`.
943
+ *
944
+ * Nothing here re-implements Roblox: every Instance, datatype, enum and signal
945
+ * a Luau script touches is the same JS object the react and vide adapters use,
946
+ * reached through a `HostClass`. What this file decides is only the Luau shape
947
+ * of each — which members are visible, what a method's result becomes, and
948
+ * which calls yield — plus the `task` library and the globals around it.
949
+ */
950
+ /** Every runtime datatype class, under its Roblox name. */
951
+ const DATATYPES = {
952
+ UDim,
953
+ UDim2,
954
+ Vector2,
955
+ Vector3,
956
+ Color3,
957
+ ColorSequence,
958
+ ColorSequenceKeypoint,
959
+ NumberSequence,
960
+ NumberSequenceKeypoint,
961
+ NumberRange,
962
+ Content,
963
+ Rect,
964
+ CFrame,
965
+ TweenInfo,
966
+ Font,
967
+ Random,
968
+ DateTime
969
+ };
970
+ /** Roblox members are PascalCase; the JS-side `add`, `toString`, … are not members. */
971
+ const isMember = (key) => typeof key === "string" && /^[A-Z]/.test(key);
972
+ function createRobloxBindings() {
973
+ let vm;
974
+ const current = () => {
975
+ if (vm === void 0) throw new Error("loom luau: bindings used before install");
976
+ return vm;
977
+ };
978
+ const connections = /* @__PURE__ */ new WeakSet();
979
+ const methodWrappers = /* @__PURE__ */ new Map();
980
+ const method = (cls, name) => {
981
+ const key = `${cls.name}.${name}`;
982
+ let wrapper = methodWrappers.get(key);
983
+ if (wrapper === void 0) {
984
+ wrapper = (self, ...args) => cls.namecall?.(self, name, args);
985
+ methodWrappers.set(key, wrapper);
986
+ }
987
+ return wrapper;
988
+ };
989
+ const datatypeClasses = /* @__PURE__ */ new Map();
990
+ for (const [name, ctor] of Object.entries(DATATYPES)) datatypeClasses.set(ctor, datatypeClass(name));
991
+ function datatypeClass(name) {
992
+ const cls = {
993
+ name,
994
+ index(self, key) {
995
+ if (!isMember(key) || !(key in self)) throw new LuauError(`${String(key)} is not a valid member of ${name}`);
996
+ const value = self[key];
997
+ return typeof value === "function" ? method(cls, key) : value;
998
+ },
999
+ newindex(_, key) {
1000
+ throw new LuauError(`${String(key)} cannot be assigned to`);
1001
+ },
1002
+ namecall(self, member, args) {
1003
+ const fn = isMember(member) ? self[member] : void 0;
1004
+ if (typeof fn !== "function") throw new LuauError(`${member} is not a valid member of ${name}`);
1005
+ const result = fn.apply(self, args);
1006
+ return Array.isArray(result) ? new Tuple(result) : result;
1007
+ },
1008
+ tostring: (self) => String(self),
1009
+ eq: (a, b) => robloxEquals(a, b),
1010
+ unm(self) {
1011
+ if (typeof self.mul !== "function") return arithmeticError("unm", self, void 0);
1012
+ return self.mul(-1);
1013
+ },
1014
+ add: (a, b) => arithmetic("add", a, b),
1015
+ sub: (a, b) => arithmetic("sub", a, b),
1016
+ mul: (a, b) => arithmetic("mul", a, b),
1017
+ div: (a, b) => arithmetic("div", a, b)
1018
+ };
1019
+ return cls;
1020
+ }
1021
+ function arithmetic(op, a, b) {
1022
+ const left = a;
1023
+ if (typeof left?.[op] === "function") return left[op](b);
1024
+ const right = b;
1025
+ if (op === "mul" && typeof a === "number" && typeof right?.mul === "function") return right.mul(a);
1026
+ return arithmeticError(op, a, b);
1027
+ }
1028
+ function arithmeticError(op, a, b) {
1029
+ throw new LuauError(`attempt to perform arithmetic (${op}) on ${typeName(a)} and ${typeName(b)}`);
1030
+ }
1031
+ /** `UDim2.new`, `Color3.fromRGB`, `Vector2.zero`, … as a plain table. */
1032
+ function library(ctor) {
1033
+ const lib = {};
1034
+ for (const key of Object.getOwnPropertyNames(ctor)) {
1035
+ if (key === "length" || key === "name" || key === "prototype") continue;
1036
+ const value = ctor[key];
1037
+ lib[key] = typeof value === "function" ? (...args) => value.apply(ctor, args) : value;
1038
+ }
1039
+ return lib;
1040
+ }
1041
+ const EnumsClass = {
1042
+ name: "Enums",
1043
+ index(self, key) {
1044
+ const value = typeof key === "string" ? self[key] : void 0;
1045
+ if (value instanceof RobloxEnum) return value;
1046
+ if (key === "GetEnums") return method(EnumsClass, key);
1047
+ throw new LuauError(`${String(key)} is not a valid member of "Enum"`);
1048
+ },
1049
+ namecall(self, member) {
1050
+ if (member === "GetEnums") return self.GetEnums();
1051
+ throw new LuauError(`${member} is not a valid member of "Enum"`);
1052
+ },
1053
+ tostring: () => "Enums"
1054
+ };
1055
+ const EnumClass = {
1056
+ name: "Enum",
1057
+ index(self, key) {
1058
+ const value = typeof key === "string" ? self[key] : void 0;
1059
+ if (value instanceof EnumItem) return value;
1060
+ if (key === "GetEnumItems" || key === "FromName" || key === "FromValue") return method(EnumClass, key);
1061
+ throw new LuauError(`${String(key)} is not a valid EnumItem`);
1062
+ },
1063
+ namecall(self, member, args) {
1064
+ switch (member) {
1065
+ case "GetEnumItems": return self.GetEnumItems();
1066
+ case "FromName": return self.FromName(args[0]);
1067
+ case "FromValue": return self.FromValue(args[0]);
1068
+ }
1069
+ throw new LuauError(`${member} is not a valid member of ${String(self)}`);
1070
+ },
1071
+ tostring: (self) => String(self)
1072
+ };
1073
+ const EnumItemClass = {
1074
+ name: "EnumItem",
1075
+ index(self, key) {
1076
+ if (key === "Name" || key === "Value" || key === "EnumType") return self[key];
1077
+ if (key === "IsA") return method(EnumItemClass, key);
1078
+ throw new LuauError(`${String(key)} is not a valid member of "${String(self)}"`);
1079
+ },
1080
+ namecall(self, member, args) {
1081
+ if (member === "IsA") return enumTypeName(self) === args[0];
1082
+ throw new LuauError(`${member} is not a valid member of "${String(self)}"`);
1083
+ },
1084
+ tostring: (self) => String(self)
1085
+ };
1086
+ const SignalClass = {
1087
+ name: "RBXScriptSignal",
1088
+ index(_, key) {
1089
+ if (key === "Connect" || key === "ConnectParallel" || key === "Once" || key === "Wait") return method(SignalClass, key);
1090
+ throw new LuauError(`${String(key)} is not a valid member of RBXScriptSignal`);
1091
+ },
1092
+ namecall(self, member, args) {
1093
+ switch (member) {
1094
+ case "Connect":
1095
+ case "ConnectParallel":
1096
+ case "Once": {
1097
+ const fn = args[0];
1098
+ const handler = (...values) => {
1099
+ const vm = current();
1100
+ if (!vm.isClosed) vm.spawn(fn, values);
1101
+ };
1102
+ const connection = member === "Once" ? self.Once(handler) : self.Connect(handler);
1103
+ connections.add(connection);
1104
+ return connection;
1105
+ }
1106
+ case "Wait": return new Suspend((resume) => {
1107
+ self.Once((...values) => resume(...values));
1108
+ });
1109
+ }
1110
+ throw new LuauError(`${member} is not a valid member of RBXScriptSignal`);
1111
+ },
1112
+ tostring: () => "Signal"
1113
+ };
1114
+ const ConnectionClass = {
1115
+ name: "RBXScriptConnection",
1116
+ index(self, key) {
1117
+ if (key === "Connected") return self.Connected;
1118
+ if (key === "Disconnect") return method(ConnectionClass, key);
1119
+ throw new LuauError(`${String(key)} is not a valid member of RBXScriptConnection`);
1120
+ },
1121
+ namecall(self, member) {
1122
+ if (member === "Disconnect") return self.Disconnect();
1123
+ throw new LuauError(`${member} is not a valid member of RBXScriptConnection`);
1124
+ },
1125
+ tostring: () => "Connection"
1126
+ };
1127
+ const InstanceClass = {
1128
+ name: "Instance",
1129
+ index(self, key) {
1130
+ if (typeof key !== "string") throw new LuauError(`${String(key)} is not a valid member of ${self.ClassName}`);
1131
+ const value = self[key];
1132
+ if (typeof value === "function") return method(InstanceClass, key);
1133
+ if (value !== void 0) return value;
1134
+ return self.FindFirstChild(key);
1135
+ },
1136
+ newindex(self, key, value) {
1137
+ self[key] = value;
1138
+ },
1139
+ namecall(self, member, args) {
1140
+ if (member === "WaitForChild") return waitForChild(self, args[0], args[1]);
1141
+ if (member === "GetAttributes") return Object.fromEntries(self.GetAttributes());
1142
+ const fn = self[member];
1143
+ if (typeof fn !== "function") throw new LuauError(`${member} is not a valid member of ${self.ClassName} "${self.Name}"`);
1144
+ const result = fn(...args.map((arg) => arg instanceof LuauFunction ? (...values) => {
1145
+ const vm = current();
1146
+ if (vm.isClosed) return void 0;
1147
+ const ended = vm.spawn(arg, values);
1148
+ return ended.status === "done" ? ended.values[0] : void 0;
1149
+ } : arg));
1150
+ if (isLuaTuple(result)) return new Tuple(result);
1151
+ if (result instanceof Promise) return new Suspend((resume) => {
1152
+ result.then((value) => resume(value));
1153
+ });
1154
+ return result;
1155
+ },
1156
+ tostring: (self) => self.Name
1157
+ };
1158
+ const modules = /* @__PURE__ */ new Map();
1159
+ /**
1160
+ * `require(moduleScript)`, as `(ok, valueOrMessage)` for the prelude's
1161
+ * `require` to raise from. A module runs once per VM, in its own thread, with
1162
+ * itself as `script`. Whoever requires it while it is still yielding waits
1163
+ * for it; requiring it again from inside its own first run is the recursion
1164
+ * Roblox reports.
1165
+ */
1166
+ function requireModule(module, from) {
1167
+ if (typeof module === "string") {
1168
+ const resolved = resolveRequirePath(module, from);
1169
+ if (typeof resolved === "string") return tuple(false, resolved);
1170
+ module = resolved;
1171
+ }
1172
+ if (!isLoomInstance(module) || !module.IsA("ModuleScript")) return tuple(false, "Attempted to call require with invalid argument(s).");
1173
+ const loaded = modules.get(module);
1174
+ if (loaded !== void 0) return settle(module, loaded);
1175
+ const source = getScriptSource(module);
1176
+ if (source === void 0) return tuple(false, `${module.GetFullName()} has no source loom can run`);
1177
+ modules.set(module, { state: "running" });
1178
+ if (current().run(source, {
1179
+ chunkname: `=${module.GetFullName()}`,
1180
+ globals: { script: module },
1181
+ onFinish: (ended) => finishModule(module, ended)
1182
+ }).status === "yielded") modules.set(module, {
1183
+ state: "waiting",
1184
+ waiters: []
1185
+ });
1186
+ return settle(module, modules.get(module));
1187
+ }
1188
+ /** `module`'s thread ended: keep what it returned and wake whoever waits on it. */
1189
+ function finishModule(module, result) {
1190
+ const before = modules.get(module);
1191
+ let after = { state: "failed" };
1192
+ if (result.status === "done") if (result.values.length === 1) after = {
1193
+ state: "done",
1194
+ value: result.values[0]
1195
+ };
1196
+ else current().report(new LuauError(`${module.GetFullName()}: Module code did not return exactly one value`));
1197
+ modules.set(module, after);
1198
+ if (before?.state === "waiting") for (const wake of before.waiters) wake();
1199
+ }
1200
+ /** What a `require` of `module`, currently `loaded`, hands back. */
1201
+ function settle(module, loaded) {
1202
+ switch (loaded.state) {
1203
+ case "done": return tuple(true, loaded.value);
1204
+ case "failed": return tuple(false, "Requested module experienced an error while loading");
1205
+ case "running": return tuple(false, "Requested module was required recursively");
1206
+ case "waiting": return new Suspend((resume) => {
1207
+ loaded.waiters.push(() => {
1208
+ resume(...settle(module, modules.get(module)).values);
1209
+ });
1210
+ });
1211
+ }
1212
+ }
1213
+ /** The real `WaitForChild`: yield until the child turns up, or `timeout` runs out. */
1214
+ function waitForChild(parent, name, timeout) {
1215
+ const existing = parent.FindFirstChild(name);
1216
+ if (existing !== void 0) return existing;
1217
+ return new Suspend((resume) => {
1218
+ let timer;
1219
+ const connection = parent.ChildAdded.Connect((child) => {
1220
+ if (child.Name !== name) return;
1221
+ connection.Disconnect();
1222
+ if (timer !== void 0) clearTimeout(timer);
1223
+ resume(child);
1224
+ });
1225
+ if (timeout !== void 0) timer = setTimeout(() => {
1226
+ connection.Disconnect();
1227
+ resume(void 0);
1228
+ }, timeout * 1e3);
1229
+ });
1230
+ }
1231
+ const classify = (value) => {
1232
+ if (isLoomInstance(value)) return InstanceClass;
1233
+ if (value instanceof LoomSignal) return SignalClass;
1234
+ if (connections.has(value)) return ConnectionClass;
1235
+ if (value instanceof EnumItem) return EnumItemClass;
1236
+ if (value instanceof RobloxEnum) return EnumClass;
1237
+ if (value === Enum) return EnumsClass;
1238
+ return datatypeClasses.get(value.constructor);
1239
+ };
1240
+ function install(target) {
1241
+ vm = target;
1242
+ for (const [name, ctor] of Object.entries(DATATYPES)) target.setGlobal(name, library(ctor));
1243
+ target.setGlobal("Enum", Enum);
1244
+ target.setGlobal("game", game);
1245
+ target.setGlobal("workspace", game.Workspace);
1246
+ target.setGlobal("Instance", { new: (className, parent) => {
1247
+ const instance = createInstance(className);
1248
+ if (parent !== void 0) instance.Parent = parent;
1249
+ return instance;
1250
+ } });
1251
+ const started = performance.now();
1252
+ target.load(PRELUDE, "=loom.prelude").call({
1253
+ wait,
1254
+ defer: (thread, ...args) => {
1255
+ queueMicrotask(() => target.resumeThread(thread, args));
1256
+ },
1257
+ delay: (seconds, thread, ...args) => {
1258
+ afterSeconds(seconds, () => target.resumeThread(thread, args));
1259
+ },
1260
+ report: (message, traceback) => {
1261
+ target.report(new LuauError(message, message, traceback));
1262
+ },
1263
+ print: (line) => console.log(line),
1264
+ warn: (line) => console.warn(line),
1265
+ require: requireModule,
1266
+ tick: () => Date.now() / 1e3,
1267
+ time: () => (performance.now() - started) / 1e3
1268
+ });
1269
+ }
1270
+ return {
1271
+ classify,
1272
+ install
1273
+ };
1274
+ }
1275
+ /**
1276
+ * Require-by-string, the engine's way: `@self/x` starts at the requiring
1277
+ * script itself, `./x` at its parent, `../x` one above that (each further
1278
+ * `..` climbs one more), and `@game/x` at the DataModel. Returns the instance
1279
+ * the path names, or the error message when it names none.
1280
+ */
1281
+ function resolveRequirePath(path, from) {
1282
+ const parts = path.split("/");
1283
+ const head = parts.shift();
1284
+ if (head === "@game") return walkPath(game, parts, path, "game");
1285
+ if (head !== "@self" && head !== "." && head !== "..") return `require("${path}"): a path must start with "./", "../", "@self/" or "@game/"`;
1286
+ if (!isLoomInstance(from)) return `require("${path}"): cannot resolve a path outside a script`;
1287
+ const parent = from.Parent;
1288
+ return walkPath(head === "@self" ? from : head === "." ? parent : parent?.Parent, parts, path, from.GetFullName());
1289
+ }
1290
+ /** Follow `parts` (child names, or `..` for a parent) down from `at`. */
1291
+ function walkPath(at, parts, path, origin) {
1292
+ for (const part of parts) {
1293
+ if (at === void 0) break;
1294
+ if (part === "" || part === ".") continue;
1295
+ at = part === ".." ? at.Parent : at.FindFirstChild(part);
1296
+ }
1297
+ return at ?? `require("${path}"): no module at that path from ${origin}`;
1298
+ }
1299
+ /** `task.wait`: yield until the first Heartbeat after `seconds`, return the time waited. */
1300
+ function wait(seconds) {
1301
+ const started = performance.now();
1302
+ return new Suspend((resume) => {
1303
+ afterSeconds(seconds ?? 0, () => resume((performance.now() - started) / 1e3));
1304
+ });
1305
+ }
1306
+ /** Run `fn` on the first Heartbeat at least `seconds` from now — Roblox's resume point. */
1307
+ function afterSeconds(seconds, fn) {
1308
+ const deadline = performance.now() + Math.max(seconds, 0) * 1e3;
1309
+ const connection = heartbeat.Connect(() => {
1310
+ if (performance.now() < deadline) return;
1311
+ connection.Disconnect();
1312
+ fn();
1313
+ });
1314
+ }
1315
+ function typeName(value) {
1316
+ if (value === void 0 || value === null) return "nil";
1317
+ if (typeof value !== "object") return typeof value;
1318
+ return Object.entries(DATATYPES).find(([, ctor]) => value.constructor === ctor)?.[0] ?? "userdata";
1319
+ }
1320
+ //#endregion
1321
+ //#region src/mount.ts
1322
+ /**
1323
+ * `mount.ts` — run a Luau LocalScript into a live loom preview.
1324
+ *
1325
+ * A Luau script builds instances imperatively and parents them under
1326
+ * `PlayerGui`, so what it needs from loom is exactly the react adapter's
1327
+ * world: the runtime PlayerGui as root, encoded → laid out → painted on every
1328
+ * dirty frame. `createWorld` is reused as-is; nothing in the layout engine or
1329
+ * the renderer knows the tree came from Luau.
1330
+ */
1331
+ /** Print an escaped error the way the engine's output does: message, then stack. */
1332
+ function printError(error) {
1333
+ console.error(`${error.message}${error.traceback ? `\n${error.traceback}` : ""}`);
1334
+ }
1335
+ /** A fresh VM with Roblox's globals installed, ready to run scripts. */
1336
+ async function createLuauVM(setup = {}) {
1337
+ const bindings = createRobloxBindings();
1338
+ const vm = await LuauVM.create({
1339
+ classify: bindings.classify,
1340
+ budgetMs: setup.budgetMs ?? 5e3,
1341
+ sharedTables: ["_G", "shared"],
1342
+ ...setup.wasmBinary ? { wasmBinary: setup.wasmBinary } : {},
1343
+ onError: setup.onError ?? printError
1344
+ });
1345
+ bindings.install(vm);
1346
+ return vm;
1347
+ }
1348
+ const HOST_ID = "loom-root";
1349
+ /**
1350
+ * Where the world paints. An explicit `target` is the mount itself, as with
1351
+ * react's `render`; without one each mount gets its own container under the
1352
+ * preview viewport (`#loom-root`, created if the page lacks it), as react's
1353
+ * `createRoot` and vide's `mount` do, so independent mounts don't clobber.
1354
+ */
1355
+ function resolveMount(target) {
1356
+ if (target) return {
1357
+ mount: target,
1358
+ owned: false
1359
+ };
1360
+ let viewport = document.getElementById(HOST_ID);
1361
+ if (!viewport) {
1362
+ viewport = document.createElement("div");
1363
+ viewport.id = HOST_ID;
1364
+ viewport.style.position = "relative";
1365
+ viewport.style.width = "100vw";
1366
+ viewport.style.height = "100vh";
1367
+ viewport.style.overflow = "hidden";
1368
+ document.body.appendChild(viewport);
1369
+ }
1370
+ const mount = document.createElement("div");
1371
+ mount.style.position = "absolute";
1372
+ mount.style.inset = "0";
1373
+ viewport.appendChild(mount);
1374
+ return {
1375
+ mount,
1376
+ owned: true
1377
+ };
1378
+ }
1379
+ /**
1380
+ * Run `source` as a LocalScript in a fresh VM and paint what it builds. The
1381
+ * script sits inside the world's default ScreenGui, so the common
1382
+ * `script.Parent` idiom lands its UI on screen, and anything it parents to
1383
+ * `Players.LocalPlayer.PlayerGui` renders as its own layer.
1384
+ */
1385
+ async function mountLuau(source, options = {}) {
1386
+ if (options.computeLayout === void 0) await initLayout();
1387
+ const vm = await createLuauVM(options);
1388
+ const { mount, owned } = resolveMount(options.target);
1389
+ const world = createWorld(mount, options);
1390
+ const name = options.name ?? "LocalScript";
1391
+ const script = createInstance("LocalScript", name);
1392
+ script.Parent = world.defaultGui;
1393
+ const result = vm.run(source, {
1394
+ chunkname: `=${name}`,
1395
+ globals: { script }
1396
+ });
1397
+ world.flushSync();
1398
+ return {
1399
+ vm,
1400
+ world,
1401
+ script,
1402
+ result,
1403
+ unmount() {
1404
+ world.dispose();
1405
+ vm.close();
1406
+ if (owned) mount.remove();
1407
+ }
1408
+ };
1409
+ }
1410
+ /**
1411
+ * Start a Rojo project the way a client joins a place: build the tree into
1412
+ * `game`, then run every LocalScript a client runs (ReplicatedFirst, the
1413
+ * StarterGui copy in PlayerGui, the StarterPlayerScripts copy in
1414
+ * PlayerScripts) in one VM, so `require` shares each module between them.
1415
+ */
1416
+ async function runLuauProject(project, options = {}) {
1417
+ if (options.computeLayout === void 0) await initLayout();
1418
+ const vm = await createLuauVM(options);
1419
+ const { mount, owned } = resolveMount(options.target);
1420
+ const world = createWorld(mount, options);
1421
+ instantiateProject(project);
1422
+ const scripts = startClient().map(({ script, source }) => ({
1423
+ script,
1424
+ result: vm.run(source, {
1425
+ chunkname: `=${script.GetFullName()}`,
1426
+ globals: { script }
1427
+ })
1428
+ }));
1429
+ world.flushSync();
1430
+ return {
1431
+ vm,
1432
+ world,
1433
+ scripts,
1434
+ unmount() {
1435
+ world.dispose();
1436
+ vm.close();
1437
+ if (owned) mount.remove();
1438
+ }
1439
+ };
1440
+ }
1441
+ //#endregion
1442
+ export { setScriptSource as _, LuauError as a, LuauTable as c, Suspend as d, Tuple as f, instantiateProject as g, getScriptSource as h, createRobloxBindings as i, LuauThread as l, cloneWithSources as m, mountLuau as n, LuauFunction as o, tuple as p, runLuauProject as r, LuauOpaque as s, createLuauVM as t, LuauVM as u };
1443
+
1444
+ //# sourceMappingURL=mount-2LXSpSYv.js.map