@creationix/rex 0.3.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,48 +4,49 @@ const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_
4
4
  const digitMap = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
5
5
 
6
6
  const OPCODES = {
7
- do: 0,
8
- add: 1,
9
- sub: 2,
10
- mul: 3,
11
- div: 4,
12
- eq: 5,
13
- neq: 6,
14
- lt: 7,
15
- lte: 8,
16
- gt: 9,
17
- gte: 10,
18
- and: 11,
19
- or: 12,
20
- xor: 13,
21
- not: 14,
22
- boolean: 15,
23
- number: 16,
24
- string: 17,
25
- array: 18,
26
- object: 19,
27
- mod: 20,
28
- neg: 21,
7
+ do: "",
8
+ add: "ad",
9
+ sub: "sb",
10
+ mul: "ml",
11
+ div: "dv",
12
+ eq: "eq",
13
+ neq: "nq",
14
+ lt: "lt",
15
+ lte: "le",
16
+ gt: "gt",
17
+ gte: "ge",
18
+ and: "an",
19
+ or: "or",
20
+ xor: "xr",
21
+ not: "nt",
22
+ boolean: "bt",
23
+ number: "nm",
24
+ string: "st",
25
+ array: "ar",
26
+ object: "ob",
27
+ mod: "md",
28
+ neg: "ng",
29
+ range: "rn",
29
30
  } as const;
30
31
 
31
32
  export type RexcContext = {
32
33
  vars?: Record<string, unknown>;
33
- refs?: Partial<Record<number, unknown>>;
34
+ refs?: Record<string, unknown>;
34
35
  self?: unknown;
35
36
  selfStack?: unknown[];
36
- opcodes?: Partial<Record<number, (args: unknown[], state: RexcRuntimeState) => unknown>>;
37
+ opcodes?: Record<string, (args: unknown[], state: RexcRuntimeState) => unknown>;
37
38
  /** Maximum loop iterations before the interpreter throws. */
38
39
  gasLimit?: number;
39
40
  };
40
41
 
41
42
  export type RexcRuntimeState = {
42
43
  vars: Record<string, unknown>;
43
- refs: Record<number, unknown>;
44
+ refs: Record<string, unknown>;
44
45
  };
45
46
 
46
47
  type LoopControl = { kind: "break" | "continue"; depth: number };
47
48
 
48
- type OpcodeMarker = { __opcode: number };
49
+ type OpcodeMarker = { __opcode: string; __receiver?: unknown };
49
50
 
50
51
  function decodePrefix(text: string, start: number, end: number): number {
51
52
  let value = 0;
@@ -64,7 +65,7 @@ function decodeZigzag(value: number): number {
64
65
  function isValueStart(char: string | undefined): boolean {
65
66
  if (!char) return false;
66
67
  if (digitMap.has(char)) return true;
67
- return "+*:%$@'^~=([{,?!|&><;".includes(char);
68
+ return "+*:%$@'^~=/([{,?!|&><;".includes(char);
68
69
  }
69
70
 
70
71
  function isDefined(value: unknown): boolean {
@@ -76,7 +77,6 @@ class CursorInterpreter {
76
77
  private pos = 0;
77
78
  private readonly state: RexcRuntimeState;
78
79
  private readonly selfStack: unknown[];
79
- private readonly opcodeMarkers: OpcodeMarker[];
80
80
  private readonly pointerCache = new Map<number, unknown>();
81
81
  private readonly gasLimit: number;
82
82
  private gas = 0;
@@ -96,31 +96,26 @@ class CursorInterpreter {
96
96
  this.state = {
97
97
  vars: ctx.vars ?? {},
98
98
  refs: {
99
- 0: ctx.refs?.[0],
100
- 1: ctx.refs?.[1] ?? true,
101
- 2: ctx.refs?.[2] ?? false,
102
- 3: ctx.refs?.[3] ?? null,
103
- 4: ctx.refs?.[4] ?? undefined,
104
- 5: ctx.refs?.[5] ?? NaN,
105
- 6: ctx.refs?.[6] ?? Infinity,
106
- 7: ctx.refs?.[7] ?? -Infinity,
99
+ tr: true,
100
+ fl: false,
101
+ nl: null,
102
+ un: undefined,
103
+ nan: NaN,
104
+ inf: Infinity,
105
+ nif: -Infinity,
106
+ ...ctx.refs,
107
107
  },
108
108
  };
109
109
  this.selfStack = ctx.selfStack && ctx.selfStack.length > 0 ? [...ctx.selfStack] : [initialSelf];
110
110
  this.gasLimit = ctx.gasLimit ?? 0;
111
- this.opcodeMarkers = Array.from({ length: 256 }, (_, id) => ({ __opcode: id }));
112
- for (const [idText, value] of Object.entries(ctx.refs ?? {})) {
113
- const id = Number(idText);
114
- if (Number.isInteger(id)) this.state.refs[id] = value;
115
- }
116
111
  if (ctx.opcodes) {
117
- for (const [idText, op] of Object.entries(ctx.opcodes)) {
118
- if (op) this.customOpcodes.set(Number(idText), op);
112
+ for (const [key, op] of Object.entries(ctx.opcodes)) {
113
+ if (op) this.customOpcodes.set(key, op);
119
114
  }
120
115
  }
121
116
  }
122
117
 
123
- private readonly customOpcodes = new Map<number, (args: unknown[], state: RexcRuntimeState) => unknown>();
118
+ private readonly customOpcodes = new Map<string, (args: unknown[], state: RexcRuntimeState) => unknown>();
124
119
 
125
120
  private readSelf(depthPrefix: number): unknown {
126
121
  const depth = depthPrefix + 1;
@@ -176,6 +171,20 @@ class CursorInterpreter {
176
171
  return { start, end, value: decodePrefix(this.text, start, end), raw: this.text.slice(start, end) };
177
172
  }
178
173
 
174
+ private advanceByBytes(start: number, byteCount: number): number {
175
+ if (byteCount <= 0) return start;
176
+ let bytes = 0;
177
+ let index = start;
178
+ for (const char of this.text.slice(start)) {
179
+ const charBytes = Buffer.byteLength(char);
180
+ if (bytes + charBytes > byteCount) break;
181
+ bytes += charBytes;
182
+ index += char.length;
183
+ if (bytes === byteCount) return index;
184
+ }
185
+ throw new Error("String container overflows input");
186
+ }
187
+
179
188
  private ensure(char: string) {
180
189
  if (this.text[this.pos] !== char) throw new Error(`Expected '${char}' at ${this.pos}`);
181
190
  this.pos += 1;
@@ -217,35 +226,34 @@ class CursorInterpreter {
217
226
  const power = decodeZigzag(prefix.value);
218
227
  const significand = this.evalValue();
219
228
  if (typeof significand !== "number") throw new Error("Decimal significand must be numeric");
220
- return significand * 10 ** power;
229
+ return parseFloat(`${significand}e${power}`);
221
230
  }
222
231
  case ":":
223
232
  this.pos += 1;
224
233
  return prefix.raw;
225
234
  case "%":
226
235
  this.pos += 1;
227
- return this.opcodeMarkers[prefix.value] ?? { __opcode: prefix.value };
236
+ return { __opcode: prefix.raw } satisfies OpcodeMarker;
228
237
  case "@":
229
238
  this.pos += 1;
230
239
  return this.readSelf(prefix.value);
231
240
  case "'":
232
241
  this.pos += 1;
233
- return this.state.refs[prefix.value];
242
+ return this.state.refs[prefix.raw];
234
243
  case "$":
235
244
  this.pos += 1;
236
245
  return this.state.vars[prefix.raw];
237
246
  case ",": {
238
247
  this.pos += 1;
239
248
  const start = this.pos;
240
- const end = start + prefix.value;
241
- if (end > this.text.length) throw new Error("String container overflows input");
249
+ const end = this.advanceByBytes(start, prefix.value);
242
250
  const value = this.text.slice(start, end);
243
251
  this.pos = end;
244
252
  return value;
245
253
  }
246
254
  case "^": {
247
255
  this.pos += 1;
248
- const target = this.pos + prefix.value;
256
+ const target = this.advanceByBytes(this.pos, prefix.value);
249
257
  if (this.pointerCache.has(target)) return this.pointerCache.get(target);
250
258
  const save = this.pos;
251
259
  this.pos = target;
@@ -261,6 +269,14 @@ class CursorInterpreter {
261
269
  this.writePlace(place, value);
262
270
  return value;
263
271
  }
272
+ case "/": {
273
+ this.pos += 1;
274
+ const place = this.readPlace();
275
+ const oldValue = this.readPlaceValue(place);
276
+ const newValue = this.evalValue();
277
+ this.writePlace(place, newValue);
278
+ return oldValue;
279
+ }
264
280
  case "~": {
265
281
  this.pos += 1;
266
282
  const place = this.readPlace();
@@ -288,7 +304,7 @@ class CursorInterpreter {
288
304
  case "<":
289
305
  return this.evalLoopLike(tag);
290
306
  case "#":
291
- return this.evalWhileLoop();
307
+ return this.evalWhileLike();
292
308
  default:
293
309
  throw new Error(`Unexpected tag '${tag}' at ${this.pos}`);
294
310
  }
@@ -311,7 +327,9 @@ class CursorInterpreter {
311
327
  this.ensure(")");
312
328
 
313
329
  if (typeof callee === "object" && callee && "__opcode" in callee) {
314
- return this.applyOpcode((callee as OpcodeMarker).__opcode, args);
330
+ const marker = callee as OpcodeMarker;
331
+ const opArgs = marker.__receiver !== undefined ? [marker.__receiver, ...args] : args;
332
+ return this.applyOpcode(marker.__opcode as string, opArgs);
315
333
  }
316
334
  return this.navigate(callee, args);
317
335
  }
@@ -498,13 +516,10 @@ class CursorInterpreter {
498
516
  if (keysOnly) return entries.map(([key]) => ({ key, value: key }));
499
517
  return entries.map(([key, value]) => ({ key, value }));
500
518
  }
501
- if (typeof iterable === "number" && Number.isFinite(iterable) && iterable > 0) {
502
- const out: Array<{ key: unknown; value: unknown }> = [];
503
- for (let index = 0; index < Math.floor(iterable); index += 1) {
504
- if (keysOnly) out.push({ key: index, value: index });
505
- else out.push({ key: index, value: index + 1 });
506
- }
507
- return out;
519
+ if (typeof iterable === "string") {
520
+ const entries = Array.from(iterable);
521
+ if (keysOnly) return entries.map((_value, index) => ({ key: index, value: index }));
522
+ return entries.map((value, index) => ({ key: index, value }));
508
523
  }
509
524
  return [];
510
525
  }
@@ -551,9 +566,12 @@ class CursorInterpreter {
551
566
  return last;
552
567
  }
553
568
 
554
- private evalWhileLoop(): unknown {
569
+ private evalWhileLike(): unknown {
555
570
  this.pos += 1; // skip '#'
556
- this.ensure("(");
571
+ const open = this.text[this.pos];
572
+ if (!open || !"([{".includes(open)) throw new Error(`Expected opener after '#' at ${this.pos}`);
573
+ const close = open === "(" ? ")" : open === "[" ? "]" : "}";
574
+ this.pos += 1;
557
575
 
558
576
  const condStart = this.pos;
559
577
 
@@ -561,18 +579,23 @@ class CursorInterpreter {
561
579
  const condValue = this.evalValue();
562
580
  const bodyStart = this.pos;
563
581
 
564
- // Skip past the body to find the closing paren
565
- const bodyValueCount = 1;
582
+ // Skip past body values to find the closing bracket
583
+ const bodyValueCount = open === "{" ? 2 : 1;
566
584
  let cursor = bodyStart;
567
585
  for (let index = 0; index < bodyValueCount; index += 1) {
568
586
  cursor = this.skipValueFrom(cursor);
569
587
  }
570
588
  const bodyEnd = cursor;
571
589
  this.pos = bodyEnd;
572
- this.ensure(")");
590
+ this.ensure(close);
573
591
  const afterClose = this.pos;
574
592
 
575
- // Now iterate
593
+ if (open === "[") return this.evalWhileArrayComprehension(condStart, bodyStart, bodyEnd, afterClose, condValue);
594
+ if (open === "{") return this.evalWhileObjectComprehension(condStart, bodyStart, bodyEnd, afterClose, condValue);
595
+ return this.evalWhileLoop(condStart, bodyStart, bodyEnd, afterClose, condValue);
596
+ }
597
+
598
+ private evalWhileLoop(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): unknown {
576
599
  let last: unknown = undefined;
577
600
  let currentCond = condValue;
578
601
  while (isDefined(currentCond)) {
@@ -588,7 +611,6 @@ class CursorInterpreter {
588
611
  last = undefined;
589
612
  }
590
613
 
591
- // Re-evaluate condition
592
614
  currentCond = this.evalBodySlice(condStart, bodyStart);
593
615
  }
594
616
 
@@ -596,6 +618,60 @@ class CursorInterpreter {
596
618
  return last;
597
619
  }
598
620
 
621
+ private evalWhileArrayComprehension(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): unknown[] | LoopControl {
622
+ const out: unknown[] = [];
623
+ let currentCond = condValue;
624
+ while (isDefined(currentCond)) {
625
+ this.tick();
626
+ this.selfStack.push(currentCond);
627
+ const value = this.evalBodySlice(bodyStart, bodyEnd);
628
+ this.selfStack.pop();
629
+
630
+ const control = this.handleLoopControl(value);
631
+ if (control) {
632
+ if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
633
+ if (control.kind === "break") break;
634
+ currentCond = this.evalBodySlice(condStart, bodyStart);
635
+ continue;
636
+ }
637
+ if (isDefined(value)) out.push(value);
638
+
639
+ currentCond = this.evalBodySlice(condStart, bodyStart);
640
+ }
641
+
642
+ this.pos = afterClose;
643
+ return out;
644
+ }
645
+
646
+ private evalWhileObjectComprehension(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): Record<string, unknown> | LoopControl {
647
+ const result: Record<string, unknown> = {};
648
+ let currentCond = condValue;
649
+ while (isDefined(currentCond)) {
650
+ this.tick();
651
+ this.selfStack.push(currentCond);
652
+ const save = this.pos;
653
+ this.pos = bodyStart;
654
+ const key = this.evalValue();
655
+ const value = this.evalValue();
656
+ this.pos = save;
657
+ this.selfStack.pop();
658
+
659
+ const control = this.handleLoopControl(value);
660
+ if (control) {
661
+ if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
662
+ if (control.kind === "break") break;
663
+ currentCond = this.evalBodySlice(condStart, bodyStart);
664
+ continue;
665
+ }
666
+ if (isDefined(value)) result[String(key)] = value;
667
+
668
+ currentCond = this.evalBodySlice(condStart, bodyStart);
669
+ }
670
+
671
+ this.pos = afterClose;
672
+ return result;
673
+ }
674
+
599
675
  private evalArrayComprehension(iterable: unknown, varA: string | undefined, varB: string | undefined, bodyStart: number, bodyEnd: number, keysOnly: boolean): unknown[] | LoopControl {
600
676
  const items = this.iterate(iterable, keysOnly);
601
677
  const out: unknown[] = [];
@@ -654,7 +730,7 @@ class CursorInterpreter {
654
730
  return out;
655
731
  }
656
732
 
657
- private applyOpcode(id: number, args: unknown[]): unknown {
733
+ private applyOpcode(id: string, args: unknown[]): unknown {
658
734
  const custom = this.customOpcodes.get(id);
659
735
  if (custom) return custom(args, this.state);
660
736
  switch (id) {
@@ -662,10 +738,26 @@ class CursorInterpreter {
662
738
  return args.length ? args[args.length - 1] : undefined;
663
739
  case OPCODES.add:
664
740
  if (args[0] === undefined || args[1] === undefined) return undefined;
665
- if (typeof args[0] === "string" || typeof args[1] === "string") {
666
- return String(args[0]) + String(args[1]);
741
+ if (Array.isArray(args[0]) && Array.isArray(args[1])) {
742
+ return [...args[0], ...args[1]];
667
743
  }
668
- return Number(args[0]) + Number(args[1]);
744
+ if (
745
+ args[0]
746
+ && args[1]
747
+ && typeof args[0] === "object"
748
+ && typeof args[1] === "object"
749
+ && !Array.isArray(args[0])
750
+ && !Array.isArray(args[1])
751
+ ) {
752
+ return { ...(args[0] as Record<string, unknown>), ...(args[1] as Record<string, unknown>) };
753
+ }
754
+ if (typeof args[0] === "string" && typeof args[1] === "string") {
755
+ return args[0] + args[1];
756
+ }
757
+ if (typeof args[0] === "number" && typeof args[1] === "number") {
758
+ return args[0] + args[1];
759
+ }
760
+ return undefined;
669
761
  case OPCODES.sub:
670
762
  if (args[0] === undefined || args[1] === undefined) return undefined;
671
763
  return Number(args[0]) - Number(args[1]);
@@ -724,6 +816,84 @@ class CursorInterpreter {
724
816
  return Array.isArray(args[0]) ? args[0] : undefined;
725
817
  case OPCODES.object:
726
818
  return args[0] && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : undefined;
819
+ case OPCODES.range: {
820
+ const from = Number(args[0]);
821
+ const to = Number(args[1]);
822
+ const step = to >= from ? 1 : -1;
823
+ const out: number[] = [];
824
+ for (let v = from; step > 0 ? v <= to : v >= to; v += step)
825
+ out.push(v);
826
+ return out;
827
+ }
828
+ case "array:push": {
829
+ const target = args[0];
830
+ if (!Array.isArray(target)) return undefined;
831
+ const next = target.slice();
832
+ for (let i = 1; i < args.length; i += 1) next.push(args[i]);
833
+ return next;
834
+ }
835
+ case "array:pop": {
836
+ const target = args[0];
837
+ if (!Array.isArray(target) || target.length === 0) return undefined;
838
+ return target[target.length - 1];
839
+ }
840
+ case "array:unshift": {
841
+ const target = args[0];
842
+ if (!Array.isArray(target)) return undefined;
843
+ const next = target.slice();
844
+ for (let i = args.length - 1; i >= 1; i -= 1) next.unshift(args[i]);
845
+ return next;
846
+ }
847
+ case "array:shift": {
848
+ const target = args[0];
849
+ if (!Array.isArray(target) || target.length === 0) return undefined;
850
+ return target[0];
851
+ }
852
+ case "array:slice": {
853
+ const target = args[0];
854
+ if (!Array.isArray(target)) return undefined;
855
+ const start = args.length > 1 && args[1] !== undefined ? Number(args[1]) : undefined;
856
+ const end = args.length > 2 && args[2] !== undefined ? Number(args[2]) : undefined;
857
+ return target.slice(start, end);
858
+ }
859
+ case "array:join": {
860
+ const target = args[0];
861
+ if (!Array.isArray(target)) return undefined;
862
+ const sep = args.length > 1 && args[1] !== undefined ? String(args[1]) : ",";
863
+ return target.map((item) => String(item)).join(sep);
864
+ }
865
+ case "string:split": {
866
+ const target = args[0];
867
+ if (typeof target !== "string") return undefined;
868
+ if (args.length < 2 || args[1] === undefined) return [target];
869
+ return target.split(String(args[1]));
870
+ }
871
+ case "string:join": {
872
+ const target = args[0];
873
+ if (typeof target !== "string") return undefined;
874
+ const parts = Array.from(target);
875
+ const sep = args.length > 1 && args[1] !== undefined ? String(args[1]) : "";
876
+ return parts.join(sep);
877
+ }
878
+ case "string:slice": {
879
+ const target = args[0];
880
+ if (typeof target !== "string") return undefined;
881
+ const start = args.length > 1 && args[1] !== undefined ? Number(args[1]) : undefined;
882
+ const end = args.length > 2 && args[2] !== undefined ? Number(args[2]) : undefined;
883
+ return Array.from(target).slice(start, end).join("");
884
+ }
885
+ case "string:starts-with": {
886
+ const target = args[0];
887
+ if (typeof target !== "string") return undefined;
888
+ const prefix = args.length > 1 && args[1] !== undefined ? String(args[1]) : "";
889
+ return target.startsWith(prefix);
890
+ }
891
+ case "string:ends-with": {
892
+ const target = args[0];
893
+ if (typeof target !== "string") return undefined;
894
+ const suffix = args.length > 1 && args[1] !== undefined ? String(args[1]) : "";
895
+ return target.endsWith(suffix);
896
+ }
727
897
  default:
728
898
  throw new Error(`Unknown opcode ${id}`);
729
899
  }
@@ -733,12 +903,97 @@ class CursorInterpreter {
733
903
  let current = base;
734
904
  for (const key of keys) {
735
905
  if (current === undefined || current === null) return undefined;
736
- current = (current as Record<string, unknown>)[String(key)];
906
+ current = this.readProperty(current, key);
907
+ if (current === undefined) return undefined;
737
908
  }
738
909
  return current;
739
910
  }
740
911
 
741
- private readPlace(): { root: string | number; keys: unknown[]; isRef: boolean } {
912
+ private readProperty(target: unknown, key: unknown): unknown {
913
+ if (typeof key === "string" && key === "size") {
914
+ if (Array.isArray(target)) return target.length;
915
+ if (typeof target === "string") return Array.from(target).length;
916
+ }
917
+ const index = this.parseIndexKey(key);
918
+ if (Array.isArray(target)) {
919
+ if (index !== undefined) return target[index];
920
+ if (typeof key === "string") return this.resolveArrayMethod(target, key);
921
+ return undefined;
922
+ }
923
+ if (typeof target === "string") {
924
+ if (index !== undefined) return Array.from(target)[index];
925
+ if (typeof key === "string") return this.resolveStringMethod(target, key);
926
+ return undefined;
927
+ }
928
+ if (this.isPlainObject(target)) {
929
+ const prop = String(key);
930
+ if (!Object.prototype.hasOwnProperty.call(target as Record<string, unknown>, prop)) return undefined;
931
+ return (target as Record<string, unknown>)[prop];
932
+ }
933
+ return undefined;
934
+ }
935
+
936
+ private resolveArrayMethod(target: unknown[], key: string): OpcodeMarker | undefined {
937
+ switch (key) {
938
+ case "push":
939
+ return { __opcode: "array:push", __receiver: target };
940
+ case "pop":
941
+ return { __opcode: "array:pop", __receiver: target };
942
+ case "unshift":
943
+ return { __opcode: "array:unshift", __receiver: target };
944
+ case "shift":
945
+ return { __opcode: "array:shift", __receiver: target };
946
+ case "slice":
947
+ return { __opcode: "array:slice", __receiver: target };
948
+ case "join":
949
+ return { __opcode: "array:join", __receiver: target };
950
+ default:
951
+ return undefined;
952
+ }
953
+ }
954
+
955
+ private resolveStringMethod(target: string, key: string): OpcodeMarker | undefined {
956
+ switch (key) {
957
+ case "split":
958
+ return { __opcode: "string:split", __receiver: target };
959
+ case "join":
960
+ return { __opcode: "string:join", __receiver: target };
961
+ case "slice":
962
+ return { __opcode: "string:slice", __receiver: target };
963
+ case "starts-with":
964
+ return { __opcode: "string:starts-with", __receiver: target };
965
+ case "ends-with":
966
+ return { __opcode: "string:ends-with", __receiver: target };
967
+ default:
968
+ return undefined;
969
+ }
970
+ }
971
+
972
+ private canWriteProperty(target: unknown, key: unknown): { kind: "array"; index: number } | { kind: "object" } | undefined {
973
+ const index = this.parseIndexKey(key);
974
+ if (Array.isArray(target)) {
975
+ if (index === undefined) return undefined;
976
+ return { kind: "array", index };
977
+ }
978
+ if (this.isPlainObject(target)) return { kind: "object" };
979
+ return undefined;
980
+ }
981
+
982
+ private parseIndexKey(key: unknown): number | undefined {
983
+ if (typeof key === "number" && Number.isInteger(key) && key >= 0) return key;
984
+ if (typeof key !== "string" || key.length === 0) return undefined;
985
+ if (!/^(0|[1-9]\d*)$/.test(key)) return undefined;
986
+ const index = Number(key);
987
+ return Number.isSafeInteger(index) ? index : undefined;
988
+ }
989
+
990
+ private isPlainObject(value: unknown): value is Record<string, unknown> {
991
+ if (!value || typeof value !== "object") return false;
992
+ const proto = Object.getPrototypeOf(value);
993
+ return proto === Object.prototype || proto === null;
994
+ }
995
+
996
+ private readPlace(): { root: string; keys: unknown[]; isRef: boolean } {
742
997
  this.skipNonCode();
743
998
  const direct = this.readRootVarOrRefIfPresent();
744
999
  if (direct) {
@@ -784,7 +1039,7 @@ class CursorInterpreter {
784
1039
  throw new Error(`Invalid place at ${this.pos}`);
785
1040
  }
786
1041
 
787
- private readRootVarOrRefIfPresent(): { root: string | number; isRef: boolean } | undefined {
1042
+ private readRootVarOrRefIfPresent(): { root: string; isRef: boolean } | undefined {
788
1043
  const save = this.pos;
789
1044
  const prefix = this.readPrefix();
790
1045
  const tag = this.text[this.pos];
@@ -794,14 +1049,14 @@ class CursorInterpreter {
794
1049
  }
795
1050
  this.pos += 1;
796
1051
  return {
797
- root: tag === "$" ? prefix.raw : prefix.value,
1052
+ root: prefix.raw,
798
1053
  isRef: tag === "'",
799
1054
  };
800
1055
  }
801
1056
 
802
- private writePlace(place: { root: string | number; keys: unknown[]; isRef: boolean }, value: unknown) {
1057
+ private writePlace(place: { root: string; keys: unknown[]; isRef: boolean }, value: unknown) {
803
1058
  const rootTable = place.isRef ? this.state.refs : this.state.vars;
804
- const rootKey = String(place.root);
1059
+ const rootKey = place.root;
805
1060
  if (place.keys.length === 0) {
806
1061
  rootTable[rootKey] = value;
807
1062
  return;
@@ -812,17 +1067,44 @@ class CursorInterpreter {
812
1067
  rootTable[rootKey] = target;
813
1068
  }
814
1069
  for (let index = 0; index < place.keys.length - 1; index += 1) {
815
- const key = String(place.keys[index]);
816
- const next = (target as Record<string, unknown>)[key];
817
- if (!next || typeof next !== "object") (target as Record<string, unknown>)[key] = {};
818
- target = (target as Record<string, unknown>)[key];
1070
+ const key = place.keys[index];
1071
+ const access = this.canWriteProperty(target, key);
1072
+ if (!access) return;
1073
+ if (access.kind === "array") {
1074
+ const next = (target as unknown[])[access.index];
1075
+ if (!next || typeof next !== "object") (target as unknown[])[access.index] = {};
1076
+ target = (target as unknown[])[access.index] as Record<string, unknown>;
1077
+ continue;
1078
+ }
1079
+ const prop = String(key);
1080
+ const next = (target as Record<string, unknown>)[prop];
1081
+ if (!next || typeof next !== "object") (target as Record<string, unknown>)[prop] = {};
1082
+ target = (target as Record<string, unknown>)[prop];
1083
+ }
1084
+ const lastKey = place.keys[place.keys.length - 1];
1085
+ const access = this.canWriteProperty(target, lastKey);
1086
+ if (!access) return;
1087
+ if (access.kind === "array") {
1088
+ (target as unknown[])[access.index] = value;
1089
+ return;
819
1090
  }
820
- (target as Record<string, unknown>)[String(place.keys[place.keys.length - 1])] = value;
1091
+ (target as Record<string, unknown>)[String(lastKey)] = value;
821
1092
  }
822
1093
 
823
- private deletePlace(place: { root: string | number; keys: unknown[]; isRef: boolean }) {
1094
+ private readPlaceValue(place: { root: string; keys: unknown[]; isRef: boolean }): unknown {
824
1095
  const rootTable = place.isRef ? this.state.refs : this.state.vars;
825
- const rootKey = String(place.root);
1096
+ let current: unknown = rootTable[place.root];
1097
+ for (const key of place.keys) {
1098
+ if (current === undefined || current === null) return undefined;
1099
+ current = this.readProperty(current, key);
1100
+ if (current === undefined) return undefined;
1101
+ }
1102
+ return current;
1103
+ }
1104
+
1105
+ private deletePlace(place: { root: string; keys: unknown[]; isRef: boolean }) {
1106
+ const rootTable = place.isRef ? this.state.refs : this.state.vars;
1107
+ const rootKey = place.root;
826
1108
  if (place.keys.length === 0) {
827
1109
  delete rootTable[rootKey];
828
1110
  return;
@@ -830,10 +1112,25 @@ class CursorInterpreter {
830
1112
  let target = rootTable[rootKey];
831
1113
  if (!target || typeof target !== "object") return;
832
1114
  for (let index = 0; index < place.keys.length - 1; index += 1) {
833
- target = (target as Record<string, unknown>)[String(place.keys[index])];
1115
+ const key = place.keys[index];
1116
+ const access = this.canWriteProperty(target, key);
1117
+ if (!access) return;
1118
+ if (access.kind === "array") {
1119
+ target = (target as unknown[])[access.index];
1120
+ if (!target || typeof target !== "object") return;
1121
+ continue;
1122
+ }
1123
+ target = (target as Record<string, unknown>)[String(key)];
834
1124
  if (!target || typeof target !== "object") return;
835
1125
  }
836
- delete (target as Record<string, unknown>)[String(place.keys[place.keys.length - 1])];
1126
+ const lastKey = place.keys[place.keys.length - 1];
1127
+ const access = this.canWriteProperty(target, lastKey);
1128
+ if (!access) return;
1129
+ if (access.kind === "array") {
1130
+ delete (target as unknown[])[access.index];
1131
+ return;
1132
+ }
1133
+ delete (target as Record<string, unknown>)[String(lastKey)];
837
1134
  }
838
1135
 
839
1136
  private skipValue() {
@@ -852,12 +1149,12 @@ class CursorInterpreter {
852
1149
  }
853
1150
 
854
1151
  if (tag === ",") {
855
- this.pos += 1 + prefix.value;
1152
+ this.pos = this.advanceByBytes(this.pos + 1, prefix.value);
856
1153
  const end = this.pos;
857
1154
  this.pos = save;
858
1155
  return end;
859
1156
  }
860
- if (tag === "=") {
1157
+ if (tag === "=" || tag === "/") {
861
1158
  this.pos += 1;
862
1159
  this.skipValue();
863
1160
  this.skipValue();
@@ -892,7 +1189,8 @@ class CursorInterpreter {
892
1189
  if (opener && "([{".includes(opener)) {
893
1190
  const close = opener === "(" ? ")" : opener === "[" ? "]" : "}";
894
1191
  if (prefix.value > 0) {
895
- this.pos += 1 + prefix.value + 1;
1192
+ const bodyEnd = this.advanceByBytes(this.pos + 1, prefix.value);
1193
+ this.pos = bodyEnd + 1;
896
1194
  const end = this.pos;
897
1195
  this.pos = save;
898
1196
  return end;