@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.
- package/README.md +24 -0
- package/package.json +9 -6
- package/rex-cli.js +1334 -1190
- package/rex-cli.ts +268 -27
- package/rex-repl.js +1048 -1135
- package/rex-repl.ts +392 -103
- package/rex.js +290 -954
- package/rex.ohm +48 -21
- package/rex.ohm-bundle.cjs +1 -1
- package/rex.ohm-bundle.d.ts +27 -8
- package/rex.ohm-bundle.js +1 -1
- package/rex.ts +388 -218
- package/rexc-interpreter.ts +386 -88
- package/rx-cli.js +2836 -0
- package/rx-cli.ts +298 -0
package/rexc-interpreter.ts
CHANGED
|
@@ -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:
|
|
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",
|
|
29
30
|
} as const;
|
|
30
31
|
|
|
31
32
|
export type RexcContext = {
|
|
32
33
|
vars?: Record<string, unknown>;
|
|
33
|
-
refs?:
|
|
34
|
+
refs?: Record<string, unknown>;
|
|
34
35
|
self?: unknown;
|
|
35
36
|
selfStack?: unknown[];
|
|
36
|
-
opcodes?:
|
|
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<
|
|
44
|
+
refs: Record<string, unknown>;
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
type LoopControl = { kind: "break" | "continue"; depth: number };
|
|
47
48
|
|
|
48
|
-
type OpcodeMarker = { __opcode:
|
|
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 "+*:%$@'
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 [
|
|
118
|
-
if (op) this.customOpcodes.set(
|
|
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<
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 === "
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
569
|
+
private evalWhileLike(): unknown {
|
|
555
570
|
this.pos += 1; // skip '#'
|
|
556
|
-
this.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
666
|
-
return
|
|
741
|
+
if (Array.isArray(args[0]) && Array.isArray(args[1])) {
|
|
742
|
+
return [...args[0], ...args[1]];
|
|
667
743
|
}
|
|
668
|
-
|
|
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
|
|
906
|
+
current = this.readProperty(current, key);
|
|
907
|
+
if (current === undefined) return undefined;
|
|
737
908
|
}
|
|
738
909
|
return current;
|
|
739
910
|
}
|
|
740
911
|
|
|
741
|
-
private
|
|
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
|
|
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:
|
|
1052
|
+
root: prefix.raw,
|
|
798
1053
|
isRef: tag === "'",
|
|
799
1054
|
};
|
|
800
1055
|
}
|
|
801
1056
|
|
|
802
|
-
private writePlace(place: { root: string
|
|
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 =
|
|
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 =
|
|
816
|
-
const
|
|
817
|
-
if (!
|
|
818
|
-
|
|
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(
|
|
1091
|
+
(target as Record<string, unknown>)[String(lastKey)] = value;
|
|
821
1092
|
}
|
|
822
1093
|
|
|
823
|
-
private
|
|
1094
|
+
private readPlaceValue(place: { root: string; keys: unknown[]; isRef: boolean }): unknown {
|
|
824
1095
|
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|