@silverbulletmd/silverbullet 2.4.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -4,6 +4,7 @@ import type {
4
4
  LuaExpression,
5
5
  LuaLValue,
6
6
  LuaStatement,
7
+ LuaTableField,
7
8
  NumericType,
8
9
  } from "./ast.ts";
9
10
  import { LuaAttribute } from "./ast.ts";
@@ -19,6 +20,7 @@ import {
19
20
  luaEnsureCloseStack,
20
21
  LuaEnv,
21
22
  luaEquals,
23
+ luaFormatNumber,
22
24
  LuaFunction,
23
25
  luaGet,
24
26
  luaIndexValue,
@@ -38,8 +40,9 @@ import {
38
40
  singleResult,
39
41
  } from "./runtime.ts";
40
42
  import {
41
- ArrayQueryCollection,
42
43
  type LuaCollectionQuery,
44
+ type LuaGroupByEntry,
45
+ toCollection,
43
46
  } from "./query_collection.ts";
44
47
  import {
45
48
  coerceNumericPair,
@@ -107,8 +110,14 @@ function astNumberKind(e: LuaExpression | undefined): NumericType | undefined {
107
110
  result = unwrapped.numericType === "int" ? "int" : "float";
108
111
  } else if (unwrapped.type === "Binary") {
109
112
  const op = unwrapped.operator;
110
- const numericOp = op === "+" || op === "-" || op === "*" || op === "/" ||
111
- op === "//" || op === "%" || op === "^";
113
+ const numericOp =
114
+ op === "+" ||
115
+ op === "-" ||
116
+ op === "*" ||
117
+ op === "/" ||
118
+ op === "//" ||
119
+ op === "%" ||
120
+ op === "^";
112
121
 
113
122
  if (numericOp) {
114
123
  const lk = astNumberKind(unwrapped.left);
@@ -178,18 +187,6 @@ function blockMetaOrThrow(
178
187
  }
179
188
  }
180
189
 
181
- // Queryable guard to avoid `(collection as any).query` usage
182
- type Queryable = {
183
- query: (
184
- q: LuaCollectionQuery,
185
- env: LuaEnv,
186
- sf: LuaStackFrame,
187
- ) => Promise<any>;
188
- };
189
- function isQueryable(x: unknown): x is Queryable {
190
- return !!x && typeof (x as any).query === "function";
191
- }
192
-
193
190
  function arithVerbFromOperator(op: string): string | null {
194
191
  switch (op) {
195
192
  case "+":
@@ -250,7 +247,7 @@ function arithCoercionErrorOrThrow(
250
247
  throw e;
251
248
  }
252
249
 
253
- function luaOp(
250
+ export function luaOp(
254
251
  op: string,
255
252
  left: any,
256
253
  right: any,
@@ -260,67 +257,68 @@ function luaOp(
260
257
  sf: LuaStackFrame,
261
258
  ): any {
262
259
  switch (op) {
263
- case "+":
264
- case "-":
265
- case "*":
260
+ case "+": {
261
+ // Ultra-fast path: both plain numbers with no float type annotation (int + int)
262
+ if (
263
+ typeof left === "number" &&
264
+ typeof right === "number" &&
265
+ leftType !== "float" &&
266
+ rightType !== "float"
267
+ ) {
268
+ return left + right;
269
+ }
270
+ return luaArithGeneric("+", left, right, leftType, rightType, ctx, sf);
271
+ }
272
+ case "-": {
273
+ if (
274
+ typeof left === "number" &&
275
+ typeof right === "number" &&
276
+ leftType !== "float" &&
277
+ rightType !== "float"
278
+ ) {
279
+ const r = left - right;
280
+ // Integer subtraction can produce -0 (e.g. 0 - 0), normalize to +0
281
+ return r === 0 ? 0 : r;
282
+ }
283
+ return luaArithGeneric("-", left, right, leftType, rightType, ctx, sf);
284
+ }
285
+ case "*": {
286
+ if (
287
+ typeof left === "number" &&
288
+ typeof right === "number" &&
289
+ leftType !== "float" &&
290
+ rightType !== "float"
291
+ ) {
292
+ const r = left * right;
293
+ // Integer multiplication can produce -0 (e.g. 0 * -1), normalize to +0
294
+ return r === 0 ? 0 : r;
295
+ }
296
+ return luaArithGeneric("*", left, right, leftType, rightType, ctx, sf);
297
+ }
266
298
  case "/":
267
299
  case "^": {
268
- const ar = numericArith[op as NumericArithOp];
269
- try {
270
- const { left: l, right: r, resultType } = coerceNumericPair(
271
- left,
272
- right,
273
- leftType,
274
- rightType,
275
- op,
276
- );
277
-
278
- let result = ar.f(l, r);
279
-
280
- if (
281
- ar.special === "sub" &&
282
- result === 0 &&
283
- isNegativeZero(result) &&
284
- resultType === "float"
285
- ) {
286
- const rhsIsIntZero = r === 0 && rightType === "int";
287
- if (rhsIsIntZero) {
288
- result = 0;
289
- }
290
- }
291
-
292
- const normalized = normalizeArithmeticResult(result, resultType);
293
-
294
- // Operators `/` and `^` produce float, wrap only if needed.
295
- if (op === "/" || op === "^") {
296
- if (normalized === 0) {
297
- return makeLuaZero(normalized, "float");
298
- }
299
- if (!Number.isFinite(normalized)) {
300
- return normalized;
301
- }
302
- if (!Number.isInteger(normalized)) {
303
- return normalized;
304
- }
305
- return makeLuaFloat(normalized);
306
- }
307
-
308
- if (normalized === 0) {
309
- return makeLuaZero(normalized, resultType);
310
- }
311
- if (resultType === "float" && Number.isInteger(normalized)) {
312
- return makeLuaFloat(normalized);
313
- }
314
- return normalized;
315
- } catch (e: any) {
316
- const meta = evalMetamethod(left, right, ar.metaMethod, ctx, sf);
317
- if (meta !== undefined) {
318
- return meta;
319
- }
320
- return arithCoercionErrorOrThrow(op, left, right, ctx, sf, e);
321
- }
300
+ return luaArithGeneric(
301
+ op as NumericArithOp,
302
+ left,
303
+ right,
304
+ leftType,
305
+ rightType,
306
+ ctx,
307
+ sf,
308
+ );
322
309
  }
323
310
  case "..": {
311
+ // Fast path: string .. string (most common in SilverBullet — key building, templates)
312
+ if (typeof left === "string" && typeof right === "string") {
313
+ return left + right;
314
+ }
315
+ // Fast path: string .. number or number .. string
316
+ if (typeof left === "string" && typeof right === "number") {
317
+ return left + luaFormatNumber(right);
318
+ }
319
+ if (typeof left === "number" && typeof right === "string") {
320
+ return luaFormatNumber(left) + right;
321
+ }
324
322
  try {
325
323
  const coerce = (v: any): string => {
326
324
  if (v === null || v === undefined) {
@@ -333,10 +331,10 @@ function luaOp(
333
331
  return v as string;
334
332
  }
335
333
  if (typeof v === "number") {
336
- return String(v);
334
+ return luaFormatNumber(v);
337
335
  }
338
336
  if (isTaggedFloat(v)) {
339
- return String(v.value);
337
+ return luaFormatNumber(v.value, "float");
340
338
  }
341
339
  const t = luaTypeName(v);
342
340
  throw new LuaRuntimeError(
@@ -354,27 +352,54 @@ function luaOp(
354
352
  }
355
353
  }
356
354
  case "==": {
355
+ // Fast path for same-type primitives
356
+ if (typeof left === typeof right && typeof left !== "object") {
357
+ return left === right;
358
+ }
357
359
  if (luaEquals(left, right)) return true;
358
360
  return luaEqWithMetamethod(left, right, ctx, sf);
359
361
  }
360
362
  case "~=":
361
363
  case "!=": {
364
+ if (typeof left === typeof right && typeof left !== "object") {
365
+ return left !== right;
366
+ }
362
367
  if (luaEquals(left, right)) {
363
368
  return false;
364
369
  }
365
370
  return !luaEqWithMetamethod(left, right, ctx, sf);
366
371
  }
367
372
  case "<": {
373
+ // Fast path: both plain numbers
374
+ if (typeof left === "number" && typeof right === "number") {
375
+ return left < right;
376
+ }
377
+ // Fast path: both strings
378
+ if (typeof left === "string" && typeof right === "string") {
379
+ return left < right;
380
+ }
368
381
  return luaRelWithMetamethod("<", left, right, ctx, sf);
369
382
  }
370
383
  case "<=": {
384
+ if (typeof left === "number" && typeof right === "number")
385
+ return left <= right;
386
+ if (typeof left === "string" && typeof right === "string")
387
+ return left <= right;
371
388
  return luaRelWithMetamethod("<=", left, right, ctx, sf);
372
389
  }
373
390
  // Lua: `a>b` is `b<a`, `a>=b` is `b<=a`
374
391
  case ">": {
392
+ if (typeof left === "number" && typeof right === "number")
393
+ return left > right;
394
+ if (typeof left === "string" && typeof right === "string")
395
+ return left > right;
375
396
  return luaRelWithMetamethod("<", right, left, ctx, sf);
376
397
  }
377
398
  case ">=": {
399
+ if (typeof left === "number" && typeof right === "number")
400
+ return left >= right;
401
+ if (typeof left === "string" && typeof right === "string")
402
+ return left >= right;
378
403
  return luaRelWithMetamethod("<=", right, left, ctx, sf);
379
404
  }
380
405
  }
@@ -405,13 +430,79 @@ function luaOp(
405
430
  }
406
431
  }
407
432
 
433
+ function luaArithGeneric(
434
+ op: NumericArithOp,
435
+ left: any,
436
+ right: any,
437
+ leftType: NumericType | undefined,
438
+ rightType: NumericType | undefined,
439
+ ctx: ASTCtx,
440
+ sf: LuaStackFrame,
441
+ ): any {
442
+ const ar = numericArith[op];
443
+ try {
444
+ const {
445
+ left: l,
446
+ right: r,
447
+ resultType,
448
+ } = coerceNumericPair(left, right, leftType, rightType, op);
449
+
450
+ let result = ar.f(l, r);
451
+
452
+ if (
453
+ ar.special === "sub" &&
454
+ result === 0 &&
455
+ isNegativeZero(result) &&
456
+ resultType === "float"
457
+ ) {
458
+ const rhsIsIntZero = r === 0 && rightType === "int";
459
+ if (rhsIsIntZero) {
460
+ result = 0;
461
+ }
462
+ }
463
+
464
+ const normalized = normalizeArithmeticResult(result, resultType);
465
+
466
+ // Operators `/` and `^` produce float, wrap only if needed.
467
+ if (op === "/" || op === "^") {
468
+ if (normalized === 0) {
469
+ return makeLuaZero(normalized, "float");
470
+ }
471
+ if (!Number.isFinite(normalized)) {
472
+ return normalized;
473
+ }
474
+ if (!Number.isInteger(normalized)) {
475
+ return normalized;
476
+ }
477
+ return makeLuaFloat(normalized);
478
+ }
479
+
480
+ if (normalized === 0) {
481
+ return makeLuaZero(normalized, resultType);
482
+ }
483
+ if (resultType === "float" && Number.isInteger(normalized)) {
484
+ return makeLuaFloat(normalized);
485
+ }
486
+ return normalized;
487
+ } catch (e: any) {
488
+ const meta = evalMetamethod(left, right, ar.metaMethod, ctx, sf);
489
+ if (meta !== undefined) {
490
+ return meta;
491
+ }
492
+ return arithCoercionErrorOrThrow(op, left, right, ctx, sf, e);
493
+ }
494
+ }
495
+
408
496
  type NumericArithOp = "+" | "-" | "*" | "/" | "^";
409
497
 
410
- const numericArith: Record<NumericArithOp, {
411
- metaMethod: "__add" | "__sub" | "__mul" | "__div" | "__pow";
412
- f: (l: number, r: number) => number;
413
- special?: "sub";
414
- }> = {
498
+ const numericArith: Record<
499
+ NumericArithOp,
500
+ {
501
+ metaMethod: "__add" | "__sub" | "__mul" | "__div" | "__pow";
502
+ f: (l: number, r: number) => number;
503
+ special?: "sub";
504
+ }
505
+ > = {
415
506
  "+": { metaMethod: "__add", f: (l, r) => l + r },
416
507
  "-": { metaMethod: "__sub", f: (l, r) => l - r, special: "sub" },
417
508
  "*": { metaMethod: "__mul", f: (l, r) => l * r },
@@ -461,10 +552,7 @@ function luaFloorDiv(
461
552
  );
462
553
 
463
554
  if (resultType === "int" && right === 0) {
464
- throw new LuaRuntimeError(
465
- `attempt to divide by zero`,
466
- sf.withCtx(ctx),
467
- );
555
+ throw new LuaRuntimeError(`attempt to divide by zero`, sf.withCtx(ctx));
468
556
  }
469
557
 
470
558
  const result = Math.floor(left / right);
@@ -495,10 +583,7 @@ function luaMod(
495
583
  );
496
584
 
497
585
  if (resultType === "int" && right === 0) {
498
- throw new LuaRuntimeError(
499
- `attempt to perform 'n%0'`,
500
- sf.withCtx(ctx),
501
- );
586
+ throw new LuaRuntimeError(`attempt to perform 'n%0'`, sf.withCtx(ctx));
502
587
  }
503
588
 
504
589
  const q = Math.floor(left / right);
@@ -519,10 +604,7 @@ function luaMod(
519
604
  return normalized;
520
605
  }
521
606
 
522
- function luaUnaryMinus(
523
- v: number,
524
- numType: NumericType | undefined,
525
- ): number {
607
+ function luaUnaryMinus(v: number, numType: NumericType | undefined): number {
526
608
  const vType = numType ?? inferNumericType(v);
527
609
 
528
610
  if (v === 0 && vType === "int") {
@@ -536,17 +618,20 @@ function luaUnaryMinus(
536
618
  return -v;
537
619
  }
538
620
 
539
- const operatorsMetaMethods: Record<string, {
540
- metaMethod?: string;
541
- nativeImplementation: (
542
- a: LuaValue,
543
- b: LuaValue,
544
- leftType: NumericType | undefined,
545
- rightType: NumericType | undefined,
546
- ctx: ASTCtx,
547
- sf: LuaStackFrame,
548
- ) => LuaValue;
549
- }> = {
621
+ const operatorsMetaMethods: Record<
622
+ string,
623
+ {
624
+ metaMethod?: string;
625
+ nativeImplementation: (
626
+ a: LuaValue,
627
+ b: LuaValue,
628
+ leftType: NumericType | undefined,
629
+ rightType: NumericType | undefined,
630
+ ctx: ASTCtx,
631
+ sf: LuaStackFrame,
632
+ ) => LuaValue;
633
+ }
634
+ > = {
550
635
  "//": {
551
636
  metaMethod: "__idiv",
552
637
  nativeImplementation: (a, b, lt, rt, ctx, sf) =>
@@ -609,6 +694,168 @@ const operatorsMetaMethods: Record<string, {
609
694
  },
610
695
  };
611
696
 
697
+ function deriveFieldName(e: LuaExpression): string | undefined {
698
+ switch (e.type) {
699
+ case "Variable":
700
+ return e.name;
701
+ case "PropertyAccess":
702
+ return e.property;
703
+ case "FunctionCall":
704
+ if (e.name) return e.name;
705
+ if (e.prefix.type === "Variable") return e.prefix.name;
706
+ if (e.prefix.type === "PropertyAccess") return e.prefix.property;
707
+ return undefined;
708
+ case "FilteredCall":
709
+ return deriveFieldName(e.call);
710
+ case "AggregateCall":
711
+ return deriveFieldName((e as any).call);
712
+ default:
713
+ return undefined;
714
+ }
715
+ }
716
+
717
+ function fieldsToExpression(
718
+ fields: LuaTableField[],
719
+ ctx: ASTCtx,
720
+ ): LuaExpression {
721
+ if (fields.length === 1 && fields[0].type === "ExpressionField") {
722
+ return fields[0].value;
723
+ }
724
+ const promoted: LuaTableField[] = fields.map((f) => {
725
+ if (f.type !== "ExpressionField") return f;
726
+ const key = deriveFieldName(f.value);
727
+ if (key) {
728
+ return {
729
+ type: "PropField",
730
+ key,
731
+ value: f.value,
732
+ ctx: f.ctx,
733
+ } as LuaTableField;
734
+ }
735
+ return f;
736
+ });
737
+ return { type: "TableConstructor", fields: promoted, ctx };
738
+ }
739
+
740
+ function fieldsToGroupByEntries(fields: LuaTableField[]): LuaGroupByEntry[] {
741
+ return fields.map((f) => {
742
+ switch (f.type) {
743
+ case "PropField":
744
+ return { expr: f.value, alias: f.key };
745
+ case "ExpressionField":
746
+ return { expr: f.value };
747
+ case "DynamicField":
748
+ return { expr: f.value };
749
+ }
750
+ });
751
+ }
752
+
753
+ type FromSource =
754
+ | { kind: "single"; objectVariable?: string; expression: LuaExpression }
755
+ | { kind: "cross"; sources: { name: string; expression: LuaExpression }[] };
756
+
757
+ function fromFieldsToSource(fields: LuaTableField[], ctx: ASTCtx): FromSource {
758
+ if (fields.length === 1) {
759
+ const f = fields[0];
760
+ if (f.type === "ExpressionField") {
761
+ return { kind: "single", expression: f.value };
762
+ }
763
+ if (f.type === "PropField") {
764
+ return { kind: "single", objectVariable: f.key, expression: f.value };
765
+ }
766
+ }
767
+
768
+ const sources: { name: string; expression: LuaExpression }[] = [];
769
+ for (const f of fields) {
770
+ if (f.type !== "PropField") {
771
+ throw new LuaRuntimeError("Multi-source 'from' requires named sources", {
772
+ ref: ctx,
773
+ } as any);
774
+ }
775
+ sources.push({ name: f.key, expression: f.value });
776
+ }
777
+ return { kind: "cross", sources };
778
+ }
779
+
780
+ async function normalizeToArray(
781
+ collection: LuaValue,
782
+ sf: LuaStackFrame,
783
+ ): Promise<any[]> {
784
+ if (collection instanceof LuaTable && collection.empty()) {
785
+ return [];
786
+ }
787
+ if (collection instanceof LuaTable) {
788
+ if (collection.length > 0) {
789
+ const arr: any[] = [];
790
+ for (let i = 1; i <= collection.length; i++) {
791
+ arr.push(collection.rawGet(i));
792
+ }
793
+ return arr;
794
+ }
795
+ return [collection];
796
+ }
797
+ if (Array.isArray(collection)) {
798
+ return collection;
799
+ }
800
+ if (
801
+ typeof collection === "object" &&
802
+ collection !== null &&
803
+ "query" in collection &&
804
+ typeof (collection as any).query === "function"
805
+ ) {
806
+ const allItems = await (collection as any).query(
807
+ { distinct: false },
808
+ new LuaEnv(),
809
+ sf,
810
+ );
811
+ return Array.isArray(allItems) ? allItems : [allItems];
812
+ }
813
+ const jsVal = luaValueToJS(collection, sf);
814
+ return Array.isArray(jsVal) ? jsVal : [jsVal];
815
+ }
816
+
817
+ async function evalCrossJoinSources(
818
+ sources: { name: string; expression: LuaExpression }[],
819
+ env: LuaEnv,
820
+ sf: LuaStackFrame,
821
+ ctx: ASTCtx,
822
+ ): Promise<LuaTable[]> {
823
+ // Evaluate each source and normalize to arrays
824
+ const arrays: { name: string; items: any[] }[] = [];
825
+ for (const src of sources) {
826
+ const val = await evalExpression(src.expression, env, sf);
827
+ if (val === null || val === undefined) {
828
+ throw new LuaRuntimeError(
829
+ `Cross-join source '${src.name}' is nil`,
830
+ sf.withCtx(ctx),
831
+ );
832
+ }
833
+ const items = await normalizeToArray(val, sf);
834
+ arrays.push({ name: src.name, items });
835
+ }
836
+
837
+ // Cartesian product
838
+ let product: Record<string, any>[] = [{}];
839
+ for (const { name, items } of arrays) {
840
+ const newProduct: Record<string, any>[] = [];
841
+ for (const combo of product) {
842
+ for (const item of items) {
843
+ newProduct.push({ ...combo, [name]: item });
844
+ }
845
+ }
846
+ product = newProduct;
847
+ }
848
+
849
+ // Convert each combination to a `LuaTable` row
850
+ return product.map((combo) => {
851
+ const row = new LuaTable();
852
+ for (const key in combo) {
853
+ void row.rawSet(key, combo[key]);
854
+ }
855
+ return row;
856
+ });
857
+ }
858
+
612
859
  export function evalExpression(
613
860
  e: LuaExpression,
614
861
  env: LuaEnv,
@@ -642,14 +889,7 @@ export function evalExpression(
642
889
  if (b.operator === "and") {
643
890
  return evalLogical("and", b.left, b.right, env, sf);
644
891
  }
645
- return evalBinaryWithLR(
646
- b.operator,
647
- b.left,
648
- b.right,
649
- b.ctx,
650
- env,
651
- sf,
652
- );
892
+ return evalBinaryWithLR(b.operator, b.left, b.right, b.ctx, env, sf);
653
893
  }
654
894
  case "Unary": {
655
895
  const u = asUnary(e);
@@ -673,59 +913,53 @@ export function evalExpression(
673
913
  const applyTyped = (typed: TypedValue) => {
674
914
  const arg = singleResult(typed.value);
675
915
 
676
- return unaryWithMeta(
677
- arg,
678
- "__unm",
679
- u.ctx,
680
- sf,
681
- () => {
682
- // Numeric-string coercion for unary minus
683
- if (typeof arg === "string") {
684
- const n = coerceToNumber(arg);
685
- if (n === null) {
686
- throw new LuaRuntimeError(
687
- "attempt to unm a 'string' with a 'string'",
688
- sf.withCtx(u.ctx),
689
- );
690
- }
691
- if (n === 0) {
692
- return 0;
693
- }
694
- return -n;
695
- }
696
-
697
- const plain = untagNumber(arg);
698
- if (typeof plain !== "number") {
916
+ return unaryWithMeta(arg, "__unm", u.ctx, sf, () => {
917
+ // Numeric-string coercion for unary minus
918
+ if (typeof arg === "string") {
919
+ const n = coerceToNumber(arg);
920
+ if (n === null) {
699
921
  throw new LuaRuntimeError(
700
- "attempt to perform arithmetic on a table value",
922
+ "attempt to unm a 'string' with a 'string'",
701
923
  sf.withCtx(u.ctx),
702
924
  );
703
925
  }
926
+ if (n === 0) {
927
+ return 0;
928
+ }
929
+ return -n;
930
+ }
704
931
 
705
- const argType = isTaggedFloat(arg)
706
- ? "float"
707
- : astNumberKind(u.argument);
932
+ const plain = untagNumber(arg);
933
+ if (typeof plain !== "number") {
934
+ throw new LuaRuntimeError(
935
+ "attempt to perform arithmetic on a table value",
936
+ sf.withCtx(u.ctx),
937
+ );
938
+ }
708
939
 
709
- const out = luaUnaryMinus(plain, argType);
940
+ const argType = isTaggedFloat(arg)
941
+ ? "float"
942
+ : astNumberKind(u.argument);
710
943
 
711
- // If the operand is a float-tagged boxed number, unary
712
- // minus must keep the result float-typed.
713
- if (isTaggedFloat(arg)) {
714
- if (out === 0) {
715
- return makeLuaZero(out, "float");
716
- }
717
- return makeLuaFloat(out);
718
- }
944
+ const out = luaUnaryMinus(plain, argType);
719
945
 
720
- // Preserve numeric kind for zero results
946
+ // If the operand is a float-tagged boxed number, unary
947
+ // minus must keep the result float-typed.
948
+ if (isTaggedFloat(arg)) {
721
949
  if (out === 0) {
722
- const outType = argType ?? inferNumericType(plain);
723
- return makeLuaZero(out, outType);
950
+ return makeLuaZero(out, "float");
724
951
  }
952
+ return makeLuaFloat(out);
953
+ }
725
954
 
726
- return out;
727
- },
728
- );
955
+ // Preserve numeric kind for zero results
956
+ if (out === 0) {
957
+ const outType = argType ?? inferNumericType(plain);
958
+ return makeLuaZero(out, outType);
959
+ }
960
+
961
+ return out;
962
+ });
729
963
  };
730
964
 
731
965
  return rpThen(tv as any, applyTyped);
@@ -740,35 +974,29 @@ export function evalExpression(
740
974
  }
741
975
  case "~": {
742
976
  const arg = singleResult(value);
743
- return unaryWithMeta(
744
- arg,
745
- "__bnot",
746
- u.ctx,
747
- sf,
748
- () => {
749
- const intVal = toInteger(arg);
750
- if (intVal === null) {
751
- if (typeof arg === "string") {
752
- throw new LuaRuntimeError(
753
- `attempt to perform bitwise operation on a string value (constant '${arg}')`,
754
- sf.withCtx(u.ctx),
755
- );
756
- }
757
- const t = luaTypeName(arg);
758
- if (t === "number") {
759
- throw new LuaRuntimeError(
760
- `number has no integer representation`,
761
- sf.withCtx(u.ctx),
762
- );
763
- }
977
+ return unaryWithMeta(arg, "__bnot", u.ctx, sf, () => {
978
+ const intVal = toInteger(arg);
979
+ if (intVal === null) {
980
+ if (typeof arg === "string") {
764
981
  throw new LuaRuntimeError(
765
- `attempt to perform bitwise operation on a ${t} value`,
982
+ `attempt to perform bitwise operation on a string value (constant '${arg}')`,
766
983
  sf.withCtx(u.ctx),
767
984
  );
768
985
  }
769
- return ~intVal;
770
- },
771
- );
986
+ const t = luaTypeName(arg);
987
+ if (t === "number") {
988
+ throw new LuaRuntimeError(
989
+ `number has no integer representation`,
990
+ sf.withCtx(u.ctx),
991
+ );
992
+ }
993
+ throw new LuaRuntimeError(
994
+ `attempt to perform bitwise operation on a ${t} value`,
995
+ sf.withCtx(u.ctx),
996
+ );
997
+ }
998
+ return ~intVal;
999
+ });
772
1000
  }
773
1001
  case "#": {
774
1002
  return luaLengthOp(singleResult(value), u.ctx, sf);
@@ -792,31 +1020,63 @@ export function evalExpression(
792
1020
  }
793
1021
  case "TableConstructor": {
794
1022
  const tc = asTableConstructor(e);
795
- return Promise.resolve().then(async () => {
796
- const table = new LuaTable();
797
- // Expression fields assign consecutive integer keys starting
798
- // at 1 and advance even when the value is `nil`.
799
- let nextArrayIndex = 1;
800
- for (const field of tc.fields) {
1023
+ const table = new LuaTable();
1024
+ // Expression fields assign consecutive integer keys starting
1025
+ // at 1 and advance even when the value is `nil`.
1026
+ let nextArrayIndex = 1;
1027
+
1028
+ const processField = (
1029
+ fieldIdx: number,
1030
+ ): LuaTable | Promise<LuaTable> => {
1031
+ for (let fi = fieldIdx; fi < tc.fields.length; fi++) {
1032
+ const field = tc.fields[fi];
801
1033
  switch (field.type) {
802
1034
  case "PropField": {
803
- const value = await evalExpression(field.value, env, sf);
804
- table.set(field.key, singleResult(value), sf);
1035
+ const value = evalExpression(field.value, env, sf);
1036
+ if (isPromise(value)) {
1037
+ return (value as Promise<any>).then((v) => {
1038
+ void table.set(field.key, singleResult(v), sf);
1039
+ return processField(fi + 1);
1040
+ });
1041
+ }
1042
+ void table.set(field.key, singleResult(value), sf);
805
1043
  break;
806
1044
  }
807
1045
  case "DynamicField": {
808
- const key = await evalExpression(field.key, env, sf);
809
- const value = await evalExpression(field.value, env, sf);
810
- table.set(singleResult(key), singleResult(value), sf);
1046
+ const key = evalExpression(field.key, env, sf);
1047
+ const val = evalExpression(field.value, env, sf);
1048
+ if (isPromise(key) || isPromise(val)) {
1049
+ return rpThen(key, (k) =>
1050
+ rpThen(val, (v) => {
1051
+ void table.set(singleResult(k), singleResult(v), sf);
1052
+ return processField(fi + 1);
1053
+ }),
1054
+ ) as Promise<LuaTable>;
1055
+ }
1056
+ void table.set(singleResult(key), singleResult(val), sf);
811
1057
  break;
812
1058
  }
813
1059
  case "ExpressionField": {
814
- const value = await evalExpression(field.value, env, sf);
815
-
1060
+ const value = evalExpression(field.value, env, sf);
1061
+ if (isPromise(value)) {
1062
+ return (value as Promise<any>).then((v) => {
1063
+ if (v instanceof LuaMultiRes) {
1064
+ const flat = v.flatten();
1065
+ for (let j = 0; j < flat.values.length; j++) {
1066
+ table.rawSetArrayIndex(nextArrayIndex, flat.values[j]);
1067
+ nextArrayIndex++;
1068
+ }
1069
+ } else {
1070
+ table.rawSetArrayIndex(nextArrayIndex, singleResult(v));
1071
+ nextArrayIndex++;
1072
+ }
1073
+ return processField(fi + 1);
1074
+ });
1075
+ }
816
1076
  if (value instanceof LuaMultiRes) {
817
1077
  const flat = value.flatten();
818
- for (let i = 0; i < flat.values.length; i++) {
819
- table.rawSetArrayIndex(nextArrayIndex, flat.values[i]);
1078
+ for (let j = 0; j < flat.values.length; j++) {
1079
+ table.rawSetArrayIndex(nextArrayIndex, flat.values[j]);
820
1080
  nextArrayIndex++;
821
1081
  }
822
1082
  } else {
@@ -828,7 +1088,9 @@ export function evalExpression(
828
1088
  }
829
1089
  }
830
1090
  return table;
831
- });
1091
+ };
1092
+
1093
+ return processField(0);
832
1094
  }
833
1095
  case "FunctionDefinition": {
834
1096
  const fd = asFunctionDef(e);
@@ -838,37 +1100,125 @@ export function evalExpression(
838
1100
  const q = asQueryExpr(e);
839
1101
  const findFromClause = q.clauses.find((c) => c.type === "From");
840
1102
  if (!findFromClause) {
841
- throw new LuaRuntimeError(
842
- "No from clause found",
843
- sf.withCtx(q.ctx),
844
- );
1103
+ throw new LuaRuntimeError("No from clause found", sf.withCtx(q.ctx));
1104
+ }
1105
+ const fromSource = fromFieldsToSource(
1106
+ findFromClause.fields,
1107
+ findFromClause.ctx,
1108
+ );
1109
+
1110
+ if (fromSource.kind === "cross") {
1111
+ // Materialize Cartesian product, then query
1112
+ return (async () => {
1113
+ const rows = await evalCrossJoinSources(
1114
+ fromSource.sources,
1115
+ env,
1116
+ sf,
1117
+ findFromClause.ctx,
1118
+ );
1119
+ const collection: any = toCollection(rows);
1120
+
1121
+ // Build up query object
1122
+ const query: LuaCollectionQuery = {
1123
+ objectVariable: undefined,
1124
+ distinct: true,
1125
+ };
1126
+
1127
+ // Map clauses to query parameters
1128
+ for (const clause of q.clauses) {
1129
+ switch (clause.type) {
1130
+ case "Where": {
1131
+ query.where = clause.expression;
1132
+ break;
1133
+ }
1134
+ case "OrderBy": {
1135
+ query.orderBy = clause.orderBy.map((o) => ({
1136
+ expr: o.expression,
1137
+ desc: o.direction === "desc",
1138
+ nulls: o.nulls,
1139
+ using: o.using,
1140
+ }));
1141
+ break;
1142
+ }
1143
+ case "Select": {
1144
+ query.select = fieldsToExpression(clause.fields, clause.ctx);
1145
+ break;
1146
+ }
1147
+ case "Limit": {
1148
+ const limitVal = await evalExpression(clause.limit, env, sf);
1149
+ query.limit = Number(limitVal);
1150
+ if (clause.offset) {
1151
+ const offsetVal = await evalExpression(
1152
+ clause.offset,
1153
+ env,
1154
+ sf,
1155
+ );
1156
+ query.offset = Number(offsetVal);
1157
+ }
1158
+ break;
1159
+ }
1160
+ case "Offset": {
1161
+ const offsetVal = await evalExpression(
1162
+ clause.offset,
1163
+ env,
1164
+ sf,
1165
+ );
1166
+ query.offset = Number(offsetVal);
1167
+ break;
1168
+ }
1169
+ case "GroupBy": {
1170
+ query.groupBy = fieldsToGroupByEntries(clause.fields);
1171
+ break;
1172
+ }
1173
+ case "Having": {
1174
+ query.having = clause.expression;
1175
+ break;
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ return (collection as any)
1181
+ .query(query, env, sf, globalThis.client?.config)
1182
+ .then(jsToLuaValue);
1183
+ })();
845
1184
  }
846
- const objectVariable = findFromClause.name;
847
- const objectExpression = findFromClause.expression;
1185
+
1186
+ // Single-source
1187
+ const { objectVariable, expression: objectExpression } = fromSource;
848
1188
  return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
849
1189
  async (collection: LuaValue) => {
850
1190
  if (!collection) {
851
- throw new LuaRuntimeError(
852
- "Collection is nil",
853
- sf.withCtx(q.ctx),
854
- );
1191
+ throw new LuaRuntimeError("Collection is nil", sf.withCtx(q.ctx));
855
1192
  }
856
- if (collection instanceof LuaTable && collection.empty()) {
857
- // Make sure we're converting an empty result to an array to "query"
858
- collection = [];
859
- } else {
860
- collection = luaValueToJS(collection, sf);
861
- }
862
- // Check if collection is a queryable collection
863
- if (!isQueryable(collection)) {
864
- if (!Array.isArray(collection)) {
865
- throw new LuaRuntimeError(
866
- "Collection does not support query",
867
- sf.withCtx(q.ctx),
868
- );
1193
+
1194
+ // If already a queryable collection (e.g. DataStoreQueryCollection),
1195
+ // use directly - skip all LuaTable/JS conversion.
1196
+ if (
1197
+ typeof collection === "object" &&
1198
+ collection !== null &&
1199
+ "query" in collection &&
1200
+ typeof (collection as any).query === "function"
1201
+ ) {
1202
+ // Already queryable, use as-is
1203
+ } else if (collection instanceof LuaTable && collection.empty()) {
1204
+ // Empty table → empty array
1205
+ collection = toCollection([]);
1206
+ } else if (collection instanceof LuaTable) {
1207
+ if (collection.length > 0) {
1208
+ // Array-like table: extract array items, keep as LuaTables
1209
+ const arr: any[] = [];
1210
+ for (let i = 1; i <= collection.length; i++) {
1211
+ arr.push(collection.rawGet(i));
1212
+ }
1213
+ collection = toCollection(arr);
1214
+ } else {
1215
+ // Record-like table (no array part): treat as singleton
1216
+ collection = toCollection([collection]);
869
1217
  }
870
- collection = new ArrayQueryCollection(collection);
1218
+ } else {
1219
+ collection = toCollection(luaValueToJS(collection, sf));
871
1220
  }
1221
+
872
1222
  // Build up query object
873
1223
  const query: LuaCollectionQuery = {
874
1224
  objectVariable,
@@ -886,11 +1236,13 @@ export function evalExpression(
886
1236
  query.orderBy = clause.orderBy.map((o) => ({
887
1237
  expr: o.expression,
888
1238
  desc: o.direction === "desc",
1239
+ nulls: o.nulls,
1240
+ using: o.using,
889
1241
  }));
890
1242
  break;
891
1243
  }
892
1244
  case "Select": {
893
- query.select = clause.expression;
1245
+ query.select = fieldsToExpression(clause.fields, clause.ctx);
894
1246
  break;
895
1247
  }
896
1248
  case "Limit": {
@@ -906,12 +1258,30 @@ export function evalExpression(
906
1258
  }
907
1259
  break;
908
1260
  }
1261
+ case "Offset": {
1262
+ const offsetVal = await evalExpression(
1263
+ clause.offset,
1264
+ env,
1265
+ sf,
1266
+ );
1267
+ query.offset = Number(offsetVal);
1268
+ break;
1269
+ }
1270
+ case "GroupBy": {
1271
+ query.groupBy = fieldsToGroupByEntries(clause.fields);
1272
+ break;
1273
+ }
1274
+ case "Having": {
1275
+ query.having = clause.expression;
1276
+ break;
1277
+ }
909
1278
  }
910
1279
  }
911
1280
 
912
- return (collection as Queryable).query(query, env, sf).then(
913
- jsToLuaValue,
914
- );
1281
+ // Always use the possibly-wrapped collection
1282
+ return (collection as any)
1283
+ .query(query, env, sf, globalThis.client?.config)
1284
+ .then(jsToLuaValue);
915
1285
  },
916
1286
  );
917
1287
  }
@@ -964,13 +1334,10 @@ function evalPrefixExpression(
964
1334
  return luaGet(table, key, ta.ctx, sf);
965
1335
  }
966
1336
 
967
- return rpThen(
968
- objV,
969
- (obj) =>
970
- rpThen(
971
- keyV,
972
- (key) => luaGet(singleResult(obj), singleResult(key), ta.ctx, sf),
973
- ),
1337
+ return rpThen(objV, (obj) =>
1338
+ rpThen(keyV, (key) =>
1339
+ luaGet(singleResult(obj), singleResult(key), ta.ctx, sf),
1340
+ ),
974
1341
  );
975
1342
  }
976
1343
 
@@ -980,50 +1347,88 @@ function evalPrefixExpression(
980
1347
  // Sync-first: evaluate object; avoid Promise when object is sync.
981
1348
  const objV = evalPrefixExpression(pa.object, env, sf);
982
1349
  if (!isPromise(objV)) {
983
- return luaGet(objV, pa.property, pa.ctx, sf);
1350
+ return luaGet(singleResult(objV), pa.property, pa.ctx, sf);
984
1351
  }
985
- return rpThen(objV, (obj) => luaGet(obj, pa.property, pa.ctx, sf));
1352
+ return rpThen(objV, (obj) =>
1353
+ luaGet(singleResult(obj), pa.property, pa.ctx, sf),
1354
+ );
986
1355
  }
987
1356
 
988
1357
  case "FunctionCall": {
989
1358
  const fc = asFunctionCall(e);
990
- const prefixValue = evalPrefixExpression(fc.prefix, env, sf);
991
- if (prefixValue === null || prefixValue === undefined) {
992
- const nilMsg = fc.prefix.type === "Variable"
993
- ? `attempt to call a nil value (global '${
994
- asVariable(fc.prefix).name
995
- }')`
996
- : `attempt to call a nil value`;
1359
+
1360
+ // `order by` inside function arguments is only valid for aggregate
1361
+ // calls evaluated by the query engine
1362
+ if (fc.orderBy && fc.orderBy.length > 0) {
997
1363
  throw new LuaRuntimeError(
998
- nilMsg,
999
- sf.withCtx(fc.prefix.ctx),
1364
+ `'order by' is not allowed in non-aggregate function calls`,
1365
+ sf.withCtx(fc.ctx),
1000
1366
  );
1001
1367
  }
1002
1368
 
1003
- let selfArgs: LuaValue[] = [];
1369
+ const prefixValue = evalPrefixExpression(fc.prefix, env, sf);
1370
+ if (prefixValue === null || prefixValue === undefined) {
1371
+ const nilMsg =
1372
+ fc.prefix.type === "Variable"
1373
+ ? `attempt to call a nil value (global '${
1374
+ asVariable(fc.prefix).name
1375
+ }')`
1376
+ : `attempt to call a nil value`;
1377
+ throw new LuaRuntimeError(nilMsg, sf.withCtx(fc.prefix.ctx));
1378
+ }
1379
+
1380
+ // Fast path: non-method call with sync prefix
1381
+ if (!fc.name && !isPromise(prefixValue)) {
1382
+ const argsVal = evalExpressions(fc.args, env, sf);
1383
+ if (!isPromise(argsVal)) {
1384
+ return luaCall(prefixValue, argsVal as LuaValue[], fc.ctx, sf);
1385
+ }
1386
+ return (argsVal as Promise<LuaValue[]>).then((args) =>
1387
+ luaCall(prefixValue, args, fc.ctx, sf),
1388
+ );
1389
+ }
1004
1390
 
1005
1391
  const handleFunctionCall = (
1006
1392
  calleeVal: LuaValue,
1393
+ selfArgs: LuaValue[],
1007
1394
  ): LuaValue | Promise<LuaValue> => {
1008
1395
  // Normal argument handling for hello:there(a, b, c) type calls
1009
1396
  if (fc.name) {
1010
- selfArgs = [calleeVal];
1397
+ const self = calleeVal;
1011
1398
  calleeVal = luaIndexValue(calleeVal, fc.name, sf);
1012
1399
 
1013
1400
  if (isPromise(calleeVal)) {
1014
- return (calleeVal as Promise<any>).then(handleFunctionCall);
1401
+ return (calleeVal as Promise<any>).then((cv) =>
1402
+ handleFunctionCall(cv, [self]),
1403
+ );
1015
1404
  }
1405
+ selfArgs = [self];
1016
1406
  }
1017
1407
 
1018
1408
  const argsVal = evalExpressions(fc.args, env, sf);
1019
-
1020
- const thenCall = (args: LuaValue[]) =>
1021
- luaCall(calleeVal, [...selfArgs, ...args], fc.ctx, sf);
1022
-
1023
- return rpThen(argsVal, thenCall);
1409
+ if (!isPromise(argsVal)) {
1410
+ const allArgs =
1411
+ selfArgs.length > 0
1412
+ ? [...selfArgs, ...(argsVal as LuaValue[])]
1413
+ : (argsVal as LuaValue[]);
1414
+ return luaCall(calleeVal, allArgs, fc.ctx, sf);
1415
+ }
1416
+ return (argsVal as Promise<LuaValue[]>).then((args) =>
1417
+ luaCall(
1418
+ calleeVal,
1419
+ selfArgs.length > 0 ? [...selfArgs, ...args] : args,
1420
+ fc.ctx,
1421
+ sf,
1422
+ ),
1423
+ );
1024
1424
  };
1025
1425
 
1026
- return rpThen(prefixValue, handleFunctionCall);
1426
+ if (isPromise(prefixValue)) {
1427
+ return (prefixValue as Promise<any>).then((pv) =>
1428
+ handleFunctionCall(pv, []),
1429
+ );
1430
+ }
1431
+ return handleFunctionCall(prefixValue, []);
1027
1432
  }
1028
1433
 
1029
1434
  default: {
@@ -1173,22 +1578,58 @@ function evalBinaryWithLR(
1173
1578
  : undefined;
1174
1579
  const leftVal = evalExpression(leftExpr, env, sf);
1175
1580
 
1176
- const applyLeft = (lv: any) => {
1581
+ // Sync-first fast path: avoid closure allocation when both operands are sync
1582
+ if (!isPromise(leftVal)) {
1177
1583
  const rightVal = evalExpression(rightExpr, env, sf);
1178
- const applyRight = (rv: any) => {
1584
+ if (!isPromise(rightVal)) {
1179
1585
  return luaOp(
1180
1586
  op,
1181
- singleResult(lv),
1587
+ singleResult(leftVal),
1588
+ singleResult(rightVal),
1589
+ leftType,
1590
+ rightType,
1591
+ ctx,
1592
+ sf,
1593
+ );
1594
+ }
1595
+ return (rightVal as Promise<any>).then((rv) =>
1596
+ luaOp(
1597
+ op,
1598
+ singleResult(leftVal),
1182
1599
  singleResult(rv),
1183
1600
  leftType,
1184
1601
  rightType,
1185
1602
  ctx,
1186
1603
  sf,
1604
+ ),
1605
+ );
1606
+ }
1607
+
1608
+ return (leftVal as Promise<any>).then((lv) => {
1609
+ const rightVal = evalExpression(rightExpr, env, sf);
1610
+ if (!isPromise(rightVal)) {
1611
+ return luaOp(
1612
+ op,
1613
+ singleResult(lv),
1614
+ singleResult(rightVal),
1615
+ leftType,
1616
+ rightType,
1617
+ ctx,
1618
+ sf,
1187
1619
  );
1188
- };
1189
- return rpThen(rightVal, applyRight);
1190
- };
1191
- return rpThen(leftVal, applyLeft);
1620
+ }
1621
+ return (rightVal as Promise<any>).then((rv) =>
1622
+ luaOp(
1623
+ op,
1624
+ singleResult(lv),
1625
+ singleResult(rv),
1626
+ leftType,
1627
+ rightType,
1628
+ ctx,
1629
+ sf,
1630
+ ),
1631
+ );
1632
+ });
1192
1633
  }
1193
1634
 
1194
1635
  function createBitwiseError(
@@ -1271,10 +1712,7 @@ function luaEqWithMetamethod(
1271
1712
  }
1272
1713
 
1273
1714
  const ty = luaTypeName(mm);
1274
- throw new LuaRuntimeError(
1275
- `attempt to call a ${ty} value`,
1276
- sf.withCtx(ctx),
1277
- );
1715
+ throw new LuaRuntimeError(`attempt to call a ${ty} value`, sf.withCtx(ctx));
1278
1716
  };
1279
1717
 
1280
1718
  // Try left __eq first, then right.
@@ -1331,11 +1769,7 @@ function luaRelWithMetamethod(
1331
1769
  * - for JavaScript arrays return length,
1332
1770
  * - throw error otherwise.
1333
1771
  */
1334
- function luaLengthOp(
1335
- val: any,
1336
- ctx: ASTCtx,
1337
- sf: LuaStackFrame,
1338
- ): LuaValue {
1772
+ function luaLengthOp(val: any, ctx: ASTCtx, sf: LuaStackFrame): LuaValue {
1339
1773
  // Strings: ignore `__len`
1340
1774
  if (typeof val === "string") {
1341
1775
  return val.length;
@@ -1382,22 +1816,26 @@ function evalExpressions(
1382
1816
  env: LuaEnv,
1383
1817
  sf: LuaStackFrame,
1384
1818
  ): Promise<LuaValue[]> | LuaValue[] {
1385
- // Evaluate all arguments first (sync-first); do not allocate a Promise if all are sync.
1386
- const parts = es.map((arg) => evalExpression(arg, env, sf));
1819
+ const len = es.length;
1820
+ if (len === 0) return [];
1821
+
1822
+ // Evaluate all arguments (sync-first); avoid .map() closure overhead
1823
+ const parts = new Array(len);
1824
+ for (let i = 0; i < len; i++) {
1825
+ parts[i] = evalExpression(es[i], env, sf);
1826
+ }
1387
1827
  const argsVal = rpAll(parts);
1388
1828
 
1389
1829
  // In Lua multi-returns propagate only in tail position of an expression list.
1390
1830
  const finalize = (argsResolved: any[]) => {
1391
- if (argsResolved.length === 0) {
1392
- return [];
1393
- }
1394
1831
  const out: LuaValue[] = [];
1832
+ const lastIdx = argsResolved.length - 1;
1395
1833
  // All but last expression produce a single value
1396
- for (let i = 0; i < argsResolved.length - 1; i++) {
1834
+ for (let i = 0; i < lastIdx; i++) {
1397
1835
  out.push(singleResult(argsResolved[i]));
1398
1836
  }
1399
1837
  // Last expression preserves multiple results
1400
- const last = argsResolved[argsResolved.length - 1];
1838
+ const last = argsResolved[lastIdx];
1401
1839
  if (last instanceof LuaMultiRes) {
1402
1840
  out.push(...last.flatten().values);
1403
1841
  } else {
@@ -1410,9 +1848,9 @@ function evalExpressions(
1410
1848
  }
1411
1849
 
1412
1850
  type EvalBlockResult =
1413
- | void
1851
+ | undefined
1414
1852
  | ControlSignal
1415
- | Promise<void | ControlSignal>;
1853
+ | Promise<undefined | ControlSignal>;
1416
1854
 
1417
1855
  function runStatementsNoGoto(
1418
1856
  stmts: LuaStatement[],
@@ -1420,17 +1858,12 @@ function runStatementsNoGoto(
1420
1858
  sf: LuaStackFrame,
1421
1859
  returnOnReturn: boolean,
1422
1860
  startIdx: number,
1423
- ): void | ControlSignal | Promise<void | ControlSignal> {
1861
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> {
1424
1862
  const processFrom = (
1425
1863
  idx: number,
1426
- ): void | ControlSignal | Promise<void | ControlSignal> => {
1864
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
1427
1865
  for (let i = idx; i < stmts.length; i++) {
1428
- const result = evalStatement(
1429
- stmts[i],
1430
- execEnv,
1431
- sf,
1432
- returnOnReturn,
1433
- );
1866
+ const result = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1434
1867
  if (isPromise(result)) {
1435
1868
  return (result as Promise<any>).then((res) => {
1436
1869
  if (res !== undefined) {
@@ -1482,9 +1915,8 @@ function withCloseBoundary(
1482
1915
  };
1483
1916
 
1484
1917
  const onRejected = (e: any) => {
1485
- const errObj: LuaValue = e instanceof LuaRuntimeError
1486
- ? e.message
1487
- : (e?.message ?? String(e));
1918
+ const errObj: LuaValue =
1919
+ e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
1488
1920
  const r = luaCloseFromMark(sf, mark, errObj);
1489
1921
  if (isPromise(r)) {
1490
1922
  return (r as Promise<void>).then(() => {
@@ -1527,9 +1959,7 @@ function evalBlockNoClose(
1527
1959
  if (fnHasGotos === true && !hasLabelHere && !hasGotoFlag) {
1528
1960
  const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
1529
1961
  const stmts = b.statements;
1530
- const runFrom = (
1531
- i: number,
1532
- ): EvalBlockResult => {
1962
+ const runFrom = (i: number): EvalBlockResult => {
1533
1963
  for (; i < stmts.length; i++) {
1534
1964
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1535
1965
  if (isPromise(r)) {
@@ -1574,9 +2004,7 @@ function evalBlockNoClose(
1574
2004
  const execEnv = b.needsEnv === true ? new LuaEnv(env) : env;
1575
2005
  const stmts = b.statements;
1576
2006
 
1577
- const runFrom = (
1578
- i: number,
1579
- ): EvalBlockResult => {
2007
+ const runFrom = (i: number): EvalBlockResult => {
1580
2008
  for (; i < stmts.length; i++) {
1581
2009
  const r = evalStatement(stmts[i], execEnv, sf, returnOnReturn);
1582
2010
  if (isPromise(r)) {
@@ -1621,22 +2049,25 @@ export function evalStatement(
1621
2049
  env: LuaEnv,
1622
2050
  sf: LuaStackFrame,
1623
2051
  returnOnReturn = false,
1624
- ): void | ControlSignal | Promise<void | ControlSignal> {
2052
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> {
1625
2053
  switch (s.type) {
1626
2054
  case "Assignment": {
1627
2055
  const a = asAssignment(s);
1628
2056
  const valuesRP = evalExpressions(a.expressions, env, sf);
1629
- const lvaluesRP = evalPromiseValues(a.variables
1630
- .map((lval) => evalLValue(lval, env, sf)));
2057
+ const lvaluesRP = evalPromiseValues(
2058
+ a.variables.map((lval) => evalLValue(lval, env, sf)),
2059
+ );
1631
2060
 
1632
2061
  const apply = (values: LuaValue[], lvalues: { env: any; key: any }[]) => {
2062
+ // Create the error-reporting frame once, not per-lvalue
2063
+ let errSf: LuaStackFrame | undefined;
1633
2064
  const ps: Promise<any>[] = [];
1634
2065
  for (let i = 0; i < lvalues.length; i++) {
1635
2066
  const r = luaSet(
1636
2067
  lvalues[i].env,
1637
2068
  lvalues[i].key,
1638
2069
  values[i],
1639
- sf.withCtx(a.ctx),
2070
+ errSf || (errSf = sf.withCtx(a.ctx)),
1640
2071
  );
1641
2072
 
1642
2073
  if (isPromise(r)) {
@@ -1650,29 +2081,22 @@ export function evalStatement(
1650
2081
  };
1651
2082
 
1652
2083
  if (!isPromise(valuesRP) && !isPromise(lvaluesRP)) {
1653
- return apply(
1654
- valuesRP as LuaValue[],
1655
- lvaluesRP as LuaLValueContainer[],
1656
- );
2084
+ return apply(valuesRP as LuaValue[], lvaluesRP as LuaLValueContainer[]);
1657
2085
  }
1658
- if (
1659
- isPromise(valuesRP) && !isPromise(lvaluesRP)
1660
- ) {
2086
+ if (isPromise(valuesRP) && !isPromise(lvaluesRP)) {
1661
2087
  return (valuesRP as Promise<LuaValue[]>).then((values: LuaValue[]) =>
1662
- apply(values, lvaluesRP as LuaLValueContainer[])
2088
+ apply(values, lvaluesRP as LuaLValueContainer[]),
1663
2089
  );
1664
2090
  }
1665
- if (
1666
- !isPromise(valuesRP) && isPromise(lvaluesRP)
1667
- ) {
2091
+ if (!isPromise(valuesRP) && isPromise(lvaluesRP)) {
1668
2092
  return (lvaluesRP as Promise<any[]>).then((lvalues: any[]) =>
1669
- apply(valuesRP as LuaValue[], lvalues)
2093
+ apply(valuesRP as LuaValue[], lvalues),
1670
2094
  );
1671
2095
  }
1672
2096
  return (valuesRP as Promise<LuaValue[]>).then((values: LuaValue[]) =>
1673
2097
  (lvaluesRP as Promise<any[]>).then((lvalues: any[]) =>
1674
- apply(values, lvalues)
1675
- )
2098
+ apply(values, lvalues),
2099
+ ),
1676
2100
  );
1677
2101
  }
1678
2102
  case "Local": {
@@ -1721,10 +2145,7 @@ export function evalStatement(
1721
2145
 
1722
2146
  const bindAvailable = () => {
1723
2147
  while (boundCount < l.names.length && boundCount < out.length) {
1724
- bindOne(
1725
- l.names[boundCount],
1726
- out[boundCount] ?? null,
1727
- );
2148
+ bindOne(l.names[boundCount], out[boundCount] ?? null);
1728
2149
  boundCount++;
1729
2150
  }
1730
2151
  };
@@ -1773,7 +2194,7 @@ export function evalStatement(
1773
2194
  return rpThen(rp, onValue) as any;
1774
2195
  };
1775
2196
 
1776
- return runFrom(0);
2197
+ return runFrom(0) as undefined | Promise<undefined>;
1777
2198
  }
1778
2199
  case "Semicolon": {
1779
2200
  return;
@@ -1804,9 +2225,8 @@ export function evalStatement(
1804
2225
  try {
1805
2226
  out = evalBlockNoClose(b, env, sf, returnOnReturn);
1806
2227
  } catch (e: any) {
1807
- const errObj: LuaValue = e instanceof LuaRuntimeError
1808
- ? e.message
1809
- : (e?.message ?? String(e));
2228
+ const errObj: LuaValue =
2229
+ e instanceof LuaRuntimeError ? e.message : (e?.message ?? String(e));
1810
2230
  const r = luaCloseFromMark(sf, mark, errObj);
1811
2231
  if (isPromise(r)) {
1812
2232
  return (r as Promise<void>).then(() => {
@@ -1825,10 +2245,7 @@ export function evalStatement(
1825
2245
 
1826
2246
  const runFrom = (
1827
2247
  i: number,
1828
- ):
1829
- | void
1830
- | ControlSignal
1831
- | Promise<void | ControlSignal> => {
2248
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
1832
2249
  if (i >= conds.length) {
1833
2250
  if (iff.elseBlock) {
1834
2251
  return evalStatement(iff.elseBlock, env, sf, returnOnReturn);
@@ -1855,132 +2272,84 @@ export function evalStatement(
1855
2272
  case "While": {
1856
2273
  const w = asWhile(s);
1857
2274
 
1858
- const runAsync = async (): Promise<void | ControlSignal> => {
2275
+ // Sync-first loop that re-enters sync mode after each async iteration
2276
+ const runSyncFirst = ():
2277
+ | undefined
2278
+ | ControlSignal
2279
+ | Promise<undefined | ControlSignal> => {
1859
2280
  while (true) {
1860
- const c = await evalExpression(w.condition, env, sf);
1861
- if (!luaTruthy(c)) {
1862
- break;
2281
+ const c = evalExpression(w.condition, env, sf);
2282
+ if (isPromise(c)) {
2283
+ return (c as Promise<any>).then((cv) => {
2284
+ if (!luaTruthy(cv)) return;
2285
+ return rpThen(
2286
+ evalStatement(w.block, env, sf, returnOnReturn),
2287
+ (res) => {
2288
+ if (res !== undefined) {
2289
+ return isBreakSignal(res) ? undefined : res;
2290
+ }
2291
+ return runSyncFirst();
2292
+ },
2293
+ );
2294
+ });
1863
2295
  }
2296
+ if (!luaTruthy(c)) break;
1864
2297
  const r = evalStatement(w.block, env, sf, returnOnReturn);
1865
- const res = isPromise(r) ? await r : r;
1866
- if (res !== undefined) {
1867
- if (isBreakSignal(res)) {
1868
- break;
1869
- }
1870
- return res;
2298
+ if (isPromise(r)) {
2299
+ return (r as Promise<any>).then((res) => {
2300
+ if (res !== undefined) {
2301
+ return isBreakSignal(res) ? undefined : res;
2302
+ }
2303
+ return runSyncFirst();
2304
+ });
2305
+ }
2306
+ if (r !== undefined) {
2307
+ if (isBreakSignal(r)) break;
2308
+ return r;
1871
2309
  }
1872
2310
  }
1873
2311
  return;
1874
2312
  };
1875
2313
 
1876
- while (true) {
1877
- const c = evalExpression(w.condition, env, sf);
1878
- if (isPromise(c)) {
1879
- return (c as Promise<any>).then((cv) => {
1880
- if (!luaTruthy(cv)) {
1881
- return;
1882
- }
1883
- try {
1884
- const r = evalStatement(w.block, env, sf, returnOnReturn);
1885
- if (isPromise(r)) {
1886
- return (r as Promise<any>).then((res) => {
1887
- if (res !== undefined) {
1888
- if (isBreakSignal(res)) {
1889
- return;
1890
- }
1891
- return res;
1892
- }
1893
- return runAsync();
1894
- });
1895
- }
1896
- if (r !== undefined) {
1897
- if (isBreakSignal(r)) {
1898
- return;
1899
- }
1900
- return r;
1901
- }
1902
- return runAsync();
1903
- } catch (e: any) {
1904
- throw e;
1905
- }
1906
- });
1907
- }
1908
- if (!luaTruthy(c)) {
1909
- break;
1910
- }
1911
- const r = evalStatement(w.block, env, sf, returnOnReturn);
1912
- if (isPromise(r)) {
1913
- return (r as Promise<any>).then((res) => {
1914
- if (res !== undefined) {
1915
- if (isBreakSignal(res)) {
1916
- return;
1917
- }
1918
- return res;
1919
- }
1920
- return runAsync();
1921
- });
1922
- }
1923
- if (r !== undefined) {
1924
- if (isBreakSignal(r)) {
1925
- break;
1926
- }
1927
- return r;
1928
- }
1929
- }
1930
- return;
2314
+ return runSyncFirst();
1931
2315
  }
1932
2316
  case "Repeat": {
1933
- const r = asRepeat(s);
2317
+ const rep = asRepeat(s);
1934
2318
 
1935
- const runAsync = async (): Promise<void | ControlSignal> => {
2319
+ // Sync-first loop that re-enters sync mode after each async iteration
2320
+ const runSyncFirst = ():
2321
+ | undefined
2322
+ | ControlSignal
2323
+ | Promise<undefined | ControlSignal> => {
1936
2324
  while (true) {
1937
- const rr = evalStatement(r.block, env, sf, returnOnReturn);
1938
- const res = isPromise(rr) ? await rr : rr;
1939
- if (res !== undefined) {
1940
- if (isBreakSignal(res)) {
1941
- break;
1942
- }
1943
- return res;
2325
+ const rr = evalStatement(rep.block, env, sf, returnOnReturn);
2326
+ if (isPromise(rr)) {
2327
+ return (rr as Promise<any>).then((res) => {
2328
+ if (res !== undefined) {
2329
+ return isBreakSignal(res) ? undefined : res;
2330
+ }
2331
+ return rpThen(evalExpression(rep.condition, env, sf), (cv) =>
2332
+ luaTruthy(cv) ? undefined : runSyncFirst(),
2333
+ );
2334
+ });
1944
2335
  }
1945
- const c = await evalExpression(r.condition, env, sf);
1946
- if (luaTruthy(c)) {
1947
- break;
2336
+ if (rr !== undefined) {
2337
+ if (isBreakSignal(rr)) return;
2338
+ return rr;
1948
2339
  }
1949
- }
1950
- return;
1951
- };
1952
2340
 
1953
- while (true) {
1954
- const rr = evalStatement(r.block, env, sf, returnOnReturn);
1955
- if (isPromise(rr)) {
1956
- return (rr as Promise<any>).then((res) => {
1957
- if (res !== undefined) {
1958
- if (isBreakSignal(res)) {
1959
- return;
1960
- }
1961
- return res;
1962
- }
1963
- return runAsync();
1964
- });
1965
- }
1966
- if (rr !== undefined) {
1967
- if (isBreakSignal(rr)) {
1968
- return;
2341
+ const c = evalExpression(rep.condition, env, sf);
2342
+ if (isPromise(c)) {
2343
+ return (c as Promise<any>).then((cv) =>
2344
+ luaTruthy(cv) ? undefined : runSyncFirst(),
2345
+ );
1969
2346
  }
1970
- return rr;
2347
+ if (luaTruthy(c)) break;
1971
2348
  }
2349
+ return;
2350
+ };
1972
2351
 
1973
- const c = evalExpression(r.condition, env, sf);
1974
- if (isPromise(c)) {
1975
- return (c as Promise<any>).then((cv) =>
1976
- luaTruthy(cv) ? undefined : runAsync()
1977
- );
1978
- }
1979
- if (luaTruthy(c)) {
1980
- break;
1981
- }
1982
- }
1983
- return;
2352
+ return runSyncFirst();
1984
2353
  }
1985
2354
  case "Break": {
1986
2355
  return { ctrl: "break" };
@@ -2000,7 +2369,7 @@ export function evalStatement(
2000
2369
  if (fn.name.colonName) {
2001
2370
  // function hello:there() -> function hello.there(self) transformation
2002
2371
  body = {
2003
- ...(fn.body),
2372
+ ...fn.body,
2004
2373
  parameters: ["self", ...fn.body.parameters],
2005
2374
  };
2006
2375
  propNames = [...fn.name.propNames, fn.name.colonName];
@@ -2023,17 +2392,14 @@ export function evalStatement(
2023
2392
  }
2024
2393
  case "LocalFunction": {
2025
2394
  const lf = asLocalFunction(s);
2026
- env.setLocal(
2027
- lf.name,
2028
- new LuaFunction(lf.body, env),
2029
- );
2395
+ env.setLocal(lf.name, new LuaFunction(lf.body, env));
2030
2396
  return;
2031
2397
  }
2032
2398
  case "Return": {
2033
2399
  const ret = asReturn(s);
2034
2400
 
2035
2401
  const parts = ret.expressions.map((value: LuaExpression) =>
2036
- evalExpression(value, env, sf)
2402
+ evalExpression(value, env, sf),
2037
2403
  );
2038
2404
  const valuesRP = rpAll(parts);
2039
2405
 
@@ -2081,9 +2447,7 @@ export function evalStatement(
2081
2447
  const determineLoopType = (): NumericType => {
2082
2448
  const startType = astNumberKind(fr.start);
2083
2449
  const stepType = fr.step ? astNumberKind(fr.step) : "int";
2084
- return (startType === "float" || stepType === "float")
2085
- ? "float"
2086
- : "int";
2450
+ return startType === "float" || stepType === "float" ? "float" : "int";
2087
2451
  };
2088
2452
 
2089
2453
  const wrapLoopVar = (i: number, loopType: NumericType) => {
@@ -2093,53 +2457,59 @@ export function evalStatement(
2093
2457
  return i;
2094
2458
  };
2095
2459
 
2096
- const canReuseEnv = !fr.block.hasFunctionDef ||
2097
- fr.capturesLoopVar === false;
2460
+ const canReuseEnv =
2461
+ !fr.block.hasFunctionDef || fr.capturesLoopVar === false;
2098
2462
 
2099
2463
  const executeIteration = canReuseEnv
2100
2464
  ? (
2101
- loopEnv: LuaEnv,
2102
- i: number,
2103
- loopType: NumericType,
2104
- ): void | ControlSignal | Promise<void | ControlSignal> => {
2105
- loopEnv.setLocal(fr.name, wrapLoopVar(i, loopType));
2106
- return evalStatement(fr.block, loopEnv, sf, returnOnReturn);
2107
- }
2465
+ loopEnv: LuaEnv,
2466
+ i: number,
2467
+ loopType: NumericType,
2468
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
2469
+ loopEnv.setLocal(fr.name, wrapLoopVar(i, loopType));
2470
+ return evalStatement(fr.block, loopEnv, sf, returnOnReturn);
2471
+ }
2108
2472
  : (
2109
- _loopEnv: LuaEnv,
2110
- i: number,
2111
- loopType: NumericType,
2112
- ): void | ControlSignal | Promise<void | ControlSignal> => {
2113
- const localEnv = new LuaEnv(env);
2114
- localEnv.setLocal(fr.name, wrapLoopVar(i, loopType));
2115
- return evalStatement(fr.block, localEnv, sf, returnOnReturn);
2116
- };
2473
+ _loopEnv: LuaEnv,
2474
+ i: number,
2475
+ loopType: NumericType,
2476
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
2477
+ const localEnv = new LuaEnv(env);
2478
+ localEnv.setLocal(fr.name, wrapLoopVar(i, loopType));
2479
+ return evalStatement(fr.block, localEnv, sf, returnOnReturn);
2480
+ };
2117
2481
 
2118
- const runAsync = async (
2482
+ // Continuation that re-enters sync mode after each async iteration
2483
+ const runFromIndex = (
2119
2484
  loopEnv: LuaEnv,
2120
2485
  end: number,
2121
2486
  step: number,
2122
2487
  startIndex: number,
2123
2488
  loopType: NumericType,
2124
- ) => {
2489
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
2125
2490
  if (step === 0) {
2126
2491
  throw new LuaRuntimeError("'for' step is zero", sf.withCtx(fr.ctx));
2127
2492
  }
2128
2493
 
2129
- const shouldContinue = step > 0
2130
- ? (i: number) => i <= end
2131
- : (i: number) => i >= end;
2494
+ const shouldContinue =
2495
+ step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2132
2496
 
2133
2497
  for (let i = startIndex; shouldContinue(i); i += step) {
2134
2498
  const r = executeIteration(loopEnv, i, loopType);
2135
- const res = isPromise(r) ? await r : r;
2136
- if (res !== undefined) {
2137
- if (isBreakSignal(res)) {
2138
- return;
2139
- }
2140
- return res;
2499
+ if (isPromise(r)) {
2500
+ return (r as Promise<any>).then((res) => {
2501
+ if (res !== undefined) {
2502
+ return isBreakSignal(res) ? undefined : res;
2503
+ }
2504
+ return runFromIndex(loopEnv, end, step, i + step, loopType);
2505
+ });
2506
+ }
2507
+ if (r !== undefined) {
2508
+ if (isBreakSignal(r)) return;
2509
+ return r;
2141
2510
  }
2142
2511
  }
2512
+ return;
2143
2513
  };
2144
2514
 
2145
2515
  const runSyncFirst = (
@@ -2147,17 +2517,13 @@ export function evalStatement(
2147
2517
  end: number,
2148
2518
  step: number,
2149
2519
  loopType: NumericType,
2150
- ):
2151
- | void
2152
- | ControlSignal
2153
- | Promise<void | ControlSignal> => {
2520
+ ): undefined | ControlSignal | Promise<undefined | ControlSignal> => {
2154
2521
  if (step === 0) {
2155
2522
  throw new LuaRuntimeError("'for' step is zero", sf.withCtx(fr.ctx));
2156
2523
  }
2157
2524
 
2158
- const shouldContinue = step > 0
2159
- ? (i: number) => i <= end
2160
- : (i: number) => i >= end;
2525
+ const shouldContinue =
2526
+ step > 0 ? (i: number) => i <= end : (i: number) => i >= end;
2161
2527
 
2162
2528
  const loopEnv = new LuaEnv(env);
2163
2529
 
@@ -2171,7 +2537,7 @@ export function evalStatement(
2171
2537
  }
2172
2538
  return res;
2173
2539
  }
2174
- return runAsync(loopEnv, end, step, i + step, loopType);
2540
+ return runFromIndex(loopEnv, end, step, i + step, loopType);
2175
2541
  });
2176
2542
  }
2177
2543
  if (r !== undefined) {
@@ -2186,11 +2552,7 @@ export function evalStatement(
2186
2552
 
2187
2553
  const loopType = determineLoopType();
2188
2554
 
2189
- if (
2190
- !isPromise(startV) &&
2191
- !isPromise(endV) &&
2192
- !isPromise(stepV)
2193
- ) {
2555
+ if (!isPromise(startV) && !isPromise(endV) && !isPromise(stepV)) {
2194
2556
  return runSyncFirst(
2195
2557
  untagNumber(startV) as number,
2196
2558
  untagNumber(endV) as number,
@@ -2217,8 +2579,8 @@ export function evalStatement(
2217
2579
  fi.expressions.map((e: LuaExpression) => evalExpression(e, env, sf)),
2218
2580
  );
2219
2581
 
2220
- const canReuseEnv = !fi.block.hasFunctionDef ||
2221
- fi.capturesLoopVar === false;
2582
+ const canReuseEnv =
2583
+ !fi.block.hasFunctionDef || fi.capturesLoopVar === false;
2222
2584
  const setIterVars = (
2223
2585
  localEnv: LuaEnv,
2224
2586
  names: string[],
@@ -2289,39 +2651,8 @@ export function evalStatement(
2289
2651
  return new LuaEnv(env);
2290
2652
  };
2291
2653
 
2292
- try {
2293
- const runAsync = async () => {
2294
- while (true) {
2295
- const callRes = luaCall(
2296
- iteratorValue,
2297
- [state, control],
2298
- fi.ctx,
2299
- sf,
2300
- );
2301
- const iterResult = new LuaMultiRes(
2302
- isPromise(callRes) ? await callRes : callRes,
2303
- ).flatten();
2304
- const nextControl = iterResult.values[0];
2305
- if (nextControl === null || nextControl === undefined) {
2306
- break;
2307
- }
2308
- control = nextControl;
2309
-
2310
- const localEnv = makeIterEnv();
2311
- setIterVars(localEnv, fi.names, iterResult.values);
2312
-
2313
- const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
2314
- const res = isPromise(r) ? await r : r;
2315
- if (res !== undefined) {
2316
- if (isBreakSignal(res)) {
2317
- break;
2318
- }
2319
- return await finish(res);
2320
- }
2321
- }
2322
- return await finish(undefined);
2323
- };
2324
-
2654
+ // Sync-first loop that re-enters sync mode after each async iteration
2655
+ const runSyncFirst = (): any => {
2325
2656
  while (true) {
2326
2657
  const iterCall = luaCall(
2327
2658
  iteratorValue,
@@ -2329,55 +2660,40 @@ export function evalStatement(
2329
2660
  fi.ctx,
2330
2661
  sf,
2331
2662
  );
2332
- if (isPromise(iterCall)) {
2333
- return (iterCall as Promise<any>).then((itv) => {
2334
- const iterResult = new LuaMultiRes(itv).flatten();
2335
- const nextControl = iterResult.values[0];
2336
- if (nextControl === null || nextControl === undefined) {
2337
- const r = finish(undefined);
2338
- if (isPromise(r)) return (r as Promise<void>).then(() => {});
2339
- return;
2340
- }
2341
- control = nextControl;
2342
2663
 
2343
- const localEnv = makeIterEnv();
2344
- setIterVars(localEnv, fi.names, iterResult.values);
2664
+ const afterIterCall = (itv: any): any => {
2665
+ const iterResult = new LuaMultiRes(itv).flatten();
2666
+ const nextControl = iterResult.values[0];
2667
+ if (nextControl === null || nextControl === undefined) {
2668
+ return finish(undefined);
2669
+ }
2670
+ control = nextControl;
2345
2671
 
2346
- const r = evalStatement(
2347
- fi.block,
2348
- localEnv,
2349
- sf,
2350
- returnOnReturn,
2351
- );
2352
- if (isPromise(r)) {
2353
- return (r as Promise<any>).then((res) => {
2354
- if (res !== undefined) {
2355
- if (isBreakSignal(res)) {
2356
- return finish(undefined);
2357
- }
2358
- return rpThen(finish(undefined), () => res);
2359
- }
2360
- return runAsync();
2361
- });
2362
- }
2363
- if (r !== undefined) {
2364
- if (isBreakSignal(r)) {
2672
+ const localEnv = makeIterEnv();
2673
+ setIterVars(localEnv, fi.names, iterResult.values);
2674
+
2675
+ const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
2676
+ return rpThen(r, (res) => {
2677
+ if (res !== undefined) {
2678
+ if (isBreakSignal(res)) {
2365
2679
  return finish(undefined);
2366
2680
  }
2367
- return rpThen(finish(undefined), () => r);
2681
+ return rpThen(finish(undefined), () => res);
2368
2682
  }
2369
- return runAsync();
2370
- }).catch((e: any) => finishErr(e));
2683
+ return runSyncFirst();
2684
+ });
2685
+ };
2686
+
2687
+ if (isPromise(iterCall)) {
2688
+ return (iterCall as Promise<any>)
2689
+ .then(afterIterCall)
2690
+ .catch((e: any) => finishErr(e));
2371
2691
  }
2372
2692
 
2373
2693
  const iterResult = new LuaMultiRes(iterCall).flatten();
2374
2694
  const nextControl = iterResult.values[0];
2375
2695
  if (nextControl === null || nextControl === undefined) {
2376
- const r = finish(undefined);
2377
- if (isPromise(r)) {
2378
- return (r as Promise<void>);
2379
- }
2380
- return;
2696
+ return finish(undefined);
2381
2697
  }
2382
2698
  control = nextControl;
2383
2699
 
@@ -2386,15 +2702,17 @@ export function evalStatement(
2386
2702
 
2387
2703
  const r = evalStatement(fi.block, localEnv, sf, returnOnReturn);
2388
2704
  if (isPromise(r)) {
2389
- return (r as Promise<any>).then((res) => {
2390
- if (res !== undefined) {
2391
- if (isBreakSignal(res)) {
2392
- return finish(undefined);
2705
+ return (r as Promise<any>)
2706
+ .then((res) => {
2707
+ if (res !== undefined) {
2708
+ if (isBreakSignal(res)) {
2709
+ return finish(undefined);
2710
+ }
2711
+ return rpThen(finish(undefined), () => res);
2393
2712
  }
2394
- return rpThen(finish(undefined), () => res);
2395
- }
2396
- return runAsync();
2397
- }).catch((e: any) => finishErr(e));
2713
+ return runSyncFirst();
2714
+ })
2715
+ .catch((e: any) => finishErr(e));
2398
2716
  }
2399
2717
  if (r !== undefined) {
2400
2718
  if (isBreakSignal(r)) {
@@ -2403,6 +2721,10 @@ export function evalStatement(
2403
2721
  return rpThen(finish(undefined), () => r);
2404
2722
  }
2405
2723
  }
2724
+ };
2725
+
2726
+ try {
2727
+ return runSyncFirst();
2406
2728
  } catch (e: any) {
2407
2729
  return finishErr(e);
2408
2730
  }
@@ -2431,16 +2753,9 @@ function evalLValue(
2431
2753
  }
2432
2754
  case "TableAccess": {
2433
2755
  const ta = asLValueTableAccess(lval);
2434
- const objValue = evalExpression(
2435
- ta.object,
2436
- env,
2437
- sf,
2438
- );
2756
+ const objValue = evalExpression(ta.object, env, sf);
2439
2757
  const keyValue = evalExpression(ta.key, env, sf);
2440
- if (
2441
- isPromise(objValue) ||
2442
- isPromise(keyValue)
2443
- ) {
2758
+ if (isPromise(objValue) || isPromise(keyValue)) {
2444
2759
  return Promise.all([
2445
2760
  isPromise(objValue) ? objValue : Promise.resolve(objValue),
2446
2761
  isPromise(keyValue) ? keyValue : Promise.resolve(keyValue),
@@ -2456,11 +2771,7 @@ function evalLValue(
2456
2771
  }
2457
2772
  case "PropertyAccess": {
2458
2773
  const pa = asLValuePropertyAccess(lval);
2459
- const objValue = evalExpression(
2460
- pa.object,
2461
- env,
2462
- sf,
2463
- );
2774
+ const objValue = evalExpression(pa.object, env, sf);
2464
2775
  if (isPromise(objValue)) {
2465
2776
  return (objValue as Promise<any>).then((ov) => {
2466
2777
  return {