@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/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 { grammar, stringify, parseToIR, optimizeIR, compile } from "./rex.ts";
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
- const C = {
12
- reset: "\x1b[0m",
13
- bold: "\x1b[1m",
14
- dim: "\x1b[2m",
15
- red: "\x1b[31m",
16
- green: "\x1b[32m",
17
- yellow: "\x1b[33m",
18
- blue: "\x1b[34m",
19
- cyan: "\x1b[36m",
20
- gray: "\x1b[90m",
21
- boldBlue: "\x1b[1;34m",
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.boldBlue + text + C.reset;
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.boldBlue + prefix + tag + C.reset;
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.boldBlue + prefix + tag + C.reset;
211
+ out += C.keyword + prefix + tag + C.reset;
158
212
  i++;
159
213
  break;
160
214
  case ";": // break/continue
161
- out += C.boldBlue + prefix + tag + C.reset;
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
- return `${C.gray}→${C.reset} ${highlightLine(text)}`;
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>): string {
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 handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface): boolean {
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.boldBlue}Rex REPL Commands:${C.reset}`,
342
- " .help Show this help message",
343
- " .vars Show all current variables",
344
- " .clear Clear all variables",
345
- " .ir Toggle showing IR JSON after parsing",
346
- " .rexc Toggle showing compiled rexc before execution",
347
- " .opt Toggle IR optimizations",
348
- " .exit Exit the REPL",
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 true;
526
+ return "handled";
357
527
 
358
- case ".ir":
359
- state.showIR = !state.showIR;
360
- console.log(`${C.dim} IR display: ${toggleLabel(state.showIR)}${C.reset}`);
361
- return true;
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 ".rexc":
364
- state.showRexc = !state.showRexc;
365
- console.log(`${C.dim} Rexc display: ${toggleLabel(state.showRexc)}${C.reset}`);
366
- return true;
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 ".opt":
369
- state.optimize = !state.optimize;
370
- console.log(`${C.dim} Optimizations: ${toggleLabel(state.optimize)}${C.reset}`);
371
- return true;
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 true;
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 true;
593
+ return "handled";
396
594
 
397
595
  case ".exit":
398
596
  rl.close();
399
- return true;
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 true;
635
+ return "handled";
405
636
  }
406
- return false;
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 = { vars: {}, refs: {}, showIR: false, showRexc: false, optimize: false };
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 = STYLED_PRIMARY;
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.boldBlue}Rex${C.reset} v${version} — type ${C.dim}.help${C.reset} for commands`);
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: 500,
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 = STYLED_PRIMARY;
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 = STYLED_PRIMARY;
754
+ styledPrompt = styledPrimary;
489
755
  rl.setPrompt(PRIMARY_PROMPT);
490
756
  rl.prompt();
491
757
  }
492
758
 
493
759
  // ── Line handler ──
494
- rl.on("line", (line: string) => {
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
- if (handleDotCommand(trimmed, state, rl)) {
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 = STYLED_CONT;
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
- const match = grammar.match(source);
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