@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
@@ -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
  }
@@ -128,7 +126,7 @@ export function luaIsCallable(
128
126
  }
129
127
  if (v instanceof LuaTable) {
130
128
  const mt = getMetatable(v, sf);
131
- if (mt && mt.has("__call")) {
129
+ if (mt?.has("__call")) {
132
130
  const mm = mt.get("__call", sf);
133
131
  return !!mm && (typeof mm === "function" || isILuaFunction(mm));
134
132
  }
@@ -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
+ );
500
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
+ );
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
+ }
561
581
  }
562
- return this.fn(...resolved);
582
+ if (hasAsync) {
583
+ return Promise.all(args).then((jsArgs) => this.fn(...jsArgs));
584
+ }
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.prototype.hasOwnProperty.call(init, k)) {
618
- this.stringKeys[k] = (init as any)[k];
619
- }
620
- }
621
- }
622
- this.otherKeys = null; // Only create this when needed
636
+ this.otherKeys = null;
623
637
  this.metatable = null;
624
638
  }
625
639
 
@@ -689,7 +703,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
689
703
  keys(): any[] {
690
704
  const keys: any[] = [];
691
705
  for (const k in this.stringKeys) {
692
- if (Object.prototype.hasOwnProperty.call(this.stringKeys, k)) {
706
+ if (Object.hasOwn(this.stringKeys, k)) {
693
707
  keys.push(k);
694
708
  }
695
709
  }
@@ -706,7 +720,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
706
720
 
707
721
  empty(): boolean {
708
722
  for (const k in this.stringKeys) {
709
- if (Object.prototype.hasOwnProperty.call(this.stringKeys, k)) {
723
+ if (Object.hasOwn(this.stringKeys, k)) {
710
724
  return false;
711
725
  }
712
726
  }
@@ -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,23 +958,39 @@ 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;
961
+ if (key === null || key === undefined) {
962
+ throw new LuaRuntimeError(
963
+ "table index is nil",
964
+ sf || LuaStackFrame.lostFrame,
965
+ );
966
+ }
929
967
 
930
- if (this.has(key)) {
931
- return this.rawSet(key, value, numType);
968
+ if (typeof key === "number" && Number.isNaN(key)) {
969
+ throw new LuaRuntimeError(
970
+ "table index is NaN",
971
+ sf || LuaStackFrame.lostFrame,
972
+ );
932
973
  }
933
974
 
975
+ // Fast path: no metatable — skip has() check and metamethod machinery
934
976
  if (this.metatable === null) {
935
977
  return this.rawSet(key, value, numType);
936
978
  }
937
979
 
980
+ // Key exists — rawSet directly, no metamethod needed
981
+ if (this.has(key)) {
982
+ return this.rawSet(key, value, numType);
983
+ }
984
+
938
985
  const newIndexMM = this.metatable.rawGet("__newindex");
939
986
 
940
987
  if (newIndexMM === undefined || newIndexMM === null) {
941
988
  return this.rawSet(key, value, numType);
942
989
  }
943
990
 
991
+ // Slow path: __newindex metamethod chain (rare)
992
+ const errSf = sf || LuaStackFrame.lostFrame;
993
+ const ctx = sf?.astCtx ?? EMPTY_CTX;
944
994
  const k: LuaValue = key;
945
995
  const v: LuaValue = value;
946
996
  const nt: NumericType | undefined = numType;
@@ -1000,10 +1050,19 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1000
1050
 
1001
1051
  getNumericType(key: LuaValue): NumericType | undefined {
1002
1052
  if (typeof key === "string") {
1003
- 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;
1004
1061
  }
1005
1062
  if (LuaTable.isIntegerKey(key)) {
1006
- return this.arrayTypes[LuaTable.toIndex(key)];
1063
+ return this.arrayTypes
1064
+ ? this.arrayTypes[LuaTable.toIndex(key)]
1065
+ : undefined;
1007
1066
  }
1008
1067
  if (this.otherKeyTypes) {
1009
1068
  return this.otherKeyTypes.get(key);
@@ -1016,6 +1075,19 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1016
1075
  return this.stringKeys[key];
1017
1076
  }
1018
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
+
1019
1091
  const normalizedKey = LuaTable.normalizeNumericKey(key);
1020
1092
 
1021
1093
  if (typeof normalizedKey === "string") {
@@ -1023,7 +1095,8 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1023
1095
  }
1024
1096
 
1025
1097
  if (
1026
- typeof normalizedKey === "number" && Number.isInteger(normalizedKey) &&
1098
+ typeof normalizedKey === "number" &&
1099
+ Number.isInteger(normalizedKey) &&
1027
1100
  normalizedKey >= 1
1028
1101
  ) {
1029
1102
  const idx = normalizedKey - 1;
@@ -1044,6 +1117,11 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1044
1117
  }
1045
1118
 
1046
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
+ }
1047
1125
  return luaIndexValue(this, key, sf);
1048
1126
  }
1049
1127
 
@@ -1067,8 +1145,21 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1067
1145
 
1068
1146
  toJSObject(sf = LuaStackFrame.lostFrame): Record<string, any> {
1069
1147
  const result: Record<string, any> = {};
1070
- for (const key of this.keys()) {
1071
- 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
+ }
1072
1163
  }
1073
1164
  return result;
1074
1165
  }
@@ -1097,10 +1188,7 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1097
1188
 
1098
1189
  const s = singleResult(v);
1099
1190
  if (typeof s !== "string") {
1100
- throw new LuaRuntimeError(
1101
- "'__tostring' must return a string",
1102
- sf,
1103
- );
1191
+ throw new LuaRuntimeError("'__tostring' must return a string", sf);
1104
1192
  }
1105
1193
  return s;
1106
1194
  }
@@ -1121,9 +1209,9 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
1121
1209
  if (typeof key === "string") {
1122
1210
  result += key;
1123
1211
  } else {
1124
- result += "[" + key + "]";
1212
+ result += `[${key}]`;
1125
1213
  }
1126
- result += " = " + await luaToString(this.get(key));
1214
+ result += ` = ${await luaToString(this.get(key))}`;
1127
1215
  }
1128
1216
  result += "}";
1129
1217
  return result;
@@ -1154,6 +1242,7 @@ export function luaIndexValue(
1154
1242
  if (t instanceof LuaTable) {
1155
1243
  const raw = t.rawGet(key);
1156
1244
  if (raw !== undefined) {
1245
+ if (isSqlNull(raw)) return null;
1157
1246
  return raw;
1158
1247
  }
1159
1248
  // If no metatable, raw miss => nil
@@ -1205,27 +1294,32 @@ export function luaIndexValue(
1205
1294
 
1206
1295
  export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
1207
1296
 
1208
- export async function luaSet(
1297
+ export function luaSet(
1209
1298
  obj: any,
1210
1299
  key: any,
1211
1300
  value: any,
1212
1301
  sf: LuaStackFrame,
1213
1302
  numType?: NumericType,
1214
- ): Promise<void> {
1303
+ ): void | Promise<void> {
1215
1304
  if (!obj) {
1216
- throw new LuaRuntimeError(
1217
- `Not a settable object: nil`,
1218
- sf,
1219
- );
1305
+ throw new LuaRuntimeError(`Not a settable object: nil`, sf);
1220
1306
  }
1221
1307
 
1222
1308
  const normKey = isTaggedFloat(key) ? key.value : key;
1223
1309
 
1224
1310
  if (obj instanceof LuaTable || obj instanceof LuaEnv) {
1225
- await obj.set(normKey, value, sf, numType);
1311
+ return obj.set(normKey, value, sf, numType);
1226
1312
  } else {
1227
1313
  const k = toNumKey(normKey);
1228
- (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;
1229
1323
  }
1230
1324
  }
1231
1325
 
@@ -1235,29 +1329,30 @@ export function luaGet(
1235
1329
  ctx: ASTCtx | null,
1236
1330
  sf: LuaStackFrame,
1237
1331
  ): Promise<any> | any {
1238
- const errSf = ctx ? sf.withCtx(ctx) : sf;
1239
-
1240
1332
  if (obj === null || obj === undefined) {
1241
1333
  throw new LuaRuntimeError(
1242
1334
  `attempt to index a nil value`,
1243
- errSf,
1335
+ ctx ? sf.withCtx(ctx) : sf,
1244
1336
  );
1245
1337
  }
1338
+
1339
+ // In Lua reading with a nil key returns nil silently
1246
1340
  if (key === null || key === undefined) {
1247
- throw new LuaRuntimeError(
1248
- `attempt to index with a nil key`,
1249
- errSf,
1250
- );
1341
+ return null;
1251
1342
  }
1252
1343
 
1253
1344
  if (obj instanceof LuaTable || obj instanceof LuaEnv) {
1254
1345
  return obj.get(key, sf);
1255
1346
  }
1347
+ // Native JS array access: normalize undefined → null (Lua nil).
1348
+ // Without this, accessing an out-of-bounds index on a JS array
1349
+ // leaks JS undefined into the Lua runtime, breaking nil checks
1350
+ // such as `tags[1] ~= nil` on an empty array.
1256
1351
  if (typeof key === "number") {
1257
- return (obj as any[])[key - 1];
1352
+ return (obj as any[])[key - 1] ?? null;
1258
1353
  }
1259
1354
  if (isTaggedFloat(key)) {
1260
- return (obj as any[])[key.value - 1];
1355
+ return (obj as any[])[key.value - 1] ?? null;
1261
1356
  }
1262
1357
  // Native JS object
1263
1358
  const k = toNumKey(key);
@@ -1275,7 +1370,8 @@ export function luaGet(
1275
1370
  export function luaLen(
1276
1371
  obj: any,
1277
1372
  sf?: LuaStackFrame,
1278
- ): number {
1373
+ raw = false,
1374
+ ): number | Promise<number> {
1279
1375
  if (typeof obj === "string") {
1280
1376
  return obj.length;
1281
1377
  }
@@ -1283,6 +1379,18 @@ export function luaLen(
1283
1379
  return obj.length;
1284
1380
  }
1285
1381
  if (obj instanceof LuaTable) {
1382
+ // Check __len metamethod unless raw access is requested
1383
+ if (!raw) {
1384
+ const mt = getMetatable(obj, sf || LuaStackFrame.lostFrame);
1385
+ const mm = mt ? mt.rawGet("__len") : null;
1386
+ if (mm !== undefined && mm !== null) {
1387
+ const r = luaCall(mm, [obj], (sf?.astCtx ?? {}) as ASTCtx, sf);
1388
+ if (isPromise(r)) {
1389
+ return (r as Promise<any>).then((v: any) => Number(singleResult(v)));
1390
+ }
1391
+ return Number(singleResult(r));
1392
+ }
1393
+ }
1286
1394
  return obj.rawLength;
1287
1395
  }
1288
1396
 
@@ -1308,16 +1416,23 @@ export function luaCall(
1308
1416
 
1309
1417
  // Fast path: native JS function
1310
1418
  if (typeof callee === "function") {
1311
- const jsArgs = rpAll(
1312
- args.map((v) => luaValueToJS(v, sf || LuaStackFrame.lostFrame)),
1313
- );
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
+ }
1314
1429
 
1315
- if (isPromise(jsArgs)) {
1316
- return jsArgs.then((resolved) =>
1317
- (callee as (...a: any[]) => any)(...resolved)
1430
+ if (hasAsync) {
1431
+ return Promise.all(args).then((resolved) =>
1432
+ (callee as (...a: any[]) => any)(...resolved),
1318
1433
  );
1319
1434
  }
1320
- return (callee as (...a: any[]) => any)(...jsArgs);
1435
+ return (callee as (...a: any[]) => any)(...args);
1321
1436
  }
1322
1437
 
1323
1438
  // Lua table: may be callable via __call metamethod
@@ -1353,14 +1468,20 @@ export function luaCall(
1353
1468
 
1354
1469
  // ILuaFunction (LuaFunction/LuaBuiltinFunction/LuaNativeJSFunction/etc.)
1355
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
+ }
1356
1481
  const base = (sf || LuaStackFrame.lostFrame).withCtx(ctx);
1357
- const frameForCall = callee instanceof LuaFunction
1358
- ? base.withFunction(callee)
1359
- : base;
1360
- return callee.call(
1361
- frameForCall,
1362
- ...args,
1363
- );
1482
+ const frameForCall =
1483
+ callee instanceof LuaFunction ? base.withFunction(callee) : base;
1484
+ return callee.call(frameForCall, ...args);
1364
1485
  }
1365
1486
 
1366
1487
  throw new LuaRuntimeError(
@@ -1370,8 +1491,17 @@ export function luaCall(
1370
1491
  }
1371
1492
 
1372
1493
  export function luaEquals(a: any, b: any): boolean {
1373
- const an = isTaggedFloat(a) ? a.value : a;
1374
- 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;
1375
1505
  return an === bn;
1376
1506
  }
1377
1507
 
@@ -1386,7 +1516,7 @@ export function luaKeys(val: any): any[] {
1386
1516
  }
1387
1517
 
1388
1518
  export function luaTypeOf(val: any): LuaType | Promise<LuaType> {
1389
- if (val === null || val === undefined) {
1519
+ if (val === null || val === undefined || isSqlNull(val)) {
1390
1520
  return "nil";
1391
1521
  }
1392
1522
  if (isPromise(val)) {
@@ -1462,9 +1592,10 @@ export class LuaRuntimeError extends Error {
1462
1592
  );
1463
1593
 
1464
1594
  // Add position indicator
1465
- const pointer = " ".repeat(column) + "^";
1595
+ const pointer = `${" ".repeat(column)}^`;
1466
1596
 
1467
- traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
1597
+ traceStr +=
1598
+ `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
1468
1599
  ` ${codeLine}\n` +
1469
1600
  ` ${pointer}\n`;
1470
1601
  current = current.parent;
@@ -1497,7 +1628,7 @@ export function luaToString(
1497
1628
  value: any,
1498
1629
  visited: Set<any> = new Set(),
1499
1630
  ): string | Promise<string> {
1500
- if (value === null || value === undefined) {
1631
+ if (value === null || value === undefined || isSqlNull(value)) {
1501
1632
  return "nil";
1502
1633
  }
1503
1634
  if (isPromise(value)) {
@@ -1549,7 +1680,7 @@ export function luaToString(
1549
1680
  const strVal = await luaToString(val, visited);
1550
1681
  result += strVal;
1551
1682
  }
1552
- return result + "}";
1683
+ return `${result}}`;
1553
1684
  }
1554
1685
 
1555
1686
  // Handle objects
@@ -1576,20 +1707,20 @@ export function luaToString(
1576
1707
  }
1577
1708
 
1578
1709
  export function luaFormatNumber(n: number, kind?: "int" | "float"): string {
1579
- if (kind !== "float" && Number.isInteger(n) && isFinite(n)) {
1710
+ if (kind !== "float" && Number.isInteger(n) && Number.isFinite(n)) {
1580
1711
  return String(n);
1581
1712
  }
1582
1713
  if (n !== n) return "-nan";
1583
1714
  if (n === Infinity) return "inf";
1584
1715
  if (n === -Infinity) return "-inf";
1585
1716
  if (n === 0) {
1586
- return (1 / n === -Infinity) ? "-0.0" : "0.0";
1717
+ return 1 / n === -Infinity ? "-0.0" : "0.0";
1587
1718
  }
1588
1719
  // Delegate to luaFormat for `%.14g`
1589
1720
  const s = luaFormat("%.14g", n);
1590
1721
  // Guarantee `.01 suffix for integer-valued floats
1591
1722
  if (s.indexOf(".") === -1 && s.indexOf("e") === -1) {
1592
- return s + ".0";
1723
+ return `${s}.0`;
1593
1724
  }
1594
1725
  return s;
1595
1726
  }
@@ -1613,7 +1744,7 @@ export function getMetatable(
1613
1744
  }
1614
1745
 
1615
1746
  const stringMetatable = new LuaTable();
1616
- stringMetatable.set("__index", (globalEnv as any).get("string"));
1747
+ void stringMetatable.set("__index", (globalEnv as any).get("string"));
1617
1748
  thread.setLocal("_STRING_MT", stringMetatable);
1618
1749
 
1619
1750
  return stringMetatable;
@@ -1629,6 +1760,9 @@ export function getMetatable(
1629
1760
  }
1630
1761
 
1631
1762
  export function jsToLuaValue(value: any): any {
1763
+ if (value === null || value === undefined) {
1764
+ return value;
1765
+ }
1632
1766
  if (isPromise(value)) {
1633
1767
  return (value as Promise<any>).then(jsToLuaValue);
1634
1768
  }
@@ -1643,26 +1777,26 @@ export function jsToLuaValue(value: any): any {
1643
1777
  const regexMatch = value as RegExpMatchArray;
1644
1778
  const regexMatchTable = new LuaTable();
1645
1779
  for (let i = 0; i < regexMatch.length; i++) {
1646
- regexMatchTable.set(i + 1, regexMatch[i]);
1780
+ void regexMatchTable.set(i + 1, regexMatch[i]);
1647
1781
  }
1648
- regexMatchTable.set("index", regexMatch.index);
1649
- regexMatchTable.set("input", regexMatch.input);
1650
- 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);
1651
1785
  return regexMatchTable;
1652
1786
  }
1653
1787
  if (Array.isArray(value)) {
1654
- const table = new LuaTable();
1788
+ const converted = new Array(value.length);
1655
1789
  for (let i = 0; i < value.length; i++) {
1656
- table.set(i + 1, jsToLuaValue(value[i]));
1790
+ converted[i] = jsToLuaValue(value[i]);
1657
1791
  }
1658
- return table;
1792
+ return new LuaTable(converted);
1659
1793
  }
1660
1794
  if (typeof value === "object") {
1661
- const table = new LuaTable();
1795
+ const converted: Record<string, any> = {};
1662
1796
  for (const key in value) {
1663
- table.set(key, jsToLuaValue((value as any)[key]));
1797
+ converted[key] = jsToLuaValue((value as any)[key]);
1664
1798
  }
1665
- return table;
1799
+ return new LuaTable(converted);
1666
1800
  }
1667
1801
  if (typeof value === "function") {
1668
1802
  return new LuaNativeJSFunction(value);
@@ -1679,13 +1813,12 @@ export function luaValueToJS(value: any, sf: LuaStackFrame): any {
1679
1813
  return value.toJS(sf);
1680
1814
  }
1681
1815
  if (
1682
- value instanceof LuaNativeJSFunction || value instanceof LuaFunction ||
1816
+ value instanceof LuaNativeJSFunction ||
1817
+ value instanceof LuaFunction ||
1683
1818
  value instanceof LuaBuiltinFunction
1684
1819
  ) {
1685
1820
  return (...args: any[]) => {
1686
- const jsArgs = rpAll(
1687
- args.map((v) => luaValueToJS(v, sf)),
1688
- );
1821
+ const jsArgs = rpAll(args.map((v) => luaValueToJS(v, sf)));
1689
1822
  if (isPromise(jsArgs)) {
1690
1823
  return luaValueToJS(
1691
1824
  jsArgs.then((jsArgs) => (value as ILuaFunction).call(sf, ...jsArgs)),