@silverbulletmd/silverbullet 2.5.3 → 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 (90) hide show
  1. package/README.md +4 -5
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +3 -2
  5. package/client/plugos/hooks/code_widget.ts +3 -5
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -12
  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 -5
  11. package/client/plugos/hooks/slash_command.ts +12 -27
  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 -5
  15. package/client/plugos/plug_compile.ts +67 -65
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/worker_sandbox.ts +16 -15
  19. package/client/plugos/syscalls/asset.ts +1 -3
  20. package/client/plugos/syscalls/code_widget.ts +1 -3
  21. package/client/plugos/syscalls/config.ts +1 -5
  22. package/client/plugos/syscalls/datastore.ts +1 -1
  23. package/client/plugos/syscalls/editor.ts +63 -60
  24. package/client/plugos/syscalls/event.ts +9 -12
  25. package/client/plugos/syscalls/fetch.ts +30 -22
  26. package/client/plugos/syscalls/index.ts +10 -1
  27. package/client/plugos/syscalls/jsonschema.ts +72 -32
  28. package/client/plugos/syscalls/language.ts +9 -5
  29. package/client/plugos/syscalls/markdown.ts +29 -7
  30. package/client/plugos/syscalls/mq.ts +3 -11
  31. package/client/plugos/syscalls/service_registry.ts +1 -4
  32. package/client/plugos/syscalls/shell.ts +2 -5
  33. package/client/plugos/syscalls/sync.ts +69 -60
  34. package/client/plugos/syscalls/system.ts +2 -3
  35. package/client/plugos/system.ts +4 -10
  36. package/client/plugos/worker_runtime.ts +4 -3
  37. package/client/space_lua/aggregates.ts +632 -59
  38. package/client/space_lua/ast.ts +21 -9
  39. package/client/space_lua/ast_narrow.ts +4 -2
  40. package/client/space_lua/eval.ts +842 -536
  41. package/client/space_lua/labels.ts +6 -11
  42. package/client/space_lua/liq_null.ts +6 -0
  43. package/client/space_lua/numeric.ts +5 -8
  44. package/client/space_lua/parse.ts +290 -169
  45. package/client/space_lua/query_collection.ts +213 -149
  46. package/client/space_lua/render_lua_markdown.ts +369 -0
  47. package/client/space_lua/rp.ts +5 -4
  48. package/client/space_lua/runtime.ts +245 -142
  49. package/client/space_lua/stdlib/format.ts +34 -20
  50. package/client/space_lua/stdlib/js.ts +3 -7
  51. package/client/space_lua/stdlib/load.ts +1 -3
  52. package/client/space_lua/stdlib/math.ts +15 -14
  53. package/client/space_lua/stdlib/net.ts +25 -15
  54. package/client/space_lua/stdlib/os.ts +76 -85
  55. package/client/space_lua/stdlib/pattern.ts +28 -35
  56. package/client/space_lua/stdlib/prng.ts +15 -12
  57. package/client/space_lua/stdlib/space_lua.ts +16 -17
  58. package/client/space_lua/stdlib/string.ts +7 -17
  59. package/client/space_lua/stdlib/string_pack.ts +23 -19
  60. package/client/space_lua/stdlib/table.ts +5 -9
  61. package/client/space_lua/stdlib.ts +20 -30
  62. package/client/space_lua/tonumber.ts +79 -40
  63. package/client/space_lua/util.ts +14 -10
  64. package/dist/plug-compile.js +44 -41
  65. package/package.json +24 -22
  66. package/plug-api/lib/async.ts +19 -6
  67. package/plug-api/lib/crypto.ts +5 -6
  68. package/plug-api/lib/dates.ts +15 -7
  69. package/plug-api/lib/json.ts +10 -4
  70. package/plug-api/lib/ref.ts +18 -18
  71. package/plug-api/lib/resolve.ts +7 -11
  72. package/plug-api/lib/tags.ts +13 -4
  73. package/plug-api/lib/transclusion.ts +6 -17
  74. package/plug-api/lib/tree.ts +115 -43
  75. package/plug-api/lib/yaml.ts +25 -15
  76. package/plug-api/syscalls/asset.ts +1 -1
  77. package/plug-api/syscalls/config.ts +1 -4
  78. package/plug-api/syscalls/editor.ts +14 -14
  79. package/plug-api/syscalls/jsonschema.ts +1 -3
  80. package/plug-api/syscalls/lua.ts +3 -9
  81. package/plug-api/syscalls/mq.ts +1 -4
  82. package/plug-api/syscalls/shell.ts +4 -1
  83. package/plug-api/syscalls/space.ts +3 -10
  84. package/plug-api/syscalls/system.ts +1 -4
  85. package/plug-api/syscalls/yaml.ts +2 -6
  86. package/plug-api/types/client.ts +16 -1
  87. package/plug-api/types/event.ts +6 -4
  88. package/plug-api/types/manifest.ts +8 -9
  89. package/plugs/builtin_plugs.ts +2 -2
  90. package/dist/worker_runtime_bundle.js +0 -233
@@ -3,6 +3,7 @@ import { evalStatement } from "./eval.ts";
3
3
  import { asyncQuickSort } from "./util.ts";
4
4
  import { isPromise, rpAll } from "./rp.ts";
5
5
  import { isNegativeZero, isTaggedFloat } from "./numeric.ts";
6
+ import { isSqlNull } from "./liq_null.ts";
6
7
  import { luaFormat } from "./stdlib/format.ts";
7
8
 
8
9
  export type LuaType =
@@ -79,7 +80,7 @@ function isLuaNumber(v: any): boolean {
79
80
  }
80
81
 
81
82
  export function luaTypeName(val: any): LuaType {
82
- if (val === null || val === undefined) {
83
+ if (val === null || val === undefined || isSqlNull(val)) {
83
84
  return "nil";
84
85
  }
85
86
 
@@ -113,10 +114,7 @@ export function luaTypeName(val: any): LuaType {
113
114
  }
114
115
 
115
116
  // Check whether a value is callable without invoking it.
116
- export function luaIsCallable(
117
- v: LuaValue,
118
- sf: LuaStackFrame,
119
- ): boolean {
117
+ export function luaIsCallable(v: LuaValue, sf: LuaStackFrame): boolean {
120
118
  if (v === null || v === undefined) {
121
119
  return false;
122
120
  }
@@ -137,9 +135,7 @@ export function luaIsCallable(
137
135
  }
138
136
 
139
137
  // In Lua, `__close` must be a function (no `__call` fallback).
140
- function luaIsCloseMethod(
141
- v: LuaValue,
142
- ): boolean {
138
+ function luaIsCloseMethod(v: LuaValue): boolean {
143
139
  return typeof v === "function" || isILuaFunction(v);
144
140
  }
145
141
 
@@ -261,14 +257,13 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
261
257
  private readonly consts = new Set<string>();
262
258
  private readonly numericTypes = new Map<string, NumericType>();
263
259
 
264
- constructor(readonly parent?: LuaEnv) {
265
- }
260
+ constructor(readonly parent?: LuaEnv) {}
266
261
 
267
262
  setLocal(name: string, value: LuaValue, numType?: NumericType) {
268
263
  this.variables.set(name, value);
269
- if (isLuaNumber(value) && numType) {
264
+ if (numType) {
270
265
  this.numericTypes.set(name, numType);
271
- } else {
266
+ } else if (this.numericTypes.size > 0) {
272
267
  this.numericTypes.delete(name);
273
268
  }
274
269
  }
@@ -276,9 +271,9 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
276
271
  setLocalConst(name: string, value: LuaValue, numType?: NumericType) {
277
272
  this.variables.set(name, value);
278
273
  this.consts.add(name);
279
- if (isLuaNumber(value) && numType) {
274
+ if (numType) {
280
275
  this.numericTypes.set(name, numType);
281
- } else {
276
+ } else if (this.numericTypes.size > 0) {
282
277
  this.numericTypes.delete(name);
283
278
  }
284
279
  }
@@ -297,9 +292,9 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
297
292
  );
298
293
  }
299
294
  this.variables.set(key, value);
300
- if (isLuaNumber(value) && numType) {
295
+ if (numType) {
301
296
  this.numericTypes.set(key, numType);
302
- } else {
297
+ } else if (this.numericTypes.size > 0) {
303
298
  this.numericTypes.delete(key);
304
299
  }
305
300
  } else {
@@ -327,13 +322,15 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
327
322
  return false;
328
323
  }
329
324
 
330
- get(
331
- name: string,
332
- _sf?: LuaStackFrame,
333
- ): Promise<LuaValue> | LuaValue | null {
334
- if (this.variables.has(name)) {
335
- return this.variables.get(name);
325
+ get(name: string, _sf?: LuaStackFrame): Promise<LuaValue> | LuaValue | null {
326
+ // Fast path: single Map.get() instead of has() + get() for the common case
327
+ // where the variable exists and has a non-undefined value (null = Lua nil is fine).
328
+ const v = this.variables.get(name);
329
+ if (v !== undefined) {
330
+ return v;
336
331
  }
332
+ // Variable set to null (Lua nil) returns null from Map.get() which is !== undefined,
333
+ // so it's handled above. Only fall through when the key is truly absent.
337
334
  if (this.parent) {
338
335
  return this.parent.get(name, _sf);
339
336
  }
@@ -378,8 +375,7 @@ export class LuaStackFrame {
378
375
  readonly parent?: LuaStackFrame,
379
376
  readonly currentFunction?: LuaFunction,
380
377
  readonly threadState: LuaThreadState = { closeStack: undefined },
381
- ) {
382
- }
378
+ ) {}
383
379
 
384
380
  static createWithGlobalEnv(
385
381
  globalEnv: LuaEnv,
@@ -458,46 +454,71 @@ export class LuaFunction implements ILuaFunction {
458
454
  private capturedEnv: LuaEnv;
459
455
  funcHasGotos?: boolean;
460
456
 
461
- constructor(readonly body: LuaFunctionBody, closure: LuaEnv) {
457
+ constructor(
458
+ readonly body: LuaFunctionBody,
459
+ closure: LuaEnv,
460
+ ) {
462
461
  this.capturedEnv = closure;
463
462
  }
464
463
 
465
464
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
466
465
  // Create a new environment that chains to the captured environment
467
466
  const env = new LuaEnv(this.capturedEnv);
468
- if (!sf) {
469
- console.trace(sf);
470
- }
471
467
  // Set _CTX to the thread local environment from the stack frame
472
468
  env.setLocal("_CTX", sf.threadLocal);
473
469
 
474
470
  // Eval using a stack frame that knows the current function
475
471
  const sfWithFn = sf.currentFunction === this ? sf : sf.withFunction(this);
476
472
 
473
+ const params = this.body.parameters;
474
+ const hasVarargs = params.length > 0 && params[params.length - 1] === "...";
475
+
477
476
  // Resolve args (sync-first)
478
477
  const argsRP = rpAll(args as any[]);
479
478
  const resolveArgs = (resolvedArgs: any[]) => {
480
- // Assign parameter values to variable names in env
481
- let varargs: LuaValue[] = [];
482
- for (let i = 0; i < this.body.parameters.length; i++) {
483
- const paramName = this.body.parameters[i];
484
- if (paramName === "...") {
485
- // Vararg parameter, let's collect the remainder of the resolved args into the varargs array
486
- varargs = resolvedArgs.slice(i);
487
- // Done, break out of this loop
488
- break;
479
+ if (hasVarargs) {
480
+ // Variadic function: bind named params, collect rest into varargs
481
+ const varargStart = params.length - 1;
482
+ for (let i = 0; i < varargStart; i++) {
483
+ env.setLocal(params[i], resolvedArgs[i] ?? null);
484
+ }
485
+ env.setLocal(
486
+ "...",
487
+ new LuaMultiRes(resolvedArgs.slice(varargStart)),
488
+ );
489
+ } else {
490
+ // Non-variadic: bind all named params directly
491
+ for (let i = 0; i < params.length; i++) {
492
+ env.setLocal(params[i], resolvedArgs[i] ?? null);
489
493
  }
490
- env.setLocal(paramName, resolvedArgs[i] ?? null);
491
494
  }
492
- env.setLocal("...", new LuaMultiRes(varargs));
493
495
 
494
496
  // Evaluate the function body with returnOnReturn set to true
495
497
  const r = evalStatement(this.body.block, env, sfWithFn, true);
496
498
 
497
- const map = (val: any) => {
498
- if (val === undefined) {
499
- return;
499
+ if (!isPromise(r)) {
500
+ // Fast path: synchronous result
501
+ if (r === undefined) return;
502
+ if (r && typeof r === "object" && r.ctrl === "return") {
503
+ return mapFunctionReturnValue(r.values);
504
+ }
505
+ if (r && typeof r === "object" && r.ctrl === "break") {
506
+ throw new LuaRuntimeError(
507
+ "break outside loop",
508
+ sfWithFn.withCtx(this.body.block.ctx),
509
+ );
510
+ }
511
+ if (r && typeof r === "object" && r.ctrl === "goto") {
512
+ throw new LuaRuntimeError(
513
+ "unexpected goto signal",
514
+ sfWithFn.withCtx(this.body.block.ctx),
515
+ );
500
516
  }
517
+ return;
518
+ }
519
+
520
+ return r.then((val: any) => {
521
+ if (val === undefined) return;
501
522
  if (val && typeof val === "object" && val.ctrl === "return") {
502
523
  return mapFunctionReturnValue(val.values);
503
524
  }
@@ -513,12 +534,7 @@ export class LuaFunction implements ILuaFunction {
513
534
  sfWithFn.withCtx(this.body.block.ctx),
514
535
  );
515
536
  }
516
- };
517
-
518
- if (isPromise(r)) {
519
- return r.then(map);
520
- }
521
- return map(r);
537
+ });
522
538
  };
523
539
 
524
540
  if (isPromise(argsRP)) {
@@ -549,17 +565,24 @@ function mapFunctionReturnValue(values: any[]): any {
549
565
  }
550
566
 
551
567
  export class LuaNativeJSFunction implements ILuaFunction {
552
- constructor(readonly fn: (...args: JSValue[]) => JSValue) {
553
- }
568
+ constructor(readonly fn: (...args: JSValue[]) => JSValue) {}
554
569
 
555
570
  // Performs automatic conversion between Lua and JS values for arguments, but not for return values
556
571
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
557
- const jsArgsRP = args.map((v) => luaValueToJS(v, sf));
558
- const resolved = rpAll(jsArgsRP);
559
- if (isPromise(resolved)) {
560
- return resolved.then((jsArgs) => this.fn(...jsArgs));
572
+ // Avoid .map() allocation: convert in-place and check for promises
573
+ const len = args.length;
574
+ let hasAsync = false;
575
+ for (let i = 0; i < len; i++) {
576
+ const converted = luaValueToJS(args[i], sf);
577
+ args[i] = converted;
578
+ if (!hasAsync && isPromise(converted)) {
579
+ hasAsync = true;
580
+ }
581
+ }
582
+ if (hasAsync) {
583
+ return Promise.all(args).then((jsArgs) => this.fn(...jsArgs));
561
584
  }
562
- return this.fn(...resolved);
585
+ return this.fn(...args);
563
586
  }
564
587
 
565
588
  asString(): string {
@@ -574,8 +597,7 @@ export class LuaNativeJSFunction implements ILuaFunction {
574
597
  export class LuaBuiltinFunction implements ILuaFunction {
575
598
  constructor(
576
599
  readonly fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue,
577
- ) {
578
- }
600
+ ) {}
579
601
 
580
602
  call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
581
603
  // _CTX is already available via the stack frame
@@ -602,24 +624,16 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
602
624
  // When tables are used as arrays, we use a native JavaScript array for that
603
625
  private arrayPart: any[];
604
626
 
605
- // Numeric type metadata at storage boundaries
606
- private readonly stringKeyTypes = new Map<string, NumericType>();
627
+ // Numeric type metadata at storage boundaries (lazily allocated)
628
+ private stringKeyTypes: Map<string, NumericType> | null = null;
607
629
  private otherKeyTypes: Map<any, NumericType> | null = null;
608
- private readonly arrayTypes: (NumericType | undefined)[] = [];
630
+ private arrayTypes: (NumericType | undefined)[] | null = null;
609
631
 
610
632
  constructor(init?: any[] | Record<string, any>) {
611
633
  // For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
612
634
  this.arrayPart = Array.isArray(init) ? init : [];
613
635
  this.stringKeys = init && !Array.isArray(init) ? init : {};
614
-
615
- if (init && !Array.isArray(init)) {
616
- for (const k in init) {
617
- if (Object.hasOwn(init, k)) {
618
- this.stringKeys[k] = (init as any)[k];
619
- }
620
- }
621
- }
622
- this.otherKeys = null; // Only create this when needed
636
+ this.otherKeys = null;
623
637
  this.metatable = null;
624
638
  }
625
639
 
@@ -724,10 +738,20 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
724
738
  return this.stringKeys[key] !== undefined;
725
739
  }
726
740
 
741
+ // Fast path for plain integer keys (common in for loops)
742
+ if (typeof key === "number") {
743
+ if (key >= 1 && (key | 0) === key) {
744
+ if (this.arrayPart[key - 1] !== undefined) return true;
745
+ return this.otherKeys ? this.otherKeys.has(key) : false;
746
+ }
747
+ return this.otherKeys ? this.otherKeys.has(key) : false;
748
+ }
749
+
727
750
  const normalizedKey = LuaTable.normalizeNumericKey(key);
728
751
 
729
752
  if (
730
- typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
753
+ typeof normalizedKey === "number" &&
754
+ Number.isInteger(normalizedKey) &&
731
755
  normalizedKey >= 1
732
756
  ) {
733
757
  const idx = normalizedKey - 1;
@@ -759,8 +783,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
759
783
 
760
784
  this.arrayPart[idx] = value;
761
785
  if (isLuaNumber(value) && numType) {
786
+ if (!this.arrayTypes) this.arrayTypes = [];
762
787
  this.arrayTypes[idx] = numType;
763
- } else {
788
+ } else if (this.arrayTypes) {
764
789
  this.arrayTypes[idx] = undefined;
765
790
  }
766
791
  }
@@ -785,7 +810,12 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
785
810
  }
786
811
 
787
812
  this.arrayPart.push(v);
788
- this.arrayTypes.push(nt);
813
+ if (nt) {
814
+ if (!this.arrayTypes) this.arrayTypes = [];
815
+ this.arrayTypes.push(nt);
816
+ } else if (this.arrayTypes) {
817
+ this.arrayTypes.push(undefined);
818
+ }
789
819
  }
790
820
  }
791
821
 
@@ -805,12 +835,13 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
805
835
  if (typeof key === "string") {
806
836
  if (value === null || value === undefined) {
807
837
  delete this.stringKeys[key];
808
- this.stringKeyTypes.delete(key);
838
+ if (this.stringKeyTypes) this.stringKeyTypes.delete(key);
809
839
  } else {
810
840
  this.stringKeys[key] = value;
811
- if (isLuaNumber(value) && numType) {
841
+ if (numType) {
842
+ if (!this.stringKeyTypes) this.stringKeyTypes = new Map();
812
843
  this.stringKeyTypes.set(key, numType);
813
- } else {
844
+ } else if (this.stringKeyTypes) {
814
845
  this.stringKeyTypes.delete(key);
815
846
  }
816
847
  }
@@ -822,12 +853,13 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
822
853
  if (typeof normalizedKey === "string") {
823
854
  if (value === null || value === undefined) {
824
855
  delete this.stringKeys[normalizedKey];
825
- this.stringKeyTypes.delete(normalizedKey);
856
+ if (this.stringKeyTypes) this.stringKeyTypes.delete(normalizedKey);
826
857
  } else {
827
858
  this.stringKeys[normalizedKey] = value;
828
859
  if (isLuaNumber(value) && numType) {
860
+ if (!this.stringKeyTypes) this.stringKeyTypes = new Map();
829
861
  this.stringKeyTypes.set(normalizedKey, numType);
830
- } else {
862
+ } else if (this.stringKeyTypes) {
831
863
  this.stringKeyTypes.delete(normalizedKey);
832
864
  }
833
865
  }
@@ -835,7 +867,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
835
867
  }
836
868
 
837
869
  if (
838
- typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
870
+ typeof normalizedKey === "number" &&
871
+ Number.isInteger(normalizedKey) &&
839
872
  normalizedKey >= 1
840
873
  ) {
841
874
  const idx = normalizedKey - 1;
@@ -845,8 +878,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
845
878
  if (idx <= this.arrayPart.length) {
846
879
  this.arrayPart[idx] = value;
847
880
  if (isLuaNumber(value) && numType) {
881
+ if (!this.arrayTypes) this.arrayTypes = [];
848
882
  this.arrayTypes[idx] = numType;
849
- } else {
883
+ } else if (this.arrayTypes) {
850
884
  this.arrayTypes[idx] = undefined;
851
885
  }
852
886
 
@@ -869,7 +903,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
869
903
  }
870
904
  if (n !== this.arrayPart.length) {
871
905
  this.arrayPart.length = n;
872
- this.arrayTypes.length = n;
906
+ if (this.arrayTypes) this.arrayTypes.length = n;
873
907
  }
874
908
  }
875
909
 
@@ -924,28 +958,27 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
924
958
  sf?: LuaStackFrame,
925
959
  numType?: NumericType,
926
960
  ): Promise<void> | void {
927
- const errSf = sf || LuaStackFrame.lostFrame;
928
- const ctx = sf?.astCtx ?? EMPTY_CTX;
929
-
930
961
  if (key === null || key === undefined) {
931
962
  throw new LuaRuntimeError(
932
963
  "table index is nil",
933
- errSf,
964
+ sf || LuaStackFrame.lostFrame,
934
965
  );
935
966
  }
936
967
 
937
968
  if (typeof key === "number" && Number.isNaN(key)) {
938
969
  throw new LuaRuntimeError(
939
970
  "table index is NaN",
940
- errSf,
971
+ sf || LuaStackFrame.lostFrame,
941
972
  );
942
973
  }
943
974
 
944
- if (this.has(key)) {
975
+ // Fast path: no metatable — skip has() check and metamethod machinery
976
+ if (this.metatable === null) {
945
977
  return this.rawSet(key, value, numType);
946
978
  }
947
979
 
948
- if (this.metatable === null) {
980
+ // Key exists rawSet directly, no metamethod needed
981
+ if (this.has(key)) {
949
982
  return this.rawSet(key, value, numType);
950
983
  }
951
984
 
@@ -955,6 +988,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
955
988
  return this.rawSet(key, value, numType);
956
989
  }
957
990
 
991
+ // Slow path: __newindex metamethod chain (rare)
992
+ const errSf = sf || LuaStackFrame.lostFrame;
993
+ const ctx = sf?.astCtx ?? EMPTY_CTX;
958
994
  const k: LuaValue = key;
959
995
  const v: LuaValue = value;
960
996
  const nt: NumericType | undefined = numType;
@@ -1014,10 +1050,19 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1014
1050
 
1015
1051
  getNumericType(key: LuaValue): NumericType | undefined {
1016
1052
  if (typeof key === "string") {
1017
- return this.stringKeyTypes.get(key);
1053
+ return this.stringKeyTypes ? this.stringKeyTypes.get(key) : undefined;
1054
+ }
1055
+ // Fast path for plain integer keys
1056
+ if (typeof key === "number") {
1057
+ if (key >= 1 && (key | 0) === key) {
1058
+ return this.arrayTypes ? this.arrayTypes[key - 1] : undefined;
1059
+ }
1060
+ return this.otherKeyTypes ? this.otherKeyTypes.get(key) : undefined;
1018
1061
  }
1019
1062
  if (LuaTable.isIntegerKey(key)) {
1020
- return this.arrayTypes[LuaTable.toIndex(key)];
1063
+ return this.arrayTypes
1064
+ ? this.arrayTypes[LuaTable.toIndex(key)]
1065
+ : undefined;
1021
1066
  }
1022
1067
  if (this.otherKeyTypes) {
1023
1068
  return this.otherKeyTypes.get(key);
@@ -1030,6 +1075,19 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1030
1075
  return this.stringKeys[key];
1031
1076
  }
1032
1077
 
1078
+ // Fast path for plain integer keys (common in for loops)
1079
+ if (typeof key === "number") {
1080
+ if (key >= 1 && (key | 0) === key) {
1081
+ const v = this.arrayPart[key - 1];
1082
+ if (v !== undefined) return v;
1083
+ if (this.otherKeys) return this.otherKeys.get(key);
1084
+ return undefined;
1085
+ }
1086
+ // Non-integer or zero/negative number keys
1087
+ if (this.otherKeys) return this.otherKeys.get(key);
1088
+ return undefined;
1089
+ }
1090
+
1033
1091
  const normalizedKey = LuaTable.normalizeNumericKey(key);
1034
1092
 
1035
1093
  if (typeof normalizedKey === "string") {
@@ -1037,7 +1095,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1037
1095
  }
1038
1096
 
1039
1097
  if (
1040
- typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
1098
+ typeof normalizedKey === "number" &&
1099
+ Number.isInteger(normalizedKey) &&
1041
1100
  normalizedKey >= 1
1042
1101
  ) {
1043
1102
  const idx = normalizedKey - 1;
@@ -1058,6 +1117,11 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1058
1117
  }
1059
1118
 
1060
1119
  get(key: LuaValue, sf?: LuaStackFrame): LuaValue | Promise<LuaValue> | null {
1120
+ // Fast path: no metatable means rawGet is the final answer (most tables in SilverBullet)
1121
+ if (this.metatable === null) {
1122
+ const raw = this.rawGet(key);
1123
+ return raw !== undefined ? raw : null;
1124
+ }
1061
1125
  return luaIndexValue(this, key, sf);
1062
1126
  }
1063
1127
 
@@ -1081,8 +1145,21 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1081
1145
 
1082
1146
  toJSObject(sf = LuaStackFrame.lostFrame): Record<string, any> {
1083
1147
  const result: Record<string, any> = {};
1084
- for (const key of this.keys()) {
1085
- result[key] = luaValueToJS(this.get(key, sf), sf);
1148
+ // Direct access to stringKeys avoids keys() allocation and get() metatable checks
1149
+ for (const k in this.stringKeys) {
1150
+ if (Object.hasOwn(this.stringKeys, k)) {
1151
+ result[k] = luaValueToJS(this.stringKeys[k], sf);
1152
+ }
1153
+ }
1154
+ // Include array part with 1-based keys
1155
+ for (let i = 0; i < this.arrayPart.length; i++) {
1156
+ result[i + 1] = luaValueToJS(this.arrayPart[i], sf);
1157
+ }
1158
+ // Include other keys
1159
+ if (this.otherKeys) {
1160
+ for (const [key, val] of this.otherKeys) {
1161
+ result[key] = luaValueToJS(val, sf);
1162
+ }
1086
1163
  }
1087
1164
  return result;
1088
1165
  }
@@ -1111,10 +1188,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1111
1188
 
1112
1189
  const s = singleResult(v);
1113
1190
  if (typeof s !== "string") {
1114
- throw new LuaRuntimeError(
1115
- "'__tostring' must return a string",
1116
- sf,
1117
- );
1191
+ throw new LuaRuntimeError("'__tostring' must return a string", sf);
1118
1192
  }
1119
1193
  return s;
1120
1194
  }
@@ -1168,6 +1242,7 @@ export function luaIndexValue(
1168
1242
  if (t instanceof LuaTable) {
1169
1243
  const raw = t.rawGet(key);
1170
1244
  if (raw !== undefined) {
1245
+ if (isSqlNull(raw)) return null;
1171
1246
  return raw;
1172
1247
  }
1173
1248
  // If no metatable, raw miss => nil
@@ -1219,27 +1294,32 @@ export function luaIndexValue(
1219
1294
 
1220
1295
  export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
1221
1296
 
1222
- export async function luaSet(
1297
+ export function luaSet(
1223
1298
  obj: any,
1224
1299
  key: any,
1225
1300
  value: any,
1226
1301
  sf: LuaStackFrame,
1227
1302
  numType?: NumericType,
1228
- ): Promise<void> {
1303
+ ): void | Promise<void> {
1229
1304
  if (!obj) {
1230
- throw new LuaRuntimeError(
1231
- `Not a settable object: nil`,
1232
- sf,
1233
- );
1305
+ throw new LuaRuntimeError(`Not a settable object: nil`, sf);
1234
1306
  }
1235
1307
 
1236
1308
  const normKey = isTaggedFloat(key) ? key.value : key;
1237
1309
 
1238
1310
  if (obj instanceof LuaTable || obj instanceof LuaEnv) {
1239
- await obj.set(normKey, value, sf, numType);
1311
+ return obj.set(normKey, value, sf, numType);
1240
1312
  } else {
1241
1313
  const k = toNumKey(normKey);
1242
- (obj as Record<string | number, any>)[k] = await luaValueToJS(value, sf);
1314
+ const jsVal = luaValueToJS(value, sf);
1315
+ if (isPromise(jsVal)) {
1316
+ return (jsVal as Promise<any>).then(
1317
+ (v) => {
1318
+ (obj as Record<string | number, any>)[k] = v;
1319
+ },
1320
+ );
1321
+ }
1322
+ (obj as Record<string | number, any>)[k] = jsVal;
1243
1323
  }
1244
1324
  }
1245
1325
 
@@ -1249,12 +1329,10 @@ export function luaGet(
1249
1329
  ctx: ASTCtx | null,
1250
1330
  sf: LuaStackFrame,
1251
1331
  ): Promise<any> | any {
1252
- const errSf = ctx ? sf.withCtx(ctx) : sf;
1253
-
1254
1332
  if (obj === null || obj === undefined) {
1255
1333
  throw new LuaRuntimeError(
1256
1334
  `attempt to index a nil value`,
1257
- errSf,
1335
+ ctx ? sf.withCtx(ctx) : sf,
1258
1336
  );
1259
1337
  }
1260
1338
 
@@ -1338,16 +1416,23 @@ export function luaCall(
1338
1416
 
1339
1417
  // Fast path: native JS function
1340
1418
  if (typeof callee === "function") {
1341
- const jsArgs = rpAll(
1342
- args.map((v) => luaValueToJS(v, sf || LuaStackFrame.lostFrame)),
1343
- );
1419
+ const baseSf = sf || LuaStackFrame.lostFrame;
1420
+ const len = args.length;
1421
+ let hasAsync = false;
1422
+ for (let i = 0; i < len; i++) {
1423
+ const converted = luaValueToJS(args[i], baseSf);
1424
+ args[i] = converted;
1425
+ if (!hasAsync && isPromise(converted)) {
1426
+ hasAsync = true;
1427
+ }
1428
+ }
1344
1429
 
1345
- if (isPromise(jsArgs)) {
1346
- return jsArgs.then((resolved) =>
1347
- (callee as (...a: any[]) => any)(...resolved)
1430
+ if (hasAsync) {
1431
+ return Promise.all(args).then((resolved) =>
1432
+ (callee as (...a: any[]) => any)(...resolved),
1348
1433
  );
1349
1434
  }
1350
- return (callee as (...a: any[]) => any)(...jsArgs);
1435
+ return (callee as (...a: any[]) => any)(...args);
1351
1436
  }
1352
1437
 
1353
1438
  // Lua table: may be callable via __call metamethod
@@ -1383,14 +1468,20 @@ export function luaCall(
1383
1468
 
1384
1469
  // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1385
1470
  if (isILuaFunction(callee)) {
1471
+ // Fast path for builtins: skip withFunction, defer withCtx
1472
+ if (callee instanceof LuaBuiltinFunction) {
1473
+ const base = sf || LuaStackFrame.lostFrame;
1474
+ return callee.call(base, ...args);
1475
+ }
1476
+ // Fast path for native JS functions: skip withCtx/withFunction
1477
+ if (callee instanceof LuaNativeJSFunction) {
1478
+ const base = sf || LuaStackFrame.lostFrame;
1479
+ return callee.call(base, ...args);
1480
+ }
1386
1481
  const base = (sf || LuaStackFrame.lostFrame).withCtx(ctx);
1387
- const frameForCall = callee instanceof LuaFunction
1388
- ? base.withFunction(callee)
1389
- : base;
1390
- return callee.call(
1391
- frameForCall,
1392
- ...args,
1393
- );
1482
+ const frameForCall =
1483
+ callee instanceof LuaFunction ? base.withFunction(callee) : base;
1484
+ return callee.call(frameForCall, ...args);
1394
1485
  }
1395
1486
 
1396
1487
  throw new LuaRuntimeError(
@@ -1400,8 +1491,17 @@ export function luaCall(
1400
1491
  }
1401
1492
 
1402
1493
  export function luaEquals(a: any, b: any): boolean {
1403
- const an = isTaggedFloat(a) ? a.value : a;
1404
- const bn = isTaggedFloat(b) ? b.value : b;
1494
+ // Normalize nil variants (null, undefined, SQL NULL) to null
1495
+ const an = (a === null || a === undefined || isSqlNull(a))
1496
+ ? null
1497
+ : isTaggedFloat(a)
1498
+ ? a.value
1499
+ : a;
1500
+ const bn = (b === null || b === undefined || isSqlNull(b))
1501
+ ? null
1502
+ : isTaggedFloat(b)
1503
+ ? b.value
1504
+ : b;
1405
1505
  return an === bn;
1406
1506
  }
1407
1507
 
@@ -1416,7 +1516,7 @@ export function luaKeys(val: any): any[] {
1416
1516
  }
1417
1517
 
1418
1518
  export function luaTypeOf(val: any): LuaType | Promise<LuaType> {
1419
- if (val === null || val === undefined) {
1519
+ if (val === null || val === undefined || isSqlNull(val)) {
1420
1520
  return "nil";
1421
1521
  }
1422
1522
  if (isPromise(val)) {
@@ -1494,7 +1594,8 @@ export class LuaRuntimeError extends Error {
1494
1594
  // Add position indicator
1495
1595
  const pointer = `${" ".repeat(column)}^`;
1496
1596
 
1497
- traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
1597
+ traceStr +=
1598
+ `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
1498
1599
  ` ${codeLine}\n` +
1499
1600
  ` ${pointer}\n`;
1500
1601
  current = current.parent;
@@ -1527,7 +1628,7 @@ export function luaToString(
1527
1628
  value: any,
1528
1629
  visited: Set<any> = new Set(),
1529
1630
  ): string | Promise<string> {
1530
- if (value === null || value === undefined) {
1631
+ if (value === null || value === undefined || isSqlNull(value)) {
1531
1632
  return "nil";
1532
1633
  }
1533
1634
  if (isPromise(value)) {
@@ -1613,7 +1714,7 @@ export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1613
1714
  if (n === Infinity) return "inf";
1614
1715
  if (n === -Infinity) return "-inf";
1615
1716
  if (n === 0) {
1616
- return (1 / n === -Infinity) ? "-0.0" : "0.0";
1717
+ return 1 / n === -Infinity ? "-0.0" : "0.0";
1617
1718
  }
1618
1719
  // Delegate to luaFormat for `%.14g`
1619
1720
  const s = luaFormat("%.14g", n);
@@ -1643,7 +1744,7 @@ export function getMetatable(
1643
1744
  }
1644
1745
 
1645
1746
  const stringMetatable = new LuaTable();
1646
- stringMetatable.set("__index", (globalEnv as any).get("string"));
1747
+ void stringMetatable.set("__index", (globalEnv as any).get("string"));
1647
1748
  thread.setLocal("_STRING_MT", stringMetatable);
1648
1749
 
1649
1750
  return stringMetatable;
@@ -1659,6 +1760,9 @@ export function getMetatable(
1659
1760
  }
1660
1761
 
1661
1762
  export function jsToLuaValue(value: any): any {
1763
+ if (value === null || value === undefined) {
1764
+ return value;
1765
+ }
1662
1766
  if (isPromise(value)) {
1663
1767
  return (value as Promise<any>).then(jsToLuaValue);
1664
1768
  }
@@ -1673,26 +1777,26 @@ export function jsToLuaValue(value: any): any {
1673
1777
  const regexMatch = value as RegExpMatchArray;
1674
1778
  const regexMatchTable = new LuaTable();
1675
1779
  for (let i = 0; i < regexMatch.length; i++) {
1676
- regexMatchTable.set(i + 1, regexMatch[i]);
1780
+ void regexMatchTable.set(i + 1, regexMatch[i]);
1677
1781
  }
1678
- regexMatchTable.set("index", regexMatch.index);
1679
- regexMatchTable.set("input", regexMatch.input);
1680
- regexMatchTable.set("groups", regexMatch.groups);
1782
+ void regexMatchTable.set("index", regexMatch.index);
1783
+ void regexMatchTable.set("input", regexMatch.input);
1784
+ void regexMatchTable.set("groups", regexMatch.groups);
1681
1785
  return regexMatchTable;
1682
1786
  }
1683
1787
  if (Array.isArray(value)) {
1684
- const table = new LuaTable();
1788
+ const converted = new Array(value.length);
1685
1789
  for (let i = 0; i < value.length; i++) {
1686
- table.set(i + 1, jsToLuaValue(value[i]));
1790
+ converted[i] = jsToLuaValue(value[i]);
1687
1791
  }
1688
- return table;
1792
+ return new LuaTable(converted);
1689
1793
  }
1690
1794
  if (typeof value === "object") {
1691
- const table = new LuaTable();
1795
+ const converted: Record<string, any> = {};
1692
1796
  for (const key in value) {
1693
- table.set(key, jsToLuaValue((value as any)[key]));
1797
+ converted[key] = jsToLuaValue((value as any)[key]);
1694
1798
  }
1695
- return table;
1799
+ return new LuaTable(converted);
1696
1800
  }
1697
1801
  if (typeof value === "function") {
1698
1802
  return new LuaNativeJSFunction(value);
@@ -1709,13 +1813,12 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1709
1813
  return value.toJS(sf);
1710
1814
  }
1711
1815
  if (
1712
- value instanceof LuaNativeJSFunction || value instanceof LuaFunction ||
1816
+ value instanceof LuaNativeJSFunction ||
1817
+ value instanceof LuaFunction ||
1713
1818
  value instanceof LuaBuiltinFunction
1714
1819
  ) {
1715
1820
  return (...args: any[]) => {
1716
- const jsArgs = rpAll(
1717
- args.map((v) => luaValueToJS(v, sf)),
1718
- );
1821
+ const jsArgs = rpAll(args.map((v) => luaValueToJS(v, sf)));
1719
1822
  if (isPromise(jsArgs)) {
1720
1823
  return luaValueToJS(
1721
1824
  jsArgs.then((jsArgs) => (value as ILuaFunction).call(sf, ...jsArgs)),