@creationix/rex 0.4.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 +785 -987
- package/rex-cli.ts +236 -16
- package/rex-repl.js +592 -1005
- package/rex-repl.ts +389 -101
- package/rex.js +51 -845
- package/rex.ohm +7 -8
- package/rex.ohm-bundle.cjs +1 -1
- package/rex.ohm-bundle.d.ts +4 -3
- package/rex.ohm-bundle.js +1 -1
- package/rex.ts +52 -23
- package/rexc-interpreter.ts +136 -15
- 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|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)/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
|
|
@@ -155,11 +208,11 @@ export function highlightRexc(text: string): string {
|
|
|
155
208
|
case ">": // for
|
|
156
209
|
case "<": // for-keys
|
|
157
210
|
case "#": // while
|
|
158
|
-
out += C.
|
|
211
|
+
out += C.keyword + prefix + tag + C.reset;
|
|
159
212
|
i++;
|
|
160
213
|
break;
|
|
161
214
|
case ";": // break/continue
|
|
162
|
-
out += C.
|
|
215
|
+
out += C.keyword + prefix + tag + C.reset;
|
|
163
216
|
i++;
|
|
164
217
|
break;
|
|
165
218
|
case "^": // pointer
|
|
@@ -181,6 +234,20 @@ export function highlightRexc(text: string): string {
|
|
|
181
234
|
return out;
|
|
182
235
|
}
|
|
183
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
|
+
|
|
184
251
|
// ── JSON IR highlighting ─────────────────────────────────────
|
|
185
252
|
|
|
186
253
|
const JSON_TOKEN_RE =
|
|
@@ -265,19 +332,24 @@ export function isIncomplete(buffer: string): boolean {
|
|
|
265
332
|
|
|
266
333
|
// ── Result & state formatting ─────────────────────────────────
|
|
267
334
|
|
|
268
|
-
function formatResult(value: unknown): string {
|
|
335
|
+
function formatResult(value: unknown, format: OutputFormat): string {
|
|
269
336
|
let text: string;
|
|
270
337
|
try {
|
|
271
|
-
text = stringify(value, { maxWidth: 60 });
|
|
338
|
+
text = format === "json" ? formatJson(value, 2) : stringify(value, { maxWidth: 60 });
|
|
272
339
|
} catch {
|
|
273
340
|
text = String(value);
|
|
274
341
|
}
|
|
275
|
-
|
|
342
|
+
const rendered = format === "json" ? highlightJSON(text) : highlightLine(text);
|
|
343
|
+
return `${C.gray}→${C.reset} ${rendered}`;
|
|
276
344
|
}
|
|
277
345
|
|
|
278
|
-
export function formatVarState(vars: Record<string, unknown
|
|
346
|
+
export function formatVarState(vars: Record<string, unknown>, format: OutputFormat): string {
|
|
279
347
|
const entries = Object.entries(vars);
|
|
280
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
|
+
}
|
|
281
353
|
|
|
282
354
|
const MAX_LINE = 70;
|
|
283
355
|
const MAX_VALUE = 30;
|
|
@@ -306,6 +378,19 @@ export function formatVarState(vars: Record<string, unknown>): string {
|
|
|
306
378
|
return `${C.dim} ${parts.join(", ")}${C.reset}`;
|
|
307
379
|
}
|
|
308
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
|
+
|
|
309
394
|
// ── Tab completion ────────────────────────────────────────────
|
|
310
395
|
|
|
311
396
|
const KEYWORDS = [
|
|
@@ -315,8 +400,36 @@ const KEYWORDS = [
|
|
|
315
400
|
"string", "number", "object", "array", "boolean",
|
|
316
401
|
];
|
|
317
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
|
+
|
|
318
420
|
function completer(state: ReplState): (line: string) => [string[], string] {
|
|
319
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
|
+
}
|
|
320
433
|
const match = line.match(/[a-zA-Z_][a-zA-Z0-9_.-]*$/);
|
|
321
434
|
const partial = match ? match[0] : "";
|
|
322
435
|
if (!partial) return [[], ""];
|
|
@@ -328,9 +441,56 @@ function completer(state: ReplState): (line: string) => [string[], string] {
|
|
|
328
441
|
};
|
|
329
442
|
}
|
|
330
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
|
+
|
|
331
476
|
// ── Dot commands ──────────────────────────────────────────────
|
|
332
477
|
|
|
333
|
-
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> {
|
|
334
494
|
function toggleLabel(on: boolean): string {
|
|
335
495
|
return on ? `${C.green}on${C.reset}` : `${C.dim}off${C.reset}`;
|
|
336
496
|
}
|
|
@@ -339,42 +499,53 @@ function handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface)
|
|
|
339
499
|
case ".help":
|
|
340
500
|
console.log(
|
|
341
501
|
[
|
|
342
|
-
`${C.
|
|
343
|
-
" .help
|
|
344
|
-
" .
|
|
345
|
-
" .
|
|
346
|
-
" .
|
|
347
|
-
" .
|
|
348
|
-
" .
|
|
349
|
-
" .
|
|
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",
|
|
350
517
|
"",
|
|
351
518
|
"Enter Rex expressions to evaluate them.",
|
|
352
519
|
"Multi-line: open brackets or do/end blocks continue on the next line.",
|
|
353
520
|
"Ctrl-C cancels multi-line input.",
|
|
354
521
|
"Ctrl-D exits.",
|
|
522
|
+
"",
|
|
523
|
+
"Outputs are printed as labeled blocks when enabled.",
|
|
355
524
|
].join("\n"),
|
|
356
525
|
);
|
|
357
|
-
return
|
|
526
|
+
return "handled";
|
|
358
527
|
|
|
359
|
-
case ".
|
|
360
|
-
state.
|
|
361
|
-
console.log(`${C.dim}
|
|
362
|
-
return
|
|
528
|
+
case ".expr":
|
|
529
|
+
state.showExpr = !state.showExpr;
|
|
530
|
+
console.log(`${C.dim} Expression output: ${toggleLabel(state.showExpr)}${C.reset}`);
|
|
531
|
+
return "handled";
|
|
363
532
|
|
|
364
|
-
case ".
|
|
365
|
-
state.
|
|
366
|
-
console.log(`${C.dim}
|
|
367
|
-
return
|
|
533
|
+
case ".source":
|
|
534
|
+
state.showSource = !state.showSource;
|
|
535
|
+
console.log(`${C.dim} Source output: ${toggleLabel(state.showSource)}${C.reset}`);
|
|
536
|
+
return "handled";
|
|
368
537
|
|
|
369
|
-
case ".
|
|
370
|
-
state.
|
|
371
|
-
console.log(`${C.dim}
|
|
372
|
-
return
|
|
538
|
+
case ".vars":
|
|
539
|
+
state.showVars = !state.showVars;
|
|
540
|
+
console.log(`${C.dim} Variable summary: ${toggleLabel(state.showVars)}${C.reset}`);
|
|
541
|
+
return "handled";
|
|
373
542
|
|
|
374
|
-
case ".vars": {
|
|
543
|
+
case ".vars!": {
|
|
375
544
|
const entries = Object.entries(state.vars);
|
|
376
545
|
if (entries.length === 0) {
|
|
377
546
|
console.log(`${C.dim} (no variables)${C.reset}`);
|
|
547
|
+
} else if (state.outputFormat === "json") {
|
|
548
|
+
console.log(highlightJSON(formatJson(state.vars, 2)));
|
|
378
549
|
} else {
|
|
379
550
|
for (const [key, val] of entries) {
|
|
380
551
|
let valStr: string;
|
|
@@ -386,25 +557,84 @@ function handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface)
|
|
|
386
557
|
console.log(` ${key} = ${highlightLine(valStr)}`);
|
|
387
558
|
}
|
|
388
559
|
}
|
|
389
|
-
return
|
|
560
|
+
return "handled";
|
|
390
561
|
}
|
|
391
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
|
+
|
|
392
589
|
case ".clear":
|
|
393
590
|
state.vars = {};
|
|
394
591
|
state.refs = {};
|
|
395
592
|
console.log(`${C.dim} Variables cleared.${C.reset}`);
|
|
396
|
-
return
|
|
593
|
+
return "handled";
|
|
397
594
|
|
|
398
595
|
case ".exit":
|
|
399
596
|
rl.close();
|
|
400
|
-
return
|
|
597
|
+
return "handled-noprompt";
|
|
401
598
|
|
|
402
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
|
+
}
|
|
403
633
|
if (cmd.startsWith(".")) {
|
|
404
634
|
console.log(`${C.red} Unknown command: ${cmd}. Type .help for available commands.${C.reset}`);
|
|
405
|
-
return
|
|
635
|
+
return "handled";
|
|
406
636
|
}
|
|
407
|
-
return
|
|
637
|
+
return "unhandled";
|
|
408
638
|
}
|
|
409
639
|
}
|
|
410
640
|
|
|
@@ -416,37 +646,72 @@ type ReplState = {
|
|
|
416
646
|
showIR: boolean;
|
|
417
647
|
showRexc: boolean;
|
|
418
648
|
optimize: boolean;
|
|
649
|
+
showExpr: boolean;
|
|
650
|
+
showVars: boolean;
|
|
651
|
+
showSource: boolean;
|
|
652
|
+
outputFormat: OutputFormat;
|
|
419
653
|
};
|
|
420
654
|
|
|
655
|
+
type OutputKey = "source" | "ir" | "rexc" | "vars" | "result";
|
|
656
|
+
|
|
421
657
|
// ── Gas limit for loop safety ─────────────────────────────────
|
|
422
658
|
|
|
423
659
|
const GAS_LIMIT = 10_000_000;
|
|
660
|
+
const HISTORY_LIMIT = 1000;
|
|
661
|
+
const HISTORY_PATH = resolve(homedir(), ".rex_history");
|
|
424
662
|
|
|
425
663
|
// ── Main REPL entry point ─────────────────────────────────────
|
|
426
664
|
|
|
427
665
|
export async function startRepl(): Promise<void> {
|
|
428
|
-
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
|
+
};
|
|
429
677
|
let multiLineBuffer = "";
|
|
430
678
|
|
|
431
679
|
const PRIMARY_PROMPT = "rex> ";
|
|
432
680
|
const CONT_PROMPT = "... ";
|
|
433
|
-
const STYLED_PRIMARY = `${C.boldBlue}rex${C.reset}> `;
|
|
434
|
-
const STYLED_CONT = `${C.dim}...${C.reset} `;
|
|
435
681
|
|
|
436
682
|
let currentPrompt = PRIMARY_PROMPT;
|
|
437
|
-
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();
|
|
438
694
|
|
|
439
|
-
console.log(`${C.
|
|
695
|
+
console.log(`${C.keyword}Rex${C.reset} v${version} — type ${C.dim}.help${C.reset} for commands`);
|
|
440
696
|
|
|
441
697
|
const rl = readline.createInterface({
|
|
442
698
|
input: process.stdin,
|
|
443
699
|
output: process.stdout,
|
|
444
700
|
prompt: PRIMARY_PROMPT,
|
|
445
|
-
historySize:
|
|
701
|
+
historySize: HISTORY_LIMIT,
|
|
446
702
|
completer: completer(state),
|
|
447
703
|
terminal: true,
|
|
448
704
|
});
|
|
449
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
|
+
|
|
450
715
|
// ── Syntax highlighting via keypress redraw ──
|
|
451
716
|
process.stdin.on("keypress", () => {
|
|
452
717
|
process.nextTick(() => {
|
|
@@ -474,7 +739,7 @@ export async function startRepl(): Promise<void> {
|
|
|
474
739
|
if (multiLineBuffer) {
|
|
475
740
|
multiLineBuffer = "";
|
|
476
741
|
currentPrompt = PRIMARY_PROMPT;
|
|
477
|
-
styledPrompt =
|
|
742
|
+
styledPrompt = styledPrimary;
|
|
478
743
|
rl.setPrompt(PRIMARY_PROMPT);
|
|
479
744
|
process.stdout.write("\n");
|
|
480
745
|
rl.prompt();
|
|
@@ -486,21 +751,72 @@ export async function startRepl(): Promise<void> {
|
|
|
486
751
|
|
|
487
752
|
function resetPrompt() {
|
|
488
753
|
currentPrompt = PRIMARY_PROMPT;
|
|
489
|
-
styledPrompt =
|
|
754
|
+
styledPrompt = styledPrimary;
|
|
490
755
|
rl.setPrompt(PRIMARY_PROMPT);
|
|
491
756
|
rl.prompt();
|
|
492
757
|
}
|
|
493
758
|
|
|
494
759
|
// ── Line handler ──
|
|
495
|
-
|
|
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) => {
|
|
496
810
|
const trimmed = line.trim();
|
|
497
811
|
|
|
498
812
|
// Dot commands (only when not accumulating multi-line)
|
|
499
813
|
if (!multiLineBuffer && trimmed.startsWith(".")) {
|
|
500
|
-
|
|
814
|
+
const result = await handleDotCommand(trimmed, state, rl, runSource, updatePromptStyles);
|
|
815
|
+
if (result === "handled") {
|
|
501
816
|
rl.prompt();
|
|
502
817
|
return;
|
|
503
818
|
}
|
|
819
|
+
if (result === "handled-noprompt") return;
|
|
504
820
|
}
|
|
505
821
|
|
|
506
822
|
// Accumulate
|
|
@@ -516,7 +832,7 @@ export async function startRepl(): Promise<void> {
|
|
|
516
832
|
// Check for incomplete expression
|
|
517
833
|
if (isIncomplete(multiLineBuffer)) {
|
|
518
834
|
currentPrompt = CONT_PROMPT;
|
|
519
|
-
styledPrompt =
|
|
835
|
+
styledPrompt = styledCont;
|
|
520
836
|
rl.setPrompt(CONT_PROMPT);
|
|
521
837
|
rl.prompt();
|
|
522
838
|
return;
|
|
@@ -526,52 +842,24 @@ export async function startRepl(): Promise<void> {
|
|
|
526
842
|
const source = multiLineBuffer;
|
|
527
843
|
multiLineBuffer = "";
|
|
528
844
|
|
|
529
|
-
|
|
530
|
-
if (!match.succeeded()) {
|
|
531
|
-
console.log(`${C.red} ${match.message}${C.reset}`);
|
|
532
|
-
resetPrompt();
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
const ir = parseToIR(source);
|
|
538
|
-
const lowered = state.optimize ? optimizeIR(ir) : ir;
|
|
539
|
-
|
|
540
|
-
if (state.showIR) {
|
|
541
|
-
console.log(`${C.dim} IR:${C.reset} ${highlightJSON(JSON.stringify(lowered))}`);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const rexc = compile(source, { optimize: state.optimize });
|
|
545
|
-
|
|
546
|
-
if (state.showRexc) {
|
|
547
|
-
console.log(`${C.dim} rexc:${C.reset} ${highlightRexc(rexc)}`);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const result = evaluateRexc(rexc, {
|
|
551
|
-
vars: { ...state.vars },
|
|
552
|
-
refs: { ...state.refs },
|
|
553
|
-
gasLimit: GAS_LIMIT,
|
|
554
|
-
});
|
|
555
|
-
state.vars = result.state.vars;
|
|
556
|
-
state.refs = result.state.refs;
|
|
557
|
-
|
|
558
|
-
console.log(formatResult(result.value));
|
|
559
|
-
const varLine = formatVarState(state.vars);
|
|
560
|
-
if (varLine) console.log(varLine);
|
|
561
|
-
} catch (error) {
|
|
562
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
563
|
-
if (message.includes("Gas limit exceeded")) {
|
|
564
|
-
console.log(`${C.yellow} ${message}${C.reset}`);
|
|
565
|
-
} else {
|
|
566
|
-
console.log(`${C.red} Error: ${message}${C.reset}`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
845
|
+
runSource(source);
|
|
569
846
|
|
|
570
847
|
resetPrompt();
|
|
571
848
|
});
|
|
572
849
|
|
|
573
850
|
// ── Exit ──
|
|
574
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
|
+
}
|
|
575
863
|
process.exit(0);
|
|
576
864
|
});
|
|
577
865
|
|