@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/rex-repl.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import {
|
|
3
|
+
import { readdirSync, statSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve, dirname, basename } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { grammar, stringify, parseToIR, optimizeIR, compile, formatParseError } from "./rex.ts";
|
|
4
7
|
import { evaluateRexc } from "./rexc-interpreter.ts";
|
|
5
8
|
|
|
6
9
|
const req = createRequire(import.meta.url);
|
|
@@ -8,23 +11,69 @@ const { version } = req("./package.json");
|
|
|
8
11
|
|
|
9
12
|
// ── ANSI helpers ──────────────────────────────────────────────
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
function createColors(enabled: boolean) {
|
|
15
|
+
if (!enabled) {
|
|
16
|
+
return {
|
|
17
|
+
reset: "",
|
|
18
|
+
bold: "",
|
|
19
|
+
dim: "",
|
|
20
|
+
red: "",
|
|
21
|
+
green: "",
|
|
22
|
+
yellow: "",
|
|
23
|
+
blue: "",
|
|
24
|
+
magenta: "",
|
|
25
|
+
cyan: "",
|
|
26
|
+
gray: "",
|
|
27
|
+
keyword: "",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
reset: "\x1b[0m",
|
|
32
|
+
bold: "\x1b[1m",
|
|
33
|
+
dim: "\x1b[2m",
|
|
34
|
+
red: "\x1b[38;5;203m",
|
|
35
|
+
green: "\x1b[38;5;114m",
|
|
36
|
+
yellow: "\x1b[38;5;179m",
|
|
37
|
+
blue: "\x1b[38;5;75m",
|
|
38
|
+
magenta: "\x1b[38;5;141m",
|
|
39
|
+
cyan: "\x1b[38;5;81m",
|
|
40
|
+
gray: "\x1b[38;5;245m",
|
|
41
|
+
keyword: "\x1b[1;38;5;208m",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let colorEnabled = process.stdout.isTTY;
|
|
46
|
+
let C = createColors(colorEnabled);
|
|
47
|
+
|
|
48
|
+
export function setColorEnabled(enabled: boolean) {
|
|
49
|
+
colorEnabled = enabled;
|
|
50
|
+
C = createColors(enabled);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type OutputFormat = "rex" | "json";
|
|
54
|
+
|
|
55
|
+
function formatJson(value: unknown, indent = 2): string {
|
|
56
|
+
const normalized = normalizeJsonValue(value, false);
|
|
57
|
+
const text = JSON.stringify(normalized, null, indent);
|
|
58
|
+
return text ?? "null";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeJsonValue(value: unknown, inArray: boolean): unknown {
|
|
62
|
+
if (value === undefined) return inArray ? null : undefined;
|
|
63
|
+
if (value === null || typeof value !== "object") return value;
|
|
64
|
+
if (Array.isArray(value)) return value.map((item) => normalizeJsonValue(item, true));
|
|
65
|
+
const out: Record<string, unknown> = {};
|
|
66
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
67
|
+
const normalized = normalizeJsonValue(val, false);
|
|
68
|
+
if (normalized !== undefined) out[key] = normalized;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
23
72
|
|
|
24
73
|
// ── Syntax highlighting ───────────────────────────────────────
|
|
25
74
|
|
|
26
75
|
const TOKEN_RE =
|
|
27
|
-
/(?<blockComment>\/\*[\s\S]*?(?:\*\/|$))|(?<lineComment>\/\/[^\n]*)|(?<dstring>"(?:[^"\\]|\\.)*"?)|(?<sstring>'(?:[^'\\]|\\.)*'?)|(?<keyword>\b(?:when|unless|while|for|do|end|in|of|and|or|else|break|continue|delete|self)(?![a-zA-Z0-9_-]))|(?<literal>\b(?:true|false|null|undefined|nan)(?![a-zA-Z0-9_-])|-?\binf\b)|(?<typePred>\b(?:string|number|object|array|boolean)(?![a-zA-Z0-9_-]))|(?<num>\b(?:0x[0-9a-fA-F]+|0b[01]+|(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)\b)/g;
|
|
76
|
+
/(?<blockComment>\/\*[\s\S]*?(?:\*\/|$))|(?<lineComment>\/\/[^\n]*)|(?<dstring>"(?:[^"\\]|\\.)*"?)|(?<sstring>'(?:[^'\\]|\\.)*'?)|(?<objKey>\b[A-Za-z_][A-Za-z0-9_-]*\b)(?=\s*:)|(?<keyword>\b(?:when|unless|while|for|do|end|in|of|and|or|nor|else|break|continue|delete|self)(?![a-zA-Z0-9_-]))|(?<literal>\b(?:true|false|null|undefined|nan)(?![a-zA-Z0-9_-])|-?\binf\b)|(?<typePred>\b(?:string|number|object|array|boolean)(?![a-zA-Z0-9_-]))|(?<num>\b(?:0x[0-9a-fA-F]+|0b[01]+|(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)\b)|(?<identifier>\b[A-Za-z_][A-Za-z0-9_.-]*\b)/g;
|
|
28
77
|
|
|
29
78
|
export function highlightLine(line: string): string {
|
|
30
79
|
let result = "";
|
|
@@ -39,14 +88,18 @@ export function highlightLine(line: string): string {
|
|
|
39
88
|
result += C.gray + text + C.reset;
|
|
40
89
|
} else if (g.dstring || g.sstring) {
|
|
41
90
|
result += C.green + text + C.reset;
|
|
91
|
+
} else if (g.objKey) {
|
|
92
|
+
result += C.magenta + text + C.reset;
|
|
42
93
|
} else if (g.keyword) {
|
|
43
|
-
result += C.
|
|
94
|
+
result += C.keyword + text + C.reset;
|
|
44
95
|
} else if (g.literal) {
|
|
45
96
|
result += C.yellow + text + C.reset;
|
|
46
97
|
} else if (g.typePred) {
|
|
47
98
|
result += C.cyan + text + C.reset;
|
|
48
99
|
} else if (g.num) {
|
|
49
100
|
result += C.cyan + text + C.reset;
|
|
101
|
+
} else if (g.identifier) {
|
|
102
|
+
result += C.blue + text + C.reset;
|
|
50
103
|
} else {
|
|
51
104
|
result += text;
|
|
52
105
|
}
|
|
@@ -118,7 +171,7 @@ export function highlightRexc(text: string): string {
|
|
|
118
171
|
i++;
|
|
119
172
|
break;
|
|
120
173
|
case "%": // opcode
|
|
121
|
-
out += C.
|
|
174
|
+
out += C.keyword + prefix + tag + C.reset;
|
|
122
175
|
i++;
|
|
123
176
|
break;
|
|
124
177
|
case "$": // variable
|
|
@@ -143,6 +196,7 @@ export function highlightRexc(text: string): string {
|
|
|
143
196
|
break;
|
|
144
197
|
}
|
|
145
198
|
case "=": // assignment
|
|
199
|
+
case "/": // swap-assign
|
|
146
200
|
case "~": // delete
|
|
147
201
|
out += C.red + prefix + tag + C.reset;
|
|
148
202
|
i++;
|
|
@@ -154,11 +208,11 @@ export function highlightRexc(text: string): string {
|
|
|
154
208
|
case ">": // for
|
|
155
209
|
case "<": // for-keys
|
|
156
210
|
case "#": // while
|
|
157
|
-
out += C.
|
|
211
|
+
out += C.keyword + prefix + tag + C.reset;
|
|
158
212
|
i++;
|
|
159
213
|
break;
|
|
160
214
|
case ";": // break/continue
|
|
161
|
-
out += C.
|
|
215
|
+
out += C.keyword + prefix + tag + C.reset;
|
|
162
216
|
i++;
|
|
163
217
|
break;
|
|
164
218
|
case "^": // pointer
|
|
@@ -180,6 +234,20 @@ export function highlightRexc(text: string): string {
|
|
|
180
234
|
return out;
|
|
181
235
|
}
|
|
182
236
|
|
|
237
|
+
export function highlightAuto(text: string, hint?: "rex" | "rexc"): string {
|
|
238
|
+
if (hint === "rexc") return highlightRexc(text);
|
|
239
|
+
if (hint === "rex") return text.split("\n").map((line) => highlightLine(line)).join("\n");
|
|
240
|
+
try {
|
|
241
|
+
const match = grammar.match(text);
|
|
242
|
+
if (match.succeeded()) {
|
|
243
|
+
return text.split("\n").map((line) => highlightLine(line)).join("\n");
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// fall through
|
|
247
|
+
}
|
|
248
|
+
return highlightRexc(text);
|
|
249
|
+
}
|
|
250
|
+
|
|
183
251
|
// ── JSON IR highlighting ─────────────────────────────────────
|
|
184
252
|
|
|
185
253
|
const JSON_TOKEN_RE =
|
|
@@ -257,26 +325,31 @@ export function isIncomplete(buffer: string): boolean {
|
|
|
257
325
|
// Trailing binary operator or keyword suggests continuation
|
|
258
326
|
const trimmed = buffer.trimEnd();
|
|
259
327
|
if (/[+\-*/%&|^=<>]$/.test(trimmed)) return true;
|
|
260
|
-
if (/\b(?:and|or|do|in|of)\s*$/.test(trimmed)) return true;
|
|
328
|
+
if (/\b(?:and|or|nor|do|in|of)\s*$/.test(trimmed)) return true;
|
|
261
329
|
|
|
262
330
|
return false;
|
|
263
331
|
}
|
|
264
332
|
|
|
265
333
|
// ── Result & state formatting ─────────────────────────────────
|
|
266
334
|
|
|
267
|
-
function formatResult(value: unknown): string {
|
|
335
|
+
function formatResult(value: unknown, format: OutputFormat): string {
|
|
268
336
|
let text: string;
|
|
269
337
|
try {
|
|
270
|
-
text = stringify(value, { maxWidth: 60 });
|
|
338
|
+
text = format === "json" ? formatJson(value, 2) : stringify(value, { maxWidth: 60 });
|
|
271
339
|
} catch {
|
|
272
340
|
text = String(value);
|
|
273
341
|
}
|
|
274
|
-
|
|
342
|
+
const rendered = format === "json" ? highlightJSON(text) : highlightLine(text);
|
|
343
|
+
return `${C.gray}→${C.reset} ${rendered}`;
|
|
275
344
|
}
|
|
276
345
|
|
|
277
|
-
export function formatVarState(vars: Record<string, unknown
|
|
346
|
+
export function formatVarState(vars: Record<string, unknown>, format: OutputFormat): string {
|
|
278
347
|
const entries = Object.entries(vars);
|
|
279
348
|
if (entries.length === 0) return "";
|
|
349
|
+
if (format === "json") {
|
|
350
|
+
const rendered = highlightJSON(formatJson(vars, 2));
|
|
351
|
+
return `${C.dim} vars:${C.reset} ${rendered}`;
|
|
352
|
+
}
|
|
280
353
|
|
|
281
354
|
const MAX_LINE = 70;
|
|
282
355
|
const MAX_VALUE = 30;
|
|
@@ -305,17 +378,58 @@ export function formatVarState(vars: Record<string, unknown>): string {
|
|
|
305
378
|
return `${C.dim} ${parts.join(", ")}${C.reset}`;
|
|
306
379
|
}
|
|
307
380
|
|
|
381
|
+
function renderValue(value: unknown, format: OutputFormat, kind: OutputKey): string {
|
|
382
|
+
if (format === "json") {
|
|
383
|
+
return highlightJSON(formatJson(value, 2));
|
|
384
|
+
}
|
|
385
|
+
if (kind === "source") {
|
|
386
|
+
return highlightAuto(String(value ?? ""));
|
|
387
|
+
}
|
|
388
|
+
if (kind === "rexc") {
|
|
389
|
+
return highlightRexc(String(value ?? ""));
|
|
390
|
+
}
|
|
391
|
+
return highlightLine(stringify(value, { maxWidth: 120 }));
|
|
392
|
+
}
|
|
393
|
+
|
|
308
394
|
// ── Tab completion ────────────────────────────────────────────
|
|
309
395
|
|
|
310
396
|
const KEYWORDS = [
|
|
311
397
|
"when", "unless", "while", "for", "do", "end", "in", "of",
|
|
312
|
-
"and", "or", "else", "break", "continue", "delete",
|
|
398
|
+
"and", "or", "nor", "else", "break", "continue", "delete",
|
|
313
399
|
"self", "true", "false", "null", "undefined", "nan", "inf",
|
|
314
400
|
"string", "number", "object", "array", "boolean",
|
|
315
401
|
];
|
|
316
402
|
|
|
403
|
+
const DOT_COMMANDS = [
|
|
404
|
+
".help",
|
|
405
|
+
".file",
|
|
406
|
+
".cat",
|
|
407
|
+
".expr",
|
|
408
|
+
".source",
|
|
409
|
+
".vars",
|
|
410
|
+
".vars!",
|
|
411
|
+
".clear",
|
|
412
|
+
".ir",
|
|
413
|
+
".rexc",
|
|
414
|
+
".opt",
|
|
415
|
+
".json",
|
|
416
|
+
".color",
|
|
417
|
+
".exit",
|
|
418
|
+
];
|
|
419
|
+
|
|
317
420
|
function completer(state: ReplState): (line: string) => [string[], string] {
|
|
318
421
|
return (line: string) => {
|
|
422
|
+
if (line.startsWith(".file ")) {
|
|
423
|
+
return completeFilePath(line, 6);
|
|
424
|
+
}
|
|
425
|
+
if (line.startsWith(".cat ")) {
|
|
426
|
+
return completeFilePath(line, 5);
|
|
427
|
+
}
|
|
428
|
+
if (line.startsWith(".") && !line.includes(" ")) {
|
|
429
|
+
const partial = line.trim();
|
|
430
|
+
const matches = DOT_COMMANDS.filter((cmd) => cmd.startsWith(partial));
|
|
431
|
+
return [matches, line];
|
|
432
|
+
}
|
|
319
433
|
const match = line.match(/[a-zA-Z_][a-zA-Z0-9_.-]*$/);
|
|
320
434
|
const partial = match ? match[0] : "";
|
|
321
435
|
if (!partial) return [[], ""];
|
|
@@ -327,9 +441,56 @@ function completer(state: ReplState): (line: string) => [string[], string] {
|
|
|
327
441
|
};
|
|
328
442
|
}
|
|
329
443
|
|
|
444
|
+
function completeFilePath(line: string, prefixLength: number): [string[], string] {
|
|
445
|
+
const raw = line.slice(prefixLength);
|
|
446
|
+
const trimmed = raw.trimStart();
|
|
447
|
+
const quote = trimmed.startsWith("\"") || trimmed.startsWith("'") ? trimmed[0]! : "";
|
|
448
|
+
const pathPart = quote ? trimmed.slice(1) : trimmed;
|
|
449
|
+
const endsWithSlash = pathPart.endsWith("/");
|
|
450
|
+
const rawDir = endsWithSlash ? pathPart.slice(0, -1) : pathPart;
|
|
451
|
+
const baseName = endsWithSlash ? "" : (rawDir.includes("/") ? basename(rawDir) : rawDir);
|
|
452
|
+
const dirPart = endsWithSlash ? (rawDir || ".") : (rawDir.includes("/") ? dirname(rawDir) : ".");
|
|
453
|
+
const dirPath = resolve(dirPart);
|
|
454
|
+
let entries: string[] = [];
|
|
455
|
+
try {
|
|
456
|
+
entries = readdirSync(dirPath);
|
|
457
|
+
} catch {
|
|
458
|
+
return [[], ""];
|
|
459
|
+
}
|
|
460
|
+
const prefix = dirPart === "." ? "" : `${dirPart}/`;
|
|
461
|
+
const matches = entries
|
|
462
|
+
.filter((entry) => entry.startsWith(baseName))
|
|
463
|
+
.map((entry) => {
|
|
464
|
+
const fullPath = resolve(dirPath, entry);
|
|
465
|
+
let suffix = "";
|
|
466
|
+
try {
|
|
467
|
+
if (statSync(fullPath).isDirectory()) suffix = "/";
|
|
468
|
+
} catch {
|
|
469
|
+
suffix = "";
|
|
470
|
+
}
|
|
471
|
+
return `${quote}${prefix}${entry}${suffix}`;
|
|
472
|
+
});
|
|
473
|
+
return [matches, trimmed];
|
|
474
|
+
}
|
|
475
|
+
|
|
330
476
|
// ── Dot commands ──────────────────────────────────────────────
|
|
331
477
|
|
|
332
|
-
function
|
|
478
|
+
function stripQuotes(value: string): string {
|
|
479
|
+
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
480
|
+
return value.slice(1, -1);
|
|
481
|
+
}
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
type DotCommandResult = "handled" | "handled-noprompt" | "unhandled";
|
|
486
|
+
|
|
487
|
+
async function handleDotCommand(
|
|
488
|
+
cmd: string,
|
|
489
|
+
state: ReplState,
|
|
490
|
+
rl: readline.Interface,
|
|
491
|
+
runSource: (source: string) => void,
|
|
492
|
+
updatePromptStyles: () => void,
|
|
493
|
+
): Promise<DotCommandResult> {
|
|
333
494
|
function toggleLabel(on: boolean): string {
|
|
334
495
|
return on ? `${C.green}on${C.reset}` : `${C.dim}off${C.reset}`;
|
|
335
496
|
}
|
|
@@ -338,42 +499,53 @@ function handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface)
|
|
|
338
499
|
case ".help":
|
|
339
500
|
console.log(
|
|
340
501
|
[
|
|
341
|
-
`${C.
|
|
342
|
-
" .help
|
|
343
|
-
" .
|
|
344
|
-
" .
|
|
345
|
-
" .
|
|
346
|
-
" .
|
|
347
|
-
" .
|
|
348
|
-
" .
|
|
502
|
+
`${C.keyword}Rex REPL Commands:${C.reset}`,
|
|
503
|
+
" .help Show this help message",
|
|
504
|
+
" .file <path> Load and execute a Rex file",
|
|
505
|
+
" .cat <path> Print a Rex/rexc file with highlighting",
|
|
506
|
+
" .expr Toggle showing expression results",
|
|
507
|
+
" .source Toggle showing input source",
|
|
508
|
+
" .vars Toggle showing variable summary",
|
|
509
|
+
" .vars! Show all current variables",
|
|
510
|
+
" .clear Clear all variables",
|
|
511
|
+
" .ir Toggle showing IR JSON after parsing",
|
|
512
|
+
" .rexc Toggle showing compiled rexc before execution",
|
|
513
|
+
" .opt Toggle IR optimizations",
|
|
514
|
+
" .json Toggle JSON output format",
|
|
515
|
+
" .color Toggle ANSI color output",
|
|
516
|
+
" .exit Exit the REPL",
|
|
349
517
|
"",
|
|
350
518
|
"Enter Rex expressions to evaluate them.",
|
|
351
519
|
"Multi-line: open brackets or do/end blocks continue on the next line.",
|
|
352
520
|
"Ctrl-C cancels multi-line input.",
|
|
353
521
|
"Ctrl-D exits.",
|
|
522
|
+
"",
|
|
523
|
+
"Outputs are printed as labeled blocks when enabled.",
|
|
354
524
|
].join("\n"),
|
|
355
525
|
);
|
|
356
|
-
return
|
|
526
|
+
return "handled";
|
|
357
527
|
|
|
358
|
-
case ".
|
|
359
|
-
state.
|
|
360
|
-
console.log(`${C.dim}
|
|
361
|
-
return
|
|
528
|
+
case ".expr":
|
|
529
|
+
state.showExpr = !state.showExpr;
|
|
530
|
+
console.log(`${C.dim} Expression output: ${toggleLabel(state.showExpr)}${C.reset}`);
|
|
531
|
+
return "handled";
|
|
362
532
|
|
|
363
|
-
case ".
|
|
364
|
-
state.
|
|
365
|
-
console.log(`${C.dim}
|
|
366
|
-
return
|
|
533
|
+
case ".source":
|
|
534
|
+
state.showSource = !state.showSource;
|
|
535
|
+
console.log(`${C.dim} Source output: ${toggleLabel(state.showSource)}${C.reset}`);
|
|
536
|
+
return "handled";
|
|
367
537
|
|
|
368
|
-
case ".
|
|
369
|
-
state.
|
|
370
|
-
console.log(`${C.dim}
|
|
371
|
-
return
|
|
538
|
+
case ".vars":
|
|
539
|
+
state.showVars = !state.showVars;
|
|
540
|
+
console.log(`${C.dim} Variable summary: ${toggleLabel(state.showVars)}${C.reset}`);
|
|
541
|
+
return "handled";
|
|
372
542
|
|
|
373
|
-
case ".vars": {
|
|
543
|
+
case ".vars!": {
|
|
374
544
|
const entries = Object.entries(state.vars);
|
|
375
545
|
if (entries.length === 0) {
|
|
376
546
|
console.log(`${C.dim} (no variables)${C.reset}`);
|
|
547
|
+
} else if (state.outputFormat === "json") {
|
|
548
|
+
console.log(highlightJSON(formatJson(state.vars, 2)));
|
|
377
549
|
} else {
|
|
378
550
|
for (const [key, val] of entries) {
|
|
379
551
|
let valStr: string;
|
|
@@ -385,25 +557,84 @@ function handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface)
|
|
|
385
557
|
console.log(` ${key} = ${highlightLine(valStr)}`);
|
|
386
558
|
}
|
|
387
559
|
}
|
|
388
|
-
return
|
|
560
|
+
return "handled";
|
|
389
561
|
}
|
|
390
562
|
|
|
563
|
+
case ".ir":
|
|
564
|
+
state.showIR = !state.showIR;
|
|
565
|
+
console.log(`${C.dim} IR display: ${toggleLabel(state.showIR)}${C.reset}`);
|
|
566
|
+
return "handled";
|
|
567
|
+
|
|
568
|
+
case ".rexc":
|
|
569
|
+
state.showRexc = !state.showRexc;
|
|
570
|
+
console.log(`${C.dim} Rexc display: ${toggleLabel(state.showRexc)}${C.reset}`);
|
|
571
|
+
return "handled";
|
|
572
|
+
|
|
573
|
+
case ".opt":
|
|
574
|
+
state.optimize = !state.optimize;
|
|
575
|
+
console.log(`${C.dim} Optimizations: ${toggleLabel(state.optimize)}${C.reset}`);
|
|
576
|
+
return "handled";
|
|
577
|
+
|
|
578
|
+
case ".json":
|
|
579
|
+
state.outputFormat = state.outputFormat === "json" ? "rex" : "json";
|
|
580
|
+
console.log(`${C.dim} Output format: ${state.outputFormat}${C.reset}`);
|
|
581
|
+
return "handled";
|
|
582
|
+
|
|
583
|
+
case ".color":
|
|
584
|
+
setColorEnabled(!colorEnabled);
|
|
585
|
+
updatePromptStyles();
|
|
586
|
+
console.log(`${C.dim} Color output: ${toggleLabel(colorEnabled)}${C.reset}`);
|
|
587
|
+
return "handled";
|
|
588
|
+
|
|
391
589
|
case ".clear":
|
|
392
590
|
state.vars = {};
|
|
393
591
|
state.refs = {};
|
|
394
592
|
console.log(`${C.dim} Variables cleared.${C.reset}`);
|
|
395
|
-
return
|
|
593
|
+
return "handled";
|
|
396
594
|
|
|
397
595
|
case ".exit":
|
|
398
596
|
rl.close();
|
|
399
|
-
return
|
|
597
|
+
return "handled-noprompt";
|
|
400
598
|
|
|
401
599
|
default:
|
|
600
|
+
if (cmd.startsWith(".cat ")) {
|
|
601
|
+
const rawPath = cmd.slice(5).trim();
|
|
602
|
+
if (!rawPath) {
|
|
603
|
+
console.log(`${C.red} Missing file path. Usage: .cat <path>${C.reset}`);
|
|
604
|
+
return "handled";
|
|
605
|
+
}
|
|
606
|
+
const filePath = resolve(stripQuotes(rawPath));
|
|
607
|
+
try {
|
|
608
|
+
const source = readFileSync(filePath, "utf8");
|
|
609
|
+
const hint = filePath.endsWith(".rexc") ? "rexc" : filePath.endsWith(".rex") ? "rex" : undefined;
|
|
610
|
+
console.log(highlightAuto(source, hint));
|
|
611
|
+
} catch (error) {
|
|
612
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
613
|
+
console.log(`${C.red} File error: ${message}${C.reset}`);
|
|
614
|
+
}
|
|
615
|
+
return "handled";
|
|
616
|
+
}
|
|
617
|
+
if (cmd.startsWith(".file ")) {
|
|
618
|
+
const rawPath = cmd.slice(6).trim();
|
|
619
|
+
if (!rawPath) {
|
|
620
|
+
console.log(`${C.red} Missing file path. Usage: .file <path>${C.reset}`);
|
|
621
|
+
return "handled";
|
|
622
|
+
}
|
|
623
|
+
const filePath = resolve(stripQuotes(rawPath));
|
|
624
|
+
try {
|
|
625
|
+
const source = readFileSync(filePath, "utf8");
|
|
626
|
+
runSource(source);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
629
|
+
console.log(`${C.red} File error: ${message}${C.reset}`);
|
|
630
|
+
}
|
|
631
|
+
return "handled";
|
|
632
|
+
}
|
|
402
633
|
if (cmd.startsWith(".")) {
|
|
403
634
|
console.log(`${C.red} Unknown command: ${cmd}. Type .help for available commands.${C.reset}`);
|
|
404
|
-
return
|
|
635
|
+
return "handled";
|
|
405
636
|
}
|
|
406
|
-
return
|
|
637
|
+
return "unhandled";
|
|
407
638
|
}
|
|
408
639
|
}
|
|
409
640
|
|
|
@@ -415,37 +646,72 @@ type ReplState = {
|
|
|
415
646
|
showIR: boolean;
|
|
416
647
|
showRexc: boolean;
|
|
417
648
|
optimize: boolean;
|
|
649
|
+
showExpr: boolean;
|
|
650
|
+
showVars: boolean;
|
|
651
|
+
showSource: boolean;
|
|
652
|
+
outputFormat: OutputFormat;
|
|
418
653
|
};
|
|
419
654
|
|
|
655
|
+
type OutputKey = "source" | "ir" | "rexc" | "vars" | "result";
|
|
656
|
+
|
|
420
657
|
// ── Gas limit for loop safety ─────────────────────────────────
|
|
421
658
|
|
|
422
659
|
const GAS_LIMIT = 10_000_000;
|
|
660
|
+
const HISTORY_LIMIT = 1000;
|
|
661
|
+
const HISTORY_PATH = resolve(homedir(), ".rex_history");
|
|
423
662
|
|
|
424
663
|
// ── Main REPL entry point ─────────────────────────────────────
|
|
425
664
|
|
|
426
665
|
export async function startRepl(): Promise<void> {
|
|
427
|
-
const state: ReplState = {
|
|
666
|
+
const state: ReplState = {
|
|
667
|
+
vars: {},
|
|
668
|
+
refs: {},
|
|
669
|
+
showIR: false,
|
|
670
|
+
showRexc: false,
|
|
671
|
+
optimize: false,
|
|
672
|
+
showExpr: true,
|
|
673
|
+
showVars: false,
|
|
674
|
+
showSource: false,
|
|
675
|
+
outputFormat: "rex",
|
|
676
|
+
};
|
|
428
677
|
let multiLineBuffer = "";
|
|
429
678
|
|
|
430
679
|
const PRIMARY_PROMPT = "rex> ";
|
|
431
680
|
const CONT_PROMPT = "... ";
|
|
432
|
-
const STYLED_PRIMARY = `${C.boldBlue}rex${C.reset}> `;
|
|
433
|
-
const STYLED_CONT = `${C.dim}...${C.reset} `;
|
|
434
681
|
|
|
435
682
|
let currentPrompt = PRIMARY_PROMPT;
|
|
436
|
-
let styledPrompt =
|
|
683
|
+
let styledPrompt = "";
|
|
684
|
+
let styledPrimary = "";
|
|
685
|
+
let styledCont = "";
|
|
686
|
+
|
|
687
|
+
function updatePromptStyles() {
|
|
688
|
+
styledPrimary = `${C.keyword}rex${C.reset}> `;
|
|
689
|
+
styledCont = `${C.dim}...${C.reset} `;
|
|
690
|
+
styledPrompt = currentPrompt === PRIMARY_PROMPT ? styledPrimary : styledCont;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
updatePromptStyles();
|
|
437
694
|
|
|
438
|
-
console.log(`${C.
|
|
695
|
+
console.log(`${C.keyword}Rex${C.reset} v${version} — type ${C.dim}.help${C.reset} for commands`);
|
|
439
696
|
|
|
440
697
|
const rl = readline.createInterface({
|
|
441
698
|
input: process.stdin,
|
|
442
699
|
output: process.stdout,
|
|
443
700
|
prompt: PRIMARY_PROMPT,
|
|
444
|
-
historySize:
|
|
701
|
+
historySize: HISTORY_LIMIT,
|
|
445
702
|
completer: completer(state),
|
|
446
703
|
terminal: true,
|
|
447
704
|
});
|
|
448
705
|
|
|
706
|
+
try {
|
|
707
|
+
const historyText = readFileSync(HISTORY_PATH, "utf8");
|
|
708
|
+
const lines = historyText.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
709
|
+
const recent = lines.slice(-HISTORY_LIMIT);
|
|
710
|
+
(rl as readline.Interface & { history: string[] }).history = recent.reverse();
|
|
711
|
+
} catch {
|
|
712
|
+
// ignore missing history
|
|
713
|
+
}
|
|
714
|
+
|
|
449
715
|
// ── Syntax highlighting via keypress redraw ──
|
|
450
716
|
process.stdin.on("keypress", () => {
|
|
451
717
|
process.nextTick(() => {
|
|
@@ -473,7 +739,7 @@ export async function startRepl(): Promise<void> {
|
|
|
473
739
|
if (multiLineBuffer) {
|
|
474
740
|
multiLineBuffer = "";
|
|
475
741
|
currentPrompt = PRIMARY_PROMPT;
|
|
476
|
-
styledPrompt =
|
|
742
|
+
styledPrompt = styledPrimary;
|
|
477
743
|
rl.setPrompt(PRIMARY_PROMPT);
|
|
478
744
|
process.stdout.write("\n");
|
|
479
745
|
rl.prompt();
|
|
@@ -485,21 +751,72 @@ export async function startRepl(): Promise<void> {
|
|
|
485
751
|
|
|
486
752
|
function resetPrompt() {
|
|
487
753
|
currentPrompt = PRIMARY_PROMPT;
|
|
488
|
-
styledPrompt =
|
|
754
|
+
styledPrompt = styledPrimary;
|
|
489
755
|
rl.setPrompt(PRIMARY_PROMPT);
|
|
490
756
|
rl.prompt();
|
|
491
757
|
}
|
|
492
758
|
|
|
493
759
|
// ── Line handler ──
|
|
494
|
-
|
|
760
|
+
function runSource(source: string) {
|
|
761
|
+
const match = grammar.match(source);
|
|
762
|
+
if (!match.succeeded()) {
|
|
763
|
+
const message = formatParseError(source, match as { message?: string; getRightmostFailurePosition?: () => number });
|
|
764
|
+
console.log(`${C.red} ${message}${C.reset}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const outputs: Partial<Record<OutputKey, unknown>> = {};
|
|
770
|
+
if (state.showSource) outputs.source = source;
|
|
771
|
+
const isRex = grammar.match(source).succeeded();
|
|
772
|
+
if (!isRex && state.showIR) {
|
|
773
|
+
console.log(`${C.red} IR output is only available for Rex source.${C.reset}`);
|
|
774
|
+
}
|
|
775
|
+
const rexc = isRex ? compile(source, { optimize: state.optimize }) : source;
|
|
776
|
+
if (state.showIR && isRex) {
|
|
777
|
+
const ir = parseToIR(source);
|
|
778
|
+
const lowered = state.optimize ? optimizeIR(ir) : ir;
|
|
779
|
+
outputs.ir = lowered;
|
|
780
|
+
}
|
|
781
|
+
if (state.showRexc) outputs.rexc = rexc;
|
|
782
|
+
|
|
783
|
+
const result = evaluateRexc(rexc, {
|
|
784
|
+
vars: { ...state.vars },
|
|
785
|
+
refs: { ...state.refs },
|
|
786
|
+
gasLimit: GAS_LIMIT,
|
|
787
|
+
});
|
|
788
|
+
state.vars = result.state.vars;
|
|
789
|
+
state.refs = result.state.refs;
|
|
790
|
+
if (state.showExpr) outputs.result = result.value;
|
|
791
|
+
if (state.showVars) outputs.vars = state.vars;
|
|
792
|
+
|
|
793
|
+
const order: OutputKey[] = ["source", "ir", "rexc", "vars", "result"];
|
|
794
|
+
for (const key of order) {
|
|
795
|
+
const value = outputs[key];
|
|
796
|
+
if (value === undefined) continue;
|
|
797
|
+
console.log(`${C.gray} ${key}:${C.reset} ${renderValue(value, state.outputFormat, key)}`);
|
|
798
|
+
}
|
|
799
|
+
} catch (error) {
|
|
800
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
801
|
+
if (message.includes("Gas limit exceeded")) {
|
|
802
|
+
console.log(`${C.yellow} ${message}${C.reset}`);
|
|
803
|
+
} else {
|
|
804
|
+
console.log(`${C.red} Error: ${message}${C.reset}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
rl.on("line", async (line: string) => {
|
|
495
810
|
const trimmed = line.trim();
|
|
496
811
|
|
|
497
812
|
// Dot commands (only when not accumulating multi-line)
|
|
498
813
|
if (!multiLineBuffer && trimmed.startsWith(".")) {
|
|
499
|
-
|
|
814
|
+
const result = await handleDotCommand(trimmed, state, rl, runSource, updatePromptStyles);
|
|
815
|
+
if (result === "handled") {
|
|
500
816
|
rl.prompt();
|
|
501
817
|
return;
|
|
502
818
|
}
|
|
819
|
+
if (result === "handled-noprompt") return;
|
|
503
820
|
}
|
|
504
821
|
|
|
505
822
|
// Accumulate
|
|
@@ -515,7 +832,7 @@ export async function startRepl(): Promise<void> {
|
|
|
515
832
|
// Check for incomplete expression
|
|
516
833
|
if (isIncomplete(multiLineBuffer)) {
|
|
517
834
|
currentPrompt = CONT_PROMPT;
|
|
518
|
-
styledPrompt =
|
|
835
|
+
styledPrompt = styledCont;
|
|
519
836
|
rl.setPrompt(CONT_PROMPT);
|
|
520
837
|
rl.prompt();
|
|
521
838
|
return;
|
|
@@ -525,52 +842,24 @@ export async function startRepl(): Promise<void> {
|
|
|
525
842
|
const source = multiLineBuffer;
|
|
526
843
|
multiLineBuffer = "";
|
|
527
844
|
|
|
528
|
-
|
|
529
|
-
if (!match.succeeded()) {
|
|
530
|
-
console.log(`${C.red} ${match.message}${C.reset}`);
|
|
531
|
-
resetPrompt();
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
try {
|
|
536
|
-
const ir = parseToIR(source);
|
|
537
|
-
const lowered = state.optimize ? optimizeIR(ir) : ir;
|
|
538
|
-
|
|
539
|
-
if (state.showIR) {
|
|
540
|
-
console.log(`${C.dim} IR:${C.reset} ${highlightJSON(JSON.stringify(lowered))}`);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const rexc = compile(source, { optimize: state.optimize });
|
|
544
|
-
|
|
545
|
-
if (state.showRexc) {
|
|
546
|
-
console.log(`${C.dim} rexc:${C.reset} ${highlightRexc(rexc)}`);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const result = evaluateRexc(rexc, {
|
|
550
|
-
vars: { ...state.vars },
|
|
551
|
-
refs: { ...state.refs },
|
|
552
|
-
gasLimit: GAS_LIMIT,
|
|
553
|
-
});
|
|
554
|
-
state.vars = result.state.vars;
|
|
555
|
-
state.refs = result.state.refs;
|
|
556
|
-
|
|
557
|
-
console.log(formatResult(result.value));
|
|
558
|
-
const varLine = formatVarState(state.vars);
|
|
559
|
-
if (varLine) console.log(varLine);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
562
|
-
if (message.includes("Gas limit exceeded")) {
|
|
563
|
-
console.log(`${C.yellow} ${message}${C.reset}`);
|
|
564
|
-
} else {
|
|
565
|
-
console.log(`${C.red} Error: ${message}${C.reset}`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
845
|
+
runSource(source);
|
|
568
846
|
|
|
569
847
|
resetPrompt();
|
|
570
848
|
});
|
|
571
849
|
|
|
572
850
|
// ── Exit ──
|
|
573
851
|
rl.on("close", () => {
|
|
852
|
+
try {
|
|
853
|
+
const history = (rl as readline.Interface & { history: string[] }).history ?? [];
|
|
854
|
+
const trimmed = history
|
|
855
|
+
.slice()
|
|
856
|
+
.reverse()
|
|
857
|
+
.filter((line) => line.trim().length > 0)
|
|
858
|
+
.slice(-HISTORY_LIMIT);
|
|
859
|
+
writeFileSync(HISTORY_PATH, `${trimmed.join("\n")}\n`, "utf8");
|
|
860
|
+
} catch {
|
|
861
|
+
// ignore history write errors
|
|
862
|
+
}
|
|
574
863
|
process.exit(0);
|
|
575
864
|
});
|
|
576
865
|
|