@creationix/rex 0.3.0 → 0.4.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.
- package/package.json +3 -2
- package/rex-cli.js +730 -285
- package/rex-cli.ts +34 -13
- package/rex-repl.js +695 -270
- package/rex-repl.ts +39 -5
- package/rex.js +370 -188
- package/rex.ohm +73 -25
- package/rex.ohm-bundle.cjs +1 -1
- package/rex.ohm-bundle.d.ts +31 -8
- package/rex.ohm-bundle.js +1 -1
- package/rex.ts +390 -206
- package/rexc-interpreter.ts +277 -90
package/rexc-interpreter.ts
CHANGED
|
@@ -4,48 +4,50 @@ 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:
|
|
8
|
-
add:
|
|
9
|
-
sub:
|
|
10
|
-
mul:
|
|
11
|
-
div:
|
|
12
|
-
eq:
|
|
13
|
-
neq:
|
|
14
|
-
lt:
|
|
15
|
-
lte:
|
|
16
|
-
gt:
|
|
17
|
-
gte:
|
|
18
|
-
and:
|
|
19
|
-
or:
|
|
20
|
-
xor:
|
|
21
|
-
not:
|
|
22
|
-
boolean:
|
|
23
|
-
number:
|
|
24
|
-
string:
|
|
25
|
-
array:
|
|
26
|
-
object:
|
|
27
|
-
mod:
|
|
28
|
-
neg:
|
|
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",
|
|
30
|
+
size: "sz",
|
|
29
31
|
} as const;
|
|
30
32
|
|
|
31
33
|
export type RexcContext = {
|
|
32
34
|
vars?: Record<string, unknown>;
|
|
33
|
-
refs?:
|
|
35
|
+
refs?: Record<string, unknown>;
|
|
34
36
|
self?: unknown;
|
|
35
37
|
selfStack?: unknown[];
|
|
36
|
-
opcodes?:
|
|
38
|
+
opcodes?: Record<string, (args: unknown[], state: RexcRuntimeState) => unknown>;
|
|
37
39
|
/** Maximum loop iterations before the interpreter throws. */
|
|
38
40
|
gasLimit?: number;
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
export type RexcRuntimeState = {
|
|
42
44
|
vars: Record<string, unknown>;
|
|
43
|
-
refs: Record<
|
|
45
|
+
refs: Record<string, unknown>;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
type LoopControl = { kind: "break" | "continue"; depth: number };
|
|
47
49
|
|
|
48
|
-
type OpcodeMarker = { __opcode:
|
|
50
|
+
type OpcodeMarker = { __opcode: string };
|
|
49
51
|
|
|
50
52
|
function decodePrefix(text: string, start: number, end: number): number {
|
|
51
53
|
let value = 0;
|
|
@@ -64,7 +66,7 @@ function decodeZigzag(value: number): number {
|
|
|
64
66
|
function isValueStart(char: string | undefined): boolean {
|
|
65
67
|
if (!char) return false;
|
|
66
68
|
if (digitMap.has(char)) return true;
|
|
67
|
-
return "+*:%$@'
|
|
69
|
+
return "+*:%$@'^~=/([{,?!|&><;".includes(char);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function isDefined(value: unknown): boolean {
|
|
@@ -76,7 +78,6 @@ class CursorInterpreter {
|
|
|
76
78
|
private pos = 0;
|
|
77
79
|
private readonly state: RexcRuntimeState;
|
|
78
80
|
private readonly selfStack: unknown[];
|
|
79
|
-
private readonly opcodeMarkers: OpcodeMarker[];
|
|
80
81
|
private readonly pointerCache = new Map<number, unknown>();
|
|
81
82
|
private readonly gasLimit: number;
|
|
82
83
|
private gas = 0;
|
|
@@ -96,28 +97,26 @@ class CursorInterpreter {
|
|
|
96
97
|
this.state = {
|
|
97
98
|
vars: ctx.vars ?? {},
|
|
98
99
|
refs: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
tr: true,
|
|
101
|
+
fl: false,
|
|
102
|
+
nl: null,
|
|
103
|
+
un: undefined,
|
|
104
|
+
nan: NaN,
|
|
105
|
+
inf: Infinity,
|
|
106
|
+
nif: -Infinity,
|
|
107
|
+
...ctx.refs,
|
|
104
108
|
},
|
|
105
109
|
};
|
|
106
110
|
this.selfStack = ctx.selfStack && ctx.selfStack.length > 0 ? [...ctx.selfStack] : [initialSelf];
|
|
107
111
|
this.gasLimit = ctx.gasLimit ?? 0;
|
|
108
|
-
this.opcodeMarkers = Array.from({ length: 256 }, (_, id) => ({ __opcode: id }));
|
|
109
|
-
for (const [idText, value] of Object.entries(ctx.refs ?? {})) {
|
|
110
|
-
const id = Number(idText);
|
|
111
|
-
if (Number.isInteger(id)) this.state.refs[id] = value;
|
|
112
|
-
}
|
|
113
112
|
if (ctx.opcodes) {
|
|
114
|
-
for (const [
|
|
115
|
-
if (op) this.customOpcodes.set(
|
|
113
|
+
for (const [key, op] of Object.entries(ctx.opcodes)) {
|
|
114
|
+
if (op) this.customOpcodes.set(key, op);
|
|
116
115
|
}
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
private readonly customOpcodes = new Map<
|
|
119
|
+
private readonly customOpcodes = new Map<string, (args: unknown[], state: RexcRuntimeState) => unknown>();
|
|
121
120
|
|
|
122
121
|
private readSelf(depthPrefix: number): unknown {
|
|
123
122
|
const depth = depthPrefix + 1;
|
|
@@ -173,6 +172,20 @@ class CursorInterpreter {
|
|
|
173
172
|
return { start, end, value: decodePrefix(this.text, start, end), raw: this.text.slice(start, end) };
|
|
174
173
|
}
|
|
175
174
|
|
|
175
|
+
private advanceByBytes(start: number, byteCount: number): number {
|
|
176
|
+
if (byteCount <= 0) return start;
|
|
177
|
+
let bytes = 0;
|
|
178
|
+
let index = start;
|
|
179
|
+
for (const char of this.text.slice(start)) {
|
|
180
|
+
const charBytes = Buffer.byteLength(char);
|
|
181
|
+
if (bytes + charBytes > byteCount) break;
|
|
182
|
+
bytes += charBytes;
|
|
183
|
+
index += char.length;
|
|
184
|
+
if (bytes === byteCount) return index;
|
|
185
|
+
}
|
|
186
|
+
throw new Error("String container overflows input");
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
private ensure(char: string) {
|
|
177
190
|
if (this.text[this.pos] !== char) throw new Error(`Expected '${char}' at ${this.pos}`);
|
|
178
191
|
this.pos += 1;
|
|
@@ -214,35 +227,34 @@ class CursorInterpreter {
|
|
|
214
227
|
const power = decodeZigzag(prefix.value);
|
|
215
228
|
const significand = this.evalValue();
|
|
216
229
|
if (typeof significand !== "number") throw new Error("Decimal significand must be numeric");
|
|
217
|
-
return significand
|
|
230
|
+
return parseFloat(`${significand}e${power}`);
|
|
218
231
|
}
|
|
219
232
|
case ":":
|
|
220
233
|
this.pos += 1;
|
|
221
234
|
return prefix.raw;
|
|
222
235
|
case "%":
|
|
223
236
|
this.pos += 1;
|
|
224
|
-
return
|
|
237
|
+
return { __opcode: prefix.raw } satisfies OpcodeMarker;
|
|
225
238
|
case "@":
|
|
226
239
|
this.pos += 1;
|
|
227
240
|
return this.readSelf(prefix.value);
|
|
228
241
|
case "'":
|
|
229
242
|
this.pos += 1;
|
|
230
|
-
return this.state.refs[prefix.
|
|
243
|
+
return this.state.refs[prefix.raw];
|
|
231
244
|
case "$":
|
|
232
245
|
this.pos += 1;
|
|
233
246
|
return this.state.vars[prefix.raw];
|
|
234
247
|
case ",": {
|
|
235
248
|
this.pos += 1;
|
|
236
249
|
const start = this.pos;
|
|
237
|
-
const end = start
|
|
238
|
-
if (end > this.text.length) throw new Error("String container overflows input");
|
|
250
|
+
const end = this.advanceByBytes(start, prefix.value);
|
|
239
251
|
const value = this.text.slice(start, end);
|
|
240
252
|
this.pos = end;
|
|
241
253
|
return value;
|
|
242
254
|
}
|
|
243
255
|
case "^": {
|
|
244
256
|
this.pos += 1;
|
|
245
|
-
const target = this.pos
|
|
257
|
+
const target = this.advanceByBytes(this.pos, prefix.value);
|
|
246
258
|
if (this.pointerCache.has(target)) return this.pointerCache.get(target);
|
|
247
259
|
const save = this.pos;
|
|
248
260
|
this.pos = target;
|
|
@@ -258,6 +270,14 @@ class CursorInterpreter {
|
|
|
258
270
|
this.writePlace(place, value);
|
|
259
271
|
return value;
|
|
260
272
|
}
|
|
273
|
+
case "/": {
|
|
274
|
+
this.pos += 1;
|
|
275
|
+
const place = this.readPlace();
|
|
276
|
+
const oldValue = this.readPlaceValue(place);
|
|
277
|
+
const newValue = this.evalValue();
|
|
278
|
+
this.writePlace(place, newValue);
|
|
279
|
+
return oldValue;
|
|
280
|
+
}
|
|
261
281
|
case "~": {
|
|
262
282
|
this.pos += 1;
|
|
263
283
|
const place = this.readPlace();
|
|
@@ -285,7 +305,7 @@ class CursorInterpreter {
|
|
|
285
305
|
case "<":
|
|
286
306
|
return this.evalLoopLike(tag);
|
|
287
307
|
case "#":
|
|
288
|
-
return this.
|
|
308
|
+
return this.evalWhileLike();
|
|
289
309
|
default:
|
|
290
310
|
throw new Error(`Unexpected tag '${tag}' at ${this.pos}`);
|
|
291
311
|
}
|
|
@@ -308,7 +328,7 @@ class CursorInterpreter {
|
|
|
308
328
|
this.ensure(")");
|
|
309
329
|
|
|
310
330
|
if (typeof callee === "object" && callee && "__opcode" in callee) {
|
|
311
|
-
return this.applyOpcode((callee as OpcodeMarker).__opcode, args);
|
|
331
|
+
return this.applyOpcode((callee as OpcodeMarker).__opcode as string, args);
|
|
312
332
|
}
|
|
313
333
|
return this.navigate(callee, args);
|
|
314
334
|
}
|
|
@@ -495,13 +515,10 @@ class CursorInterpreter {
|
|
|
495
515
|
if (keysOnly) return entries.map(([key]) => ({ key, value: key }));
|
|
496
516
|
return entries.map(([key, value]) => ({ key, value }));
|
|
497
517
|
}
|
|
498
|
-
if (typeof iterable === "
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
else out.push({ key: index, value: index + 1 });
|
|
503
|
-
}
|
|
504
|
-
return out;
|
|
518
|
+
if (typeof iterable === "string") {
|
|
519
|
+
const entries = Array.from(iterable);
|
|
520
|
+
if (keysOnly) return entries.map((_value, index) => ({ key: index, value: index }));
|
|
521
|
+
return entries.map((value, index) => ({ key: index, value }));
|
|
505
522
|
}
|
|
506
523
|
return [];
|
|
507
524
|
}
|
|
@@ -548,9 +565,12 @@ class CursorInterpreter {
|
|
|
548
565
|
return last;
|
|
549
566
|
}
|
|
550
567
|
|
|
551
|
-
private
|
|
568
|
+
private evalWhileLike(): unknown {
|
|
552
569
|
this.pos += 1; // skip '#'
|
|
553
|
-
this.
|
|
570
|
+
const open = this.text[this.pos];
|
|
571
|
+
if (!open || !"([{".includes(open)) throw new Error(`Expected opener after '#' at ${this.pos}`);
|
|
572
|
+
const close = open === "(" ? ")" : open === "[" ? "]" : "}";
|
|
573
|
+
this.pos += 1;
|
|
554
574
|
|
|
555
575
|
const condStart = this.pos;
|
|
556
576
|
|
|
@@ -558,18 +578,23 @@ class CursorInterpreter {
|
|
|
558
578
|
const condValue = this.evalValue();
|
|
559
579
|
const bodyStart = this.pos;
|
|
560
580
|
|
|
561
|
-
// Skip past
|
|
562
|
-
const bodyValueCount = 1;
|
|
581
|
+
// Skip past body values to find the closing bracket
|
|
582
|
+
const bodyValueCount = open === "{" ? 2 : 1;
|
|
563
583
|
let cursor = bodyStart;
|
|
564
584
|
for (let index = 0; index < bodyValueCount; index += 1) {
|
|
565
585
|
cursor = this.skipValueFrom(cursor);
|
|
566
586
|
}
|
|
567
587
|
const bodyEnd = cursor;
|
|
568
588
|
this.pos = bodyEnd;
|
|
569
|
-
this.ensure(
|
|
589
|
+
this.ensure(close);
|
|
570
590
|
const afterClose = this.pos;
|
|
571
591
|
|
|
572
|
-
|
|
592
|
+
if (open === "[") return this.evalWhileArrayComprehension(condStart, bodyStart, bodyEnd, afterClose, condValue);
|
|
593
|
+
if (open === "{") return this.evalWhileObjectComprehension(condStart, bodyStart, bodyEnd, afterClose, condValue);
|
|
594
|
+
return this.evalWhileLoop(condStart, bodyStart, bodyEnd, afterClose, condValue);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private evalWhileLoop(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): unknown {
|
|
573
598
|
let last: unknown = undefined;
|
|
574
599
|
let currentCond = condValue;
|
|
575
600
|
while (isDefined(currentCond)) {
|
|
@@ -585,7 +610,6 @@ class CursorInterpreter {
|
|
|
585
610
|
last = undefined;
|
|
586
611
|
}
|
|
587
612
|
|
|
588
|
-
// Re-evaluate condition
|
|
589
613
|
currentCond = this.evalBodySlice(condStart, bodyStart);
|
|
590
614
|
}
|
|
591
615
|
|
|
@@ -593,6 +617,60 @@ class CursorInterpreter {
|
|
|
593
617
|
return last;
|
|
594
618
|
}
|
|
595
619
|
|
|
620
|
+
private evalWhileArrayComprehension(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): unknown[] | LoopControl {
|
|
621
|
+
const out: unknown[] = [];
|
|
622
|
+
let currentCond = condValue;
|
|
623
|
+
while (isDefined(currentCond)) {
|
|
624
|
+
this.tick();
|
|
625
|
+
this.selfStack.push(currentCond);
|
|
626
|
+
const value = this.evalBodySlice(bodyStart, bodyEnd);
|
|
627
|
+
this.selfStack.pop();
|
|
628
|
+
|
|
629
|
+
const control = this.handleLoopControl(value);
|
|
630
|
+
if (control) {
|
|
631
|
+
if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
|
|
632
|
+
if (control.kind === "break") break;
|
|
633
|
+
currentCond = this.evalBodySlice(condStart, bodyStart);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (isDefined(value)) out.push(value);
|
|
637
|
+
|
|
638
|
+
currentCond = this.evalBodySlice(condStart, bodyStart);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
this.pos = afterClose;
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private evalWhileObjectComprehension(condStart: number, bodyStart: number, bodyEnd: number, afterClose: number, condValue: unknown): Record<string, unknown> | LoopControl {
|
|
646
|
+
const result: Record<string, unknown> = {};
|
|
647
|
+
let currentCond = condValue;
|
|
648
|
+
while (isDefined(currentCond)) {
|
|
649
|
+
this.tick();
|
|
650
|
+
this.selfStack.push(currentCond);
|
|
651
|
+
const save = this.pos;
|
|
652
|
+
this.pos = bodyStart;
|
|
653
|
+
const key = this.evalValue();
|
|
654
|
+
const value = this.evalValue();
|
|
655
|
+
this.pos = save;
|
|
656
|
+
this.selfStack.pop();
|
|
657
|
+
|
|
658
|
+
const control = this.handleLoopControl(value);
|
|
659
|
+
if (control) {
|
|
660
|
+
if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
|
|
661
|
+
if (control.kind === "break") break;
|
|
662
|
+
currentCond = this.evalBodySlice(condStart, bodyStart);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (isDefined(value)) result[String(key)] = value;
|
|
666
|
+
|
|
667
|
+
currentCond = this.evalBodySlice(condStart, bodyStart);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
this.pos = afterClose;
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
|
|
596
674
|
private evalArrayComprehension(iterable: unknown, varA: string | undefined, varB: string | undefined, bodyStart: number, bodyEnd: number, keysOnly: boolean): unknown[] | LoopControl {
|
|
597
675
|
const items = this.iterate(iterable, keysOnly);
|
|
598
676
|
const out: unknown[] = [];
|
|
@@ -651,31 +729,38 @@ class CursorInterpreter {
|
|
|
651
729
|
return out;
|
|
652
730
|
}
|
|
653
731
|
|
|
654
|
-
private applyOpcode(id:
|
|
732
|
+
private applyOpcode(id: string, args: unknown[]): unknown {
|
|
655
733
|
const custom = this.customOpcodes.get(id);
|
|
656
734
|
if (custom) return custom(args, this.state);
|
|
657
735
|
switch (id) {
|
|
658
736
|
case OPCODES.do:
|
|
659
737
|
return args.length ? args[args.length - 1] : undefined;
|
|
660
738
|
case OPCODES.add:
|
|
739
|
+
if (args[0] === undefined || args[1] === undefined) return undefined;
|
|
661
740
|
if (typeof args[0] === "string" || typeof args[1] === "string") {
|
|
662
|
-
return String(args[0]
|
|
741
|
+
return String(args[0]) + String(args[1]);
|
|
663
742
|
}
|
|
664
|
-
return Number(args[0]
|
|
743
|
+
return Number(args[0]) + Number(args[1]);
|
|
665
744
|
case OPCODES.sub:
|
|
666
|
-
|
|
745
|
+
if (args[0] === undefined || args[1] === undefined) return undefined;
|
|
746
|
+
return Number(args[0]) - Number(args[1]);
|
|
667
747
|
case OPCODES.mul:
|
|
668
|
-
|
|
748
|
+
if (args[0] === undefined || args[1] === undefined) return undefined;
|
|
749
|
+
return Number(args[0]) * Number(args[1]);
|
|
669
750
|
case OPCODES.div:
|
|
670
|
-
|
|
751
|
+
if (args[0] === undefined || args[1] === undefined) return undefined;
|
|
752
|
+
return Number(args[0]) / Number(args[1]);
|
|
671
753
|
case OPCODES.mod:
|
|
672
|
-
|
|
754
|
+
if (args[0] === undefined || args[1] === undefined) return undefined;
|
|
755
|
+
return Number(args[0]) % Number(args[1]);
|
|
673
756
|
case OPCODES.neg:
|
|
674
|
-
|
|
757
|
+
if (args[0] === undefined) return undefined;
|
|
758
|
+
return -Number(args[0]);
|
|
675
759
|
case OPCODES.not: {
|
|
676
760
|
const value = args[0];
|
|
761
|
+
if (value === undefined) return undefined;
|
|
677
762
|
if (typeof value === "boolean") return !value;
|
|
678
|
-
return ~Number(value
|
|
763
|
+
return ~Number(value);
|
|
679
764
|
}
|
|
680
765
|
case OPCODES.and: {
|
|
681
766
|
const [a, b] = args;
|
|
@@ -714,6 +799,22 @@ class CursorInterpreter {
|
|
|
714
799
|
return Array.isArray(args[0]) ? args[0] : undefined;
|
|
715
800
|
case OPCODES.object:
|
|
716
801
|
return args[0] && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : undefined;
|
|
802
|
+
case OPCODES.range: {
|
|
803
|
+
const from = Number(args[0]);
|
|
804
|
+
const to = Number(args[1]);
|
|
805
|
+
const step = to >= from ? 1 : -1;
|
|
806
|
+
const out: number[] = [];
|
|
807
|
+
for (let v = from; step > 0 ? v <= to : v >= to; v += step)
|
|
808
|
+
out.push(v);
|
|
809
|
+
return out;
|
|
810
|
+
}
|
|
811
|
+
case OPCODES.size: {
|
|
812
|
+
const target = args[0];
|
|
813
|
+
if (Array.isArray(target)) return target.length;
|
|
814
|
+
if (typeof target === "string") return Array.from(target).length;
|
|
815
|
+
if (target && typeof target === "object") return Object.keys(target as Record<string, unknown>).length;
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
717
818
|
default:
|
|
718
819
|
throw new Error(`Unknown opcode ${id}`);
|
|
719
820
|
}
|
|
@@ -723,12 +824,55 @@ class CursorInterpreter {
|
|
|
723
824
|
let current = base;
|
|
724
825
|
for (const key of keys) {
|
|
725
826
|
if (current === undefined || current === null) return undefined;
|
|
726
|
-
current = (current
|
|
827
|
+
current = this.readProperty(current, key);
|
|
828
|
+
if (current === undefined) return undefined;
|
|
727
829
|
}
|
|
728
830
|
return current;
|
|
729
831
|
}
|
|
730
832
|
|
|
731
|
-
private
|
|
833
|
+
private readProperty(target: unknown, key: unknown): unknown {
|
|
834
|
+
const index = this.parseIndexKey(key);
|
|
835
|
+
if (Array.isArray(target)) {
|
|
836
|
+
if (index === undefined) return undefined;
|
|
837
|
+
return target[index];
|
|
838
|
+
}
|
|
839
|
+
if (typeof target === "string") {
|
|
840
|
+
if (index === undefined) return undefined;
|
|
841
|
+
return Array.from(target)[index];
|
|
842
|
+
}
|
|
843
|
+
if (this.isPlainObject(target)) {
|
|
844
|
+
const prop = String(key);
|
|
845
|
+
if (!Object.prototype.hasOwnProperty.call(target as Record<string, unknown>, prop)) return undefined;
|
|
846
|
+
return (target as Record<string, unknown>)[prop];
|
|
847
|
+
}
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private canWriteProperty(target: unknown, key: unknown): { kind: "array"; index: number } | { kind: "object" } | undefined {
|
|
852
|
+
const index = this.parseIndexKey(key);
|
|
853
|
+
if (Array.isArray(target)) {
|
|
854
|
+
if (index === undefined) return undefined;
|
|
855
|
+
return { kind: "array", index };
|
|
856
|
+
}
|
|
857
|
+
if (this.isPlainObject(target)) return { kind: "object" };
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private parseIndexKey(key: unknown): number | undefined {
|
|
862
|
+
if (typeof key === "number" && Number.isInteger(key) && key >= 0) return key;
|
|
863
|
+
if (typeof key !== "string" || key.length === 0) return undefined;
|
|
864
|
+
if (!/^(0|[1-9]\d*)$/.test(key)) return undefined;
|
|
865
|
+
const index = Number(key);
|
|
866
|
+
return Number.isSafeInteger(index) ? index : undefined;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
870
|
+
if (!value || typeof value !== "object") return false;
|
|
871
|
+
const proto = Object.getPrototypeOf(value);
|
|
872
|
+
return proto === Object.prototype || proto === null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private readPlace(): { root: string; keys: unknown[]; isRef: boolean } {
|
|
732
876
|
this.skipNonCode();
|
|
733
877
|
const direct = this.readRootVarOrRefIfPresent();
|
|
734
878
|
if (direct) {
|
|
@@ -774,7 +918,7 @@ class CursorInterpreter {
|
|
|
774
918
|
throw new Error(`Invalid place at ${this.pos}`);
|
|
775
919
|
}
|
|
776
920
|
|
|
777
|
-
private readRootVarOrRefIfPresent(): { root: string
|
|
921
|
+
private readRootVarOrRefIfPresent(): { root: string; isRef: boolean } | undefined {
|
|
778
922
|
const save = this.pos;
|
|
779
923
|
const prefix = this.readPrefix();
|
|
780
924
|
const tag = this.text[this.pos];
|
|
@@ -784,14 +928,14 @@ class CursorInterpreter {
|
|
|
784
928
|
}
|
|
785
929
|
this.pos += 1;
|
|
786
930
|
return {
|
|
787
|
-
root:
|
|
931
|
+
root: prefix.raw,
|
|
788
932
|
isRef: tag === "'",
|
|
789
933
|
};
|
|
790
934
|
}
|
|
791
935
|
|
|
792
|
-
private writePlace(place: { root: string
|
|
936
|
+
private writePlace(place: { root: string; keys: unknown[]; isRef: boolean }, value: unknown) {
|
|
793
937
|
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
794
|
-
const rootKey =
|
|
938
|
+
const rootKey = place.root;
|
|
795
939
|
if (place.keys.length === 0) {
|
|
796
940
|
rootTable[rootKey] = value;
|
|
797
941
|
return;
|
|
@@ -802,17 +946,44 @@ class CursorInterpreter {
|
|
|
802
946
|
rootTable[rootKey] = target;
|
|
803
947
|
}
|
|
804
948
|
for (let index = 0; index < place.keys.length - 1; index += 1) {
|
|
805
|
-
const key =
|
|
806
|
-
const
|
|
807
|
-
if (!
|
|
808
|
-
|
|
949
|
+
const key = place.keys[index];
|
|
950
|
+
const access = this.canWriteProperty(target, key);
|
|
951
|
+
if (!access) return;
|
|
952
|
+
if (access.kind === "array") {
|
|
953
|
+
const next = (target as unknown[])[access.index];
|
|
954
|
+
if (!next || typeof next !== "object") (target as unknown[])[access.index] = {};
|
|
955
|
+
target = (target as unknown[])[access.index] as Record<string, unknown>;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const prop = String(key);
|
|
959
|
+
const next = (target as Record<string, unknown>)[prop];
|
|
960
|
+
if (!next || typeof next !== "object") (target as Record<string, unknown>)[prop] = {};
|
|
961
|
+
target = (target as Record<string, unknown>)[prop];
|
|
809
962
|
}
|
|
810
|
-
|
|
963
|
+
const lastKey = place.keys[place.keys.length - 1];
|
|
964
|
+
const access = this.canWriteProperty(target, lastKey);
|
|
965
|
+
if (!access) return;
|
|
966
|
+
if (access.kind === "array") {
|
|
967
|
+
(target as unknown[])[access.index] = value;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
(target as Record<string, unknown>)[String(lastKey)] = value;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private readPlaceValue(place: { root: string; keys: unknown[]; isRef: boolean }): unknown {
|
|
974
|
+
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
975
|
+
let current: unknown = rootTable[place.root];
|
|
976
|
+
for (const key of place.keys) {
|
|
977
|
+
if (current === undefined || current === null) return undefined;
|
|
978
|
+
current = this.readProperty(current, key);
|
|
979
|
+
if (current === undefined) return undefined;
|
|
980
|
+
}
|
|
981
|
+
return current;
|
|
811
982
|
}
|
|
812
983
|
|
|
813
|
-
private deletePlace(place: { root: string
|
|
984
|
+
private deletePlace(place: { root: string; keys: unknown[]; isRef: boolean }) {
|
|
814
985
|
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
815
|
-
const rootKey =
|
|
986
|
+
const rootKey = place.root;
|
|
816
987
|
if (place.keys.length === 0) {
|
|
817
988
|
delete rootTable[rootKey];
|
|
818
989
|
return;
|
|
@@ -820,10 +991,25 @@ class CursorInterpreter {
|
|
|
820
991
|
let target = rootTable[rootKey];
|
|
821
992
|
if (!target || typeof target !== "object") return;
|
|
822
993
|
for (let index = 0; index < place.keys.length - 1; index += 1) {
|
|
823
|
-
|
|
994
|
+
const key = place.keys[index];
|
|
995
|
+
const access = this.canWriteProperty(target, key);
|
|
996
|
+
if (!access) return;
|
|
997
|
+
if (access.kind === "array") {
|
|
998
|
+
target = (target as unknown[])[access.index];
|
|
999
|
+
if (!target || typeof target !== "object") return;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
target = (target as Record<string, unknown>)[String(key)];
|
|
824
1003
|
if (!target || typeof target !== "object") return;
|
|
825
1004
|
}
|
|
826
|
-
|
|
1005
|
+
const lastKey = place.keys[place.keys.length - 1];
|
|
1006
|
+
const access = this.canWriteProperty(target, lastKey);
|
|
1007
|
+
if (!access) return;
|
|
1008
|
+
if (access.kind === "array") {
|
|
1009
|
+
delete (target as unknown[])[access.index];
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
delete (target as Record<string, unknown>)[String(lastKey)];
|
|
827
1013
|
}
|
|
828
1014
|
|
|
829
1015
|
private skipValue() {
|
|
@@ -842,12 +1028,12 @@ class CursorInterpreter {
|
|
|
842
1028
|
}
|
|
843
1029
|
|
|
844
1030
|
if (tag === ",") {
|
|
845
|
-
this.pos
|
|
1031
|
+
this.pos = this.advanceByBytes(this.pos + 1, prefix.value);
|
|
846
1032
|
const end = this.pos;
|
|
847
1033
|
this.pos = save;
|
|
848
1034
|
return end;
|
|
849
1035
|
}
|
|
850
|
-
if (tag === "=") {
|
|
1036
|
+
if (tag === "=" || tag === "/") {
|
|
851
1037
|
this.pos += 1;
|
|
852
1038
|
this.skipValue();
|
|
853
1039
|
this.skipValue();
|
|
@@ -882,7 +1068,8 @@ class CursorInterpreter {
|
|
|
882
1068
|
if (opener && "([{".includes(opener)) {
|
|
883
1069
|
const close = opener === "(" ? ")" : opener === "[" ? "]" : "}";
|
|
884
1070
|
if (prefix.value > 0) {
|
|
885
|
-
this.pos
|
|
1071
|
+
const bodyEnd = this.advanceByBytes(this.pos + 1, prefix.value);
|
|
1072
|
+
this.pos = bodyEnd + 1;
|
|
886
1073
|
const end = this.pos;
|
|
887
1074
|
this.pos = save;
|
|
888
1075
|
return end;
|