@creationix/rex 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +56 -0
- package/rex-cli.ts +3 -0
- package/rex-compile.ts +196 -0
- package/rex.ohm +408 -0
- package/rex.ohm-bundle.d.ts +195 -0
- package/rex.ohm-bundle.js +1 -0
- package/rex.ts +2418 -0
- package/rexc-interpreter.ts +848 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { compile } from "./rex.ts";
|
|
2
|
+
|
|
3
|
+
const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
|
|
4
|
+
const digitMap = new Map<string, number>(Array.from(DIGITS).map((char, index) => [char, index]));
|
|
5
|
+
|
|
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,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export type RexcContext = {
|
|
32
|
+
vars?: Record<string, unknown>;
|
|
33
|
+
refs?: Partial<Record<number, unknown>>;
|
|
34
|
+
self?: unknown;
|
|
35
|
+
selfStack?: unknown[];
|
|
36
|
+
opcodes?: Partial<Record<number, (args: unknown[], state: RexcRuntimeState) => unknown>>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RexcRuntimeState = {
|
|
40
|
+
vars: Record<string, unknown>;
|
|
41
|
+
refs: Record<number, unknown>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LoopControl = { kind: "break" | "continue"; depth: number };
|
|
45
|
+
|
|
46
|
+
type OpcodeMarker = { __opcode: number };
|
|
47
|
+
|
|
48
|
+
function decodePrefix(text: string, start: number, end: number): number {
|
|
49
|
+
let value = 0;
|
|
50
|
+
for (let index = start; index < end; index += 1) {
|
|
51
|
+
const digit = digitMap.get(text[index] ?? "") ?? -1;
|
|
52
|
+
if (digit < 0) throw new Error(`Invalid digit '${text[index]}'`);
|
|
53
|
+
value = value * 64 + digit;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decodeZigzag(value: number): number {
|
|
59
|
+
return value % 2 === 0 ? value / 2 : -(value + 1) / 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isValueStart(char: string | undefined): boolean {
|
|
63
|
+
if (!char) return false;
|
|
64
|
+
if (digitMap.has(char)) return true;
|
|
65
|
+
return "+*:%$@'^~=([{,?!|&><;".includes(char);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isDefined(value: unknown): boolean {
|
|
69
|
+
return value !== undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class CursorInterpreter {
|
|
73
|
+
private readonly text: string;
|
|
74
|
+
private pos = 0;
|
|
75
|
+
private readonly state: RexcRuntimeState;
|
|
76
|
+
private readonly selfStack: unknown[];
|
|
77
|
+
private readonly opcodeMarkers: OpcodeMarker[];
|
|
78
|
+
private readonly pointerCache = new Map<number, unknown>();
|
|
79
|
+
|
|
80
|
+
constructor(text: string, ctx: RexcContext = {}) {
|
|
81
|
+
const initialSelf = ctx.selfStack && ctx.selfStack.length > 0
|
|
82
|
+
? ctx.selfStack[ctx.selfStack.length - 1]
|
|
83
|
+
: ctx.self;
|
|
84
|
+
this.text = text;
|
|
85
|
+
this.state = {
|
|
86
|
+
vars: ctx.vars ?? {},
|
|
87
|
+
refs: {
|
|
88
|
+
0: ctx.refs?.[0],
|
|
89
|
+
1: ctx.refs?.[1] ?? true,
|
|
90
|
+
2: ctx.refs?.[2] ?? false,
|
|
91
|
+
3: ctx.refs?.[3] ?? null,
|
|
92
|
+
4: ctx.refs?.[4] ?? undefined,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
this.selfStack = ctx.selfStack && ctx.selfStack.length > 0 ? [...ctx.selfStack] : [initialSelf];
|
|
96
|
+
this.opcodeMarkers = Array.from({ length: 256 }, (_, id) => ({ __opcode: id }));
|
|
97
|
+
for (const [idText, value] of Object.entries(ctx.refs ?? {})) {
|
|
98
|
+
const id = Number(idText);
|
|
99
|
+
if (Number.isInteger(id)) this.state.refs[id] = value;
|
|
100
|
+
}
|
|
101
|
+
if (ctx.opcodes) {
|
|
102
|
+
for (const [idText, op] of Object.entries(ctx.opcodes)) {
|
|
103
|
+
if (op) this.customOpcodes.set(Number(idText), op);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private readonly customOpcodes = new Map<number, (args: unknown[], state: RexcRuntimeState) => unknown>();
|
|
109
|
+
|
|
110
|
+
private readSelf(depthPrefix: number): unknown {
|
|
111
|
+
const depth = depthPrefix + 1;
|
|
112
|
+
const index = this.selfStack.length - depth;
|
|
113
|
+
if (index < 0) return undefined;
|
|
114
|
+
return this.selfStack[index];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get runtimeState() {
|
|
118
|
+
return this.state;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
evaluateTopLevel(): unknown {
|
|
122
|
+
this.skipNonCode();
|
|
123
|
+
if (this.pos >= this.text.length) return undefined;
|
|
124
|
+
const value = this.evalValue();
|
|
125
|
+
this.skipNonCode();
|
|
126
|
+
if (this.pos < this.text.length) throw new Error(`Unexpected trailing input at ${this.pos}`);
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private skipNonCode() {
|
|
131
|
+
while (this.pos < this.text.length) {
|
|
132
|
+
const ch = this.text[this.pos];
|
|
133
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
134
|
+
this.pos += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === "/" && this.text[this.pos + 1] === "/") {
|
|
138
|
+
this.pos += 2;
|
|
139
|
+
while (this.pos < this.text.length && this.text[this.pos] !== "\n") this.pos += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (ch === "/" && this.text[this.pos + 1] === "*") {
|
|
143
|
+
this.pos += 2;
|
|
144
|
+
while (this.pos < this.text.length) {
|
|
145
|
+
if (this.text[this.pos] === "*" && this.text[this.pos + 1] === "/") {
|
|
146
|
+
this.pos += 2;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
this.pos += 1;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private readPrefix() {
|
|
158
|
+
const start = this.pos;
|
|
159
|
+
while (this.pos < this.text.length && digitMap.has(this.text[this.pos] ?? "")) this.pos += 1;
|
|
160
|
+
const end = this.pos;
|
|
161
|
+
return { start, end, value: decodePrefix(this.text, start, end), raw: this.text.slice(start, end) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private ensure(char: string) {
|
|
165
|
+
if (this.text[this.pos] !== char) throw new Error(`Expected '${char}' at ${this.pos}`);
|
|
166
|
+
this.pos += 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private hasMoreBefore(close: string) {
|
|
170
|
+
const save = this.pos;
|
|
171
|
+
this.skipNonCode();
|
|
172
|
+
const more = this.pos < this.text.length && this.text[this.pos] !== close;
|
|
173
|
+
this.pos = save;
|
|
174
|
+
return more;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private readBindingVarIfPresent(): string | undefined {
|
|
178
|
+
const save = this.pos;
|
|
179
|
+
this.skipNonCode();
|
|
180
|
+
const prefix = this.readPrefix();
|
|
181
|
+
const tag = this.text[this.pos];
|
|
182
|
+
if (tag === "$" && prefix.raw.length > 0) {
|
|
183
|
+
this.pos += 1;
|
|
184
|
+
return prefix.raw;
|
|
185
|
+
}
|
|
186
|
+
this.pos = save;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private evalValue(): unknown {
|
|
191
|
+
this.skipNonCode();
|
|
192
|
+
const prefix = this.readPrefix();
|
|
193
|
+
const tag = this.text[this.pos];
|
|
194
|
+
if (!tag) throw new Error("Unexpected end of input");
|
|
195
|
+
|
|
196
|
+
switch (tag) {
|
|
197
|
+
case "+":
|
|
198
|
+
this.pos += 1;
|
|
199
|
+
return decodeZigzag(prefix.value);
|
|
200
|
+
case "*": {
|
|
201
|
+
this.pos += 1;
|
|
202
|
+
const power = decodeZigzag(prefix.value);
|
|
203
|
+
const significand = this.evalValue();
|
|
204
|
+
if (typeof significand !== "number") throw new Error("Decimal significand must be numeric");
|
|
205
|
+
return significand * 10 ** power;
|
|
206
|
+
}
|
|
207
|
+
case ":":
|
|
208
|
+
this.pos += 1;
|
|
209
|
+
return prefix.raw;
|
|
210
|
+
case "%":
|
|
211
|
+
this.pos += 1;
|
|
212
|
+
return this.opcodeMarkers[prefix.value] ?? { __opcode: prefix.value };
|
|
213
|
+
case "@":
|
|
214
|
+
this.pos += 1;
|
|
215
|
+
return this.readSelf(prefix.value);
|
|
216
|
+
case "'":
|
|
217
|
+
this.pos += 1;
|
|
218
|
+
return this.state.refs[prefix.value];
|
|
219
|
+
case "$":
|
|
220
|
+
this.pos += 1;
|
|
221
|
+
return this.state.vars[prefix.raw];
|
|
222
|
+
case ",": {
|
|
223
|
+
this.pos += 1;
|
|
224
|
+
const start = this.pos;
|
|
225
|
+
const end = start + prefix.value;
|
|
226
|
+
if (end > this.text.length) throw new Error("String container overflows input");
|
|
227
|
+
const value = this.text.slice(start, end);
|
|
228
|
+
this.pos = end;
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
case "^": {
|
|
232
|
+
this.pos += 1;
|
|
233
|
+
const target = this.pos + prefix.value;
|
|
234
|
+
if (this.pointerCache.has(target)) return this.pointerCache.get(target);
|
|
235
|
+
const save = this.pos;
|
|
236
|
+
this.pos = target;
|
|
237
|
+
const value = this.evalValue();
|
|
238
|
+
this.pos = save;
|
|
239
|
+
this.pointerCache.set(target, value);
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
case "=": {
|
|
243
|
+
this.pos += 1;
|
|
244
|
+
const place = this.readPlace();
|
|
245
|
+
const value = this.evalValue();
|
|
246
|
+
this.writePlace(place, value);
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
case "~": {
|
|
250
|
+
this.pos += 1;
|
|
251
|
+
const place = this.readPlace();
|
|
252
|
+
this.deletePlace(place);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
case ";": {
|
|
256
|
+
this.pos += 1;
|
|
257
|
+
const kind: LoopControl["kind"] = prefix.value % 2 === 0 ? "break" : "continue";
|
|
258
|
+
const depth = Math.floor(prefix.value / 2) + 1;
|
|
259
|
+
return { kind, depth } satisfies LoopControl;
|
|
260
|
+
}
|
|
261
|
+
case "(":
|
|
262
|
+
return this.evalCall(prefix.value);
|
|
263
|
+
case "[":
|
|
264
|
+
return this.evalArray(prefix.value);
|
|
265
|
+
case "{":
|
|
266
|
+
return this.evalObject(prefix.value);
|
|
267
|
+
case "?":
|
|
268
|
+
case "!":
|
|
269
|
+
case "|":
|
|
270
|
+
case "&":
|
|
271
|
+
return this.evalFlowParen(tag);
|
|
272
|
+
case ">":
|
|
273
|
+
case "<":
|
|
274
|
+
return this.evalLoopLike(tag);
|
|
275
|
+
default:
|
|
276
|
+
throw new Error(`Unexpected tag '${tag}' at ${this.pos}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private evalCall(_prefix: number) {
|
|
281
|
+
this.ensure("(");
|
|
282
|
+
this.skipNonCode();
|
|
283
|
+
if (this.text[this.pos] === ")") {
|
|
284
|
+
this.pos += 1;
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const callee = this.evalValue();
|
|
288
|
+
const args: unknown[] = [];
|
|
289
|
+
while (true) {
|
|
290
|
+
this.skipNonCode();
|
|
291
|
+
if (this.text[this.pos] === ")") break;
|
|
292
|
+
args.push(this.evalValue());
|
|
293
|
+
}
|
|
294
|
+
this.ensure(")");
|
|
295
|
+
|
|
296
|
+
if (typeof callee === "object" && callee && "__opcode" in callee) {
|
|
297
|
+
return this.applyOpcode((callee as OpcodeMarker).__opcode, args);
|
|
298
|
+
}
|
|
299
|
+
return this.navigate(callee, args);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private evalArray(_prefix: number) {
|
|
303
|
+
this.ensure("[");
|
|
304
|
+
this.skipNonCode();
|
|
305
|
+
this.skipIndexHeaderIfPresent();
|
|
306
|
+
const out: unknown[] = [];
|
|
307
|
+
while (true) {
|
|
308
|
+
this.skipNonCode();
|
|
309
|
+
if (this.text[this.pos] === "]") break;
|
|
310
|
+
out.push(this.evalValue());
|
|
311
|
+
}
|
|
312
|
+
this.ensure("]");
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private evalObject(_prefix: number) {
|
|
317
|
+
this.ensure("{");
|
|
318
|
+
this.skipNonCode();
|
|
319
|
+
this.skipIndexHeaderIfPresent();
|
|
320
|
+
const out: Record<string, unknown> = {};
|
|
321
|
+
while (true) {
|
|
322
|
+
this.skipNonCode();
|
|
323
|
+
if (this.text[this.pos] === "}") break;
|
|
324
|
+
const key = this.evalValue();
|
|
325
|
+
const value = this.evalValue();
|
|
326
|
+
out[String(key)] = value;
|
|
327
|
+
}
|
|
328
|
+
this.ensure("}");
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private skipIndexHeaderIfPresent() {
|
|
333
|
+
const save = this.pos;
|
|
334
|
+
const countPrefix = this.readPrefix();
|
|
335
|
+
if (this.text[this.pos] !== "#") {
|
|
336
|
+
this.pos = save;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.pos += 1;
|
|
340
|
+
const widthChar = this.text[this.pos];
|
|
341
|
+
const width = widthChar ? digitMap.get(widthChar) ?? 0 : 0;
|
|
342
|
+
if (widthChar) this.pos += 1;
|
|
343
|
+
const skipLen = countPrefix.value * width;
|
|
344
|
+
this.pos += skipLen;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private evalFlowParen(tag: "?" | "!" | "|" | "&") {
|
|
348
|
+
this.pos += 1;
|
|
349
|
+
this.ensure("(");
|
|
350
|
+
if (tag === "?") {
|
|
351
|
+
const cond = this.evalValue();
|
|
352
|
+
if (isDefined(cond)) {
|
|
353
|
+
const thenValue = this.evalValue();
|
|
354
|
+
if (this.hasMoreBefore(")")) this.skipValue();
|
|
355
|
+
this.ensure(")");
|
|
356
|
+
return thenValue;
|
|
357
|
+
}
|
|
358
|
+
this.skipValue();
|
|
359
|
+
let elseValue: unknown = undefined;
|
|
360
|
+
if (this.hasMoreBefore(")")) elseValue = this.evalValue();
|
|
361
|
+
this.ensure(")");
|
|
362
|
+
return elseValue;
|
|
363
|
+
}
|
|
364
|
+
if (tag === "!") {
|
|
365
|
+
const cond = this.evalValue();
|
|
366
|
+
if (!isDefined(cond)) {
|
|
367
|
+
const thenValue = this.evalValue();
|
|
368
|
+
if (this.hasMoreBefore(")")) this.skipValue();
|
|
369
|
+
this.ensure(")");
|
|
370
|
+
return thenValue;
|
|
371
|
+
}
|
|
372
|
+
this.skipValue();
|
|
373
|
+
let elseValue: unknown = undefined;
|
|
374
|
+
if (this.hasMoreBefore(")")) elseValue = this.evalValue();
|
|
375
|
+
this.ensure(")");
|
|
376
|
+
return elseValue;
|
|
377
|
+
}
|
|
378
|
+
if (tag === "|") {
|
|
379
|
+
let result: unknown = undefined;
|
|
380
|
+
while (this.hasMoreBefore(")")) {
|
|
381
|
+
if (isDefined(result)) this.skipValue();
|
|
382
|
+
else result = this.evalValue();
|
|
383
|
+
}
|
|
384
|
+
this.ensure(")");
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
let result: unknown = undefined;
|
|
388
|
+
while (this.hasMoreBefore(")")) {
|
|
389
|
+
if (!isDefined(result) && result !== undefined) {
|
|
390
|
+
this.skipValue();
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
result = this.evalValue();
|
|
394
|
+
if (!isDefined(result)) {
|
|
395
|
+
while (this.hasMoreBefore(")")) this.skipValue();
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.ensure(")");
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private evalLoopLike(tag: ">" | "<") {
|
|
404
|
+
this.pos += 1;
|
|
405
|
+
const open = this.text[this.pos];
|
|
406
|
+
if (!open || !"([{".includes(open)) throw new Error(`Expected loop opener after '${tag}'`);
|
|
407
|
+
const close = open === "(" ? ")" : open === "[" ? "]" : "}";
|
|
408
|
+
this.pos += 1;
|
|
409
|
+
|
|
410
|
+
const iterable = this.evalValue();
|
|
411
|
+
const afterIterable = this.pos;
|
|
412
|
+
const bodyValueCount = open === "{" ? 2 : 1;
|
|
413
|
+
|
|
414
|
+
let varA: string | undefined;
|
|
415
|
+
let varB: string | undefined;
|
|
416
|
+
let bodyStart = afterIterable;
|
|
417
|
+
let bodyEnd = afterIterable;
|
|
418
|
+
|
|
419
|
+
let matched = false;
|
|
420
|
+
for (const bindingCount of [2, 1, 0]) {
|
|
421
|
+
this.pos = afterIterable;
|
|
422
|
+
const vars: string[] = [];
|
|
423
|
+
let bindingsOk = true;
|
|
424
|
+
for (let index = 0; index < bindingCount; index += 1) {
|
|
425
|
+
const binding = this.readBindingVarIfPresent();
|
|
426
|
+
if (!binding) {
|
|
427
|
+
bindingsOk = false;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
vars.push(binding);
|
|
431
|
+
}
|
|
432
|
+
if (!bindingsOk) continue;
|
|
433
|
+
|
|
434
|
+
const candidateBodyStart = this.pos;
|
|
435
|
+
let cursor = candidateBodyStart;
|
|
436
|
+
let bodyOk = true;
|
|
437
|
+
for (let index = 0; index < bodyValueCount; index += 1) {
|
|
438
|
+
const next = this.skipValueFrom(cursor);
|
|
439
|
+
if (next <= cursor) {
|
|
440
|
+
bodyOk = false;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
cursor = next;
|
|
444
|
+
}
|
|
445
|
+
if (!bodyOk) continue;
|
|
446
|
+
|
|
447
|
+
this.pos = cursor;
|
|
448
|
+
this.skipNonCode();
|
|
449
|
+
if (this.text[this.pos] !== close) continue;
|
|
450
|
+
|
|
451
|
+
varA = vars[0];
|
|
452
|
+
varB = vars[1];
|
|
453
|
+
bodyStart = candidateBodyStart;
|
|
454
|
+
bodyEnd = cursor;
|
|
455
|
+
matched = true;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!matched) throw new Error(`Invalid loop/comprehension body before '${close}' at ${this.pos}`);
|
|
460
|
+
this.pos = bodyEnd;
|
|
461
|
+
this.ensure(close);
|
|
462
|
+
|
|
463
|
+
if (open === "[") return this.evalArrayComprehension(iterable, varA, varB, bodyStart, bodyEnd, tag === "<");
|
|
464
|
+
if (open === "{") return this.evalObjectComprehension(iterable, varA, varB, bodyStart, bodyEnd, tag === "<");
|
|
465
|
+
return this.evalForLoop(iterable, varA, varB, bodyStart, bodyEnd, tag === "<");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private iterate(iterable: unknown, keysOnly: boolean): Array<{ key: unknown; value: unknown }> {
|
|
469
|
+
if (Array.isArray(iterable)) {
|
|
470
|
+
if (keysOnly) return iterable.map((_value, index) => ({ key: index, value: index }));
|
|
471
|
+
return iterable.map((value, index) => ({ key: index, value }));
|
|
472
|
+
}
|
|
473
|
+
if (iterable && typeof iterable === "object") {
|
|
474
|
+
const entries = Object.entries(iterable as Record<string, unknown>);
|
|
475
|
+
if (keysOnly) return entries.map(([key]) => ({ key, value: key }));
|
|
476
|
+
return entries.map(([key, value]) => ({ key, value }));
|
|
477
|
+
}
|
|
478
|
+
if (typeof iterable === "number" && Number.isFinite(iterable) && iterable > 0) {
|
|
479
|
+
const out: Array<{ key: unknown; value: unknown }> = [];
|
|
480
|
+
for (let index = 0; index < Math.floor(iterable); index += 1) {
|
|
481
|
+
if (keysOnly) out.push({ key: index, value: index });
|
|
482
|
+
else out.push({ key: index, value: index + 1 });
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private evalBodySlice(start: number, end: number): unknown {
|
|
490
|
+
const save = this.pos;
|
|
491
|
+
this.pos = start;
|
|
492
|
+
const value = this.evalValue();
|
|
493
|
+
this.pos = end;
|
|
494
|
+
this.pos = save;
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private handleLoopControl(value: unknown): LoopControl | undefined {
|
|
499
|
+
if (value && typeof value === "object" && "kind" in value && "depth" in value) {
|
|
500
|
+
return value as LoopControl;
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private evalForLoop(iterable: unknown, varA: string | undefined, varB: string | undefined, bodyStart: number, bodyEnd: number, keysOnly: boolean): unknown {
|
|
506
|
+
const items = this.iterate(iterable, keysOnly);
|
|
507
|
+
let last: unknown = undefined;
|
|
508
|
+
for (const item of items) {
|
|
509
|
+
const currentSelf = keysOnly ? item.key : item.value;
|
|
510
|
+
this.selfStack.push(currentSelf);
|
|
511
|
+
if (varA && varB) {
|
|
512
|
+
this.state.vars[varA] = item.key;
|
|
513
|
+
this.state.vars[varB] = keysOnly ? item.key : item.value;
|
|
514
|
+
}
|
|
515
|
+
else if (varA) {
|
|
516
|
+
this.state.vars[varA] = keysOnly ? item.key : item.value;
|
|
517
|
+
}
|
|
518
|
+
last = this.evalBodySlice(bodyStart, bodyEnd);
|
|
519
|
+
this.selfStack.pop();
|
|
520
|
+
const control = this.handleLoopControl(last);
|
|
521
|
+
if (!control) continue;
|
|
522
|
+
if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
|
|
523
|
+
if (control.kind === "break") return undefined;
|
|
524
|
+
last = undefined;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
return last;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private evalArrayComprehension(iterable: unknown, varA: string | undefined, varB: string | undefined, bodyStart: number, bodyEnd: number, keysOnly: boolean): unknown[] | LoopControl {
|
|
531
|
+
const items = this.iterate(iterable, keysOnly);
|
|
532
|
+
const out: unknown[] = [];
|
|
533
|
+
for (const item of items) {
|
|
534
|
+
const currentSelf = keysOnly ? item.key : item.value;
|
|
535
|
+
this.selfStack.push(currentSelf);
|
|
536
|
+
if (varA && varB) {
|
|
537
|
+
this.state.vars[varA] = item.key;
|
|
538
|
+
this.state.vars[varB] = keysOnly ? item.key : item.value;
|
|
539
|
+
}
|
|
540
|
+
else if (varA) {
|
|
541
|
+
this.state.vars[varA] = keysOnly ? item.key : item.value;
|
|
542
|
+
}
|
|
543
|
+
const value = this.evalBodySlice(bodyStart, bodyEnd);
|
|
544
|
+
this.selfStack.pop();
|
|
545
|
+
const control = this.handleLoopControl(value);
|
|
546
|
+
if (control) {
|
|
547
|
+
if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
|
|
548
|
+
if (control.kind === "break") break;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (isDefined(value)) out.push(value);
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private evalObjectComprehension(iterable: unknown, varA: string | undefined, varB: string | undefined, bodyStart: number, bodyEnd: number, keysOnly: boolean): Record<string, unknown> | LoopControl {
|
|
557
|
+
const items = this.iterate(iterable, keysOnly);
|
|
558
|
+
const out: Record<string, unknown> = {};
|
|
559
|
+
for (const item of items) {
|
|
560
|
+
const currentSelf = keysOnly ? item.key : item.value;
|
|
561
|
+
this.selfStack.push(currentSelf);
|
|
562
|
+
if (varA && varB) {
|
|
563
|
+
this.state.vars[varA] = item.key;
|
|
564
|
+
this.state.vars[varB] = keysOnly ? item.key : item.value;
|
|
565
|
+
}
|
|
566
|
+
else if (varA) {
|
|
567
|
+
this.state.vars[varA] = keysOnly ? item.key : item.value;
|
|
568
|
+
}
|
|
569
|
+
const save = this.pos;
|
|
570
|
+
this.pos = bodyStart;
|
|
571
|
+
const key = this.evalValue();
|
|
572
|
+
const value = this.evalValue();
|
|
573
|
+
this.pos = save;
|
|
574
|
+
this.selfStack.pop();
|
|
575
|
+
const control = this.handleLoopControl(value);
|
|
576
|
+
if (control) {
|
|
577
|
+
if (control.depth > 1) return { kind: control.kind, depth: control.depth - 1 } satisfies LoopControl;
|
|
578
|
+
if (control.kind === "break") break;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (isDefined(value)) out[String(key)] = value;
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private applyOpcode(id: number, args: unknown[]): unknown {
|
|
587
|
+
const custom = this.customOpcodes.get(id);
|
|
588
|
+
if (custom) return custom(args, this.state);
|
|
589
|
+
switch (id) {
|
|
590
|
+
case OPCODES.do:
|
|
591
|
+
return args.length ? args[args.length - 1] : undefined;
|
|
592
|
+
case OPCODES.add:
|
|
593
|
+
if (typeof args[0] === "string" || typeof args[1] === "string") {
|
|
594
|
+
return String(args[0] ?? "") + String(args[1] ?? "");
|
|
595
|
+
}
|
|
596
|
+
return Number(args[0] ?? 0) + Number(args[1] ?? 0);
|
|
597
|
+
case OPCODES.sub:
|
|
598
|
+
return Number(args[0] ?? 0) - Number(args[1] ?? 0);
|
|
599
|
+
case OPCODES.mul:
|
|
600
|
+
return Number(args[0] ?? 0) * Number(args[1] ?? 0);
|
|
601
|
+
case OPCODES.div:
|
|
602
|
+
return Number(args[0] ?? 0) / Number(args[1] ?? 0);
|
|
603
|
+
case OPCODES.mod:
|
|
604
|
+
return Number(args[0] ?? 0) % Number(args[1] ?? 0);
|
|
605
|
+
case OPCODES.neg:
|
|
606
|
+
return -Number(args[0] ?? 0);
|
|
607
|
+
case OPCODES.not: {
|
|
608
|
+
const value = args[0];
|
|
609
|
+
if (typeof value === "boolean") return !value;
|
|
610
|
+
return ~Number(value ?? 0);
|
|
611
|
+
}
|
|
612
|
+
case OPCODES.and: {
|
|
613
|
+
const [a, b] = args;
|
|
614
|
+
if (typeof a === "boolean" || typeof b === "boolean") return Boolean(a) && Boolean(b);
|
|
615
|
+
return Number(a ?? 0) & Number(b ?? 0);
|
|
616
|
+
}
|
|
617
|
+
case OPCODES.or: {
|
|
618
|
+
const [a, b] = args;
|
|
619
|
+
if (typeof a === "boolean" || typeof b === "boolean") return Boolean(a) || Boolean(b);
|
|
620
|
+
return Number(a ?? 0) | Number(b ?? 0);
|
|
621
|
+
}
|
|
622
|
+
case OPCODES.xor: {
|
|
623
|
+
const [a, b] = args;
|
|
624
|
+
if (typeof a === "boolean" || typeof b === "boolean") return Boolean(a) !== Boolean(b);
|
|
625
|
+
return Number(a ?? 0) ^ Number(b ?? 0);
|
|
626
|
+
}
|
|
627
|
+
case OPCODES.eq:
|
|
628
|
+
return args[0] === args[1] ? args[0] : undefined;
|
|
629
|
+
case OPCODES.neq:
|
|
630
|
+
return args[0] !== args[1] ? args[0] : undefined;
|
|
631
|
+
case OPCODES.gt:
|
|
632
|
+
return Number(args[0]) > Number(args[1]) ? args[0] : undefined;
|
|
633
|
+
case OPCODES.gte:
|
|
634
|
+
return Number(args[0]) >= Number(args[1]) ? args[0] : undefined;
|
|
635
|
+
case OPCODES.lt:
|
|
636
|
+
return Number(args[0]) < Number(args[1]) ? args[0] : undefined;
|
|
637
|
+
case OPCODES.lte:
|
|
638
|
+
return Number(args[0]) <= Number(args[1]) ? args[0] : undefined;
|
|
639
|
+
case OPCODES.boolean:
|
|
640
|
+
return typeof args[0] === "boolean" ? args[0] : undefined;
|
|
641
|
+
case OPCODES.number:
|
|
642
|
+
return typeof args[0] === "number" ? args[0] : undefined;
|
|
643
|
+
case OPCODES.string:
|
|
644
|
+
return typeof args[0] === "string" ? args[0] : undefined;
|
|
645
|
+
case OPCODES.array:
|
|
646
|
+
return Array.isArray(args[0]) ? args[0] : undefined;
|
|
647
|
+
case OPCODES.object:
|
|
648
|
+
return args[0] && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : undefined;
|
|
649
|
+
default:
|
|
650
|
+
throw new Error(`Unknown opcode ${id}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private navigate(base: unknown, keys: unknown[]): unknown {
|
|
655
|
+
let current = base;
|
|
656
|
+
for (const key of keys) {
|
|
657
|
+
if (current === undefined || current === null) return undefined;
|
|
658
|
+
current = (current as Record<string, unknown>)[String(key)];
|
|
659
|
+
}
|
|
660
|
+
return current;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private readPlace(): { root: string | number; keys: unknown[]; isRef: boolean } {
|
|
664
|
+
this.skipNonCode();
|
|
665
|
+
const direct = this.readRootVarOrRefIfPresent();
|
|
666
|
+
if (direct) {
|
|
667
|
+
const keys: unknown[] = [];
|
|
668
|
+
this.skipNonCode();
|
|
669
|
+
if (this.text[this.pos] === "(") {
|
|
670
|
+
this.pos += 1;
|
|
671
|
+
while (true) {
|
|
672
|
+
this.skipNonCode();
|
|
673
|
+
if (this.text[this.pos] === ")") break;
|
|
674
|
+
keys.push(this.evalValue());
|
|
675
|
+
}
|
|
676
|
+
this.pos += 1;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
root: direct.root,
|
|
680
|
+
keys,
|
|
681
|
+
isRef: direct.isRef,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (this.text[this.pos] === "(") {
|
|
686
|
+
this.pos += 1;
|
|
687
|
+
this.skipNonCode();
|
|
688
|
+
const rootFromNav = this.readRootVarOrRefIfPresent();
|
|
689
|
+
if (!rootFromNav) throw new Error(`Invalid place root at ${this.pos}`);
|
|
690
|
+
|
|
691
|
+
const keys: unknown[] = [];
|
|
692
|
+
while (true) {
|
|
693
|
+
this.skipNonCode();
|
|
694
|
+
if (this.text[this.pos] === ")") break;
|
|
695
|
+
keys.push(this.evalValue());
|
|
696
|
+
}
|
|
697
|
+
this.pos += 1;
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
root: rootFromNav.root,
|
|
701
|
+
keys,
|
|
702
|
+
isRef: rootFromNav.isRef,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
throw new Error(`Invalid place at ${this.pos}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private readRootVarOrRefIfPresent(): { root: string | number; isRef: boolean } | undefined {
|
|
710
|
+
const save = this.pos;
|
|
711
|
+
const prefix = this.readPrefix();
|
|
712
|
+
const tag = this.text[this.pos];
|
|
713
|
+
if (tag !== "$" && tag !== "'") {
|
|
714
|
+
this.pos = save;
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
this.pos += 1;
|
|
718
|
+
return {
|
|
719
|
+
root: tag === "$" ? prefix.raw : prefix.value,
|
|
720
|
+
isRef: tag === "'",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private writePlace(place: { root: string | number; keys: unknown[]; isRef: boolean }, value: unknown) {
|
|
725
|
+
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
726
|
+
const rootKey = String(place.root);
|
|
727
|
+
if (place.keys.length === 0) {
|
|
728
|
+
rootTable[rootKey] = value;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
let target = rootTable[rootKey];
|
|
732
|
+
if (!target || typeof target !== "object") {
|
|
733
|
+
target = {};
|
|
734
|
+
rootTable[rootKey] = target;
|
|
735
|
+
}
|
|
736
|
+
for (let index = 0; index < place.keys.length - 1; index += 1) {
|
|
737
|
+
const key = String(place.keys[index]);
|
|
738
|
+
const next = (target as Record<string, unknown>)[key];
|
|
739
|
+
if (!next || typeof next !== "object") (target as Record<string, unknown>)[key] = {};
|
|
740
|
+
target = (target as Record<string, unknown>)[key];
|
|
741
|
+
}
|
|
742
|
+
(target as Record<string, unknown>)[String(place.keys[place.keys.length - 1])] = value;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private deletePlace(place: { root: string | number; keys: unknown[]; isRef: boolean }) {
|
|
746
|
+
const rootTable = place.isRef ? this.state.refs : this.state.vars;
|
|
747
|
+
const rootKey = String(place.root);
|
|
748
|
+
if (place.keys.length === 0) {
|
|
749
|
+
delete rootTable[rootKey];
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
let target = rootTable[rootKey];
|
|
753
|
+
if (!target || typeof target !== "object") return;
|
|
754
|
+
for (let index = 0; index < place.keys.length - 1; index += 1) {
|
|
755
|
+
target = (target as Record<string, unknown>)[String(place.keys[index])];
|
|
756
|
+
if (!target || typeof target !== "object") return;
|
|
757
|
+
}
|
|
758
|
+
delete (target as Record<string, unknown>)[String(place.keys[place.keys.length - 1])];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private skipValue() {
|
|
762
|
+
this.pos = this.skipValueFrom(this.pos);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private skipValueFrom(startPos: number): number {
|
|
766
|
+
const save = this.pos;
|
|
767
|
+
this.pos = startPos;
|
|
768
|
+
this.skipNonCode();
|
|
769
|
+
const prefix = this.readPrefix();
|
|
770
|
+
const tag = this.text[this.pos];
|
|
771
|
+
if (!tag) {
|
|
772
|
+
this.pos = save;
|
|
773
|
+
return startPos;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (tag === ",") {
|
|
777
|
+
this.pos += 1 + prefix.value;
|
|
778
|
+
const end = this.pos;
|
|
779
|
+
this.pos = save;
|
|
780
|
+
return end;
|
|
781
|
+
}
|
|
782
|
+
if (tag === "=") {
|
|
783
|
+
this.pos += 1;
|
|
784
|
+
this.skipValue();
|
|
785
|
+
this.skipValue();
|
|
786
|
+
const end = this.pos;
|
|
787
|
+
this.pos = save;
|
|
788
|
+
return end;
|
|
789
|
+
}
|
|
790
|
+
if (tag === "~") {
|
|
791
|
+
this.pos += 1;
|
|
792
|
+
this.skipValue();
|
|
793
|
+
const end = this.pos;
|
|
794
|
+
this.pos = save;
|
|
795
|
+
return end;
|
|
796
|
+
}
|
|
797
|
+
if (tag === "*") {
|
|
798
|
+
this.pos += 1;
|
|
799
|
+
this.skipValue();
|
|
800
|
+
const end = this.pos;
|
|
801
|
+
this.pos = save;
|
|
802
|
+
return end;
|
|
803
|
+
}
|
|
804
|
+
if ("+:%$@'^;".includes(tag)) {
|
|
805
|
+
this.pos += 1;
|
|
806
|
+
const end = this.pos;
|
|
807
|
+
this.pos = save;
|
|
808
|
+
return end;
|
|
809
|
+
}
|
|
810
|
+
if (tag === "?" || tag === "!" || tag === "|" || tag === "&" || tag === ">" || tag === "<") {
|
|
811
|
+
this.pos += 1;
|
|
812
|
+
}
|
|
813
|
+
const opener = this.text[this.pos];
|
|
814
|
+
if (opener && "([{".includes(opener)) {
|
|
815
|
+
const close = opener === "(" ? ")" : opener === "[" ? "]" : "}";
|
|
816
|
+
if (prefix.value > 0) {
|
|
817
|
+
this.pos += 1 + prefix.value + 1;
|
|
818
|
+
const end = this.pos;
|
|
819
|
+
this.pos = save;
|
|
820
|
+
return end;
|
|
821
|
+
}
|
|
822
|
+
this.pos += 1;
|
|
823
|
+
while (true) {
|
|
824
|
+
this.skipNonCode();
|
|
825
|
+
if (this.text[this.pos] === close) break;
|
|
826
|
+
this.skipValue();
|
|
827
|
+
}
|
|
828
|
+
this.pos += 1;
|
|
829
|
+
const end = this.pos;
|
|
830
|
+
this.pos = save;
|
|
831
|
+
return end;
|
|
832
|
+
}
|
|
833
|
+
this.pos += 1;
|
|
834
|
+
const end = this.pos;
|
|
835
|
+
this.pos = save;
|
|
836
|
+
return end;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function evaluateRexc(text: string, ctx: RexcContext = {}) {
|
|
841
|
+
const interpreter = new CursorInterpreter(text, ctx);
|
|
842
|
+
const value = interpreter.evaluateTopLevel();
|
|
843
|
+
return { value, state: interpreter.runtimeState };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function evaluateSource(source: string, ctx: RexcContext = {}) {
|
|
847
|
+
return evaluateRexc(compile(source), ctx);
|
|
848
|
+
}
|