@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/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|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.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
@@ -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.boldBlue + prefix + tag + C.reset;
211
+ out += C.keyword + prefix + tag + C.reset;
159
212
  i++;
160
213
  break;
161
214
  case ";": // break/continue
162
- out += C.boldBlue + prefix + tag + C.reset;
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
- return `${C.gray}→${C.reset} ${highlightLine(text)}`;
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>): string {
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 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> {
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.boldBlue}Rex REPL Commands:${C.reset}`,
343
- " .help Show this help message",
344
- " .vars Show all current variables",
345
- " .clear Clear all variables",
346
- " .ir Toggle showing IR JSON after parsing",
347
- " .rexc Toggle showing compiled rexc before execution",
348
- " .opt Toggle IR optimizations",
349
- " .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",
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 true;
526
+ return "handled";
358
527
 
359
- case ".ir":
360
- state.showIR = !state.showIR;
361
- console.log(`${C.dim} IR display: ${toggleLabel(state.showIR)}${C.reset}`);
362
- 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";
363
532
 
364
- case ".rexc":
365
- state.showRexc = !state.showRexc;
366
- console.log(`${C.dim} Rexc display: ${toggleLabel(state.showRexc)}${C.reset}`);
367
- 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";
368
537
 
369
- case ".opt":
370
- state.optimize = !state.optimize;
371
- console.log(`${C.dim} Optimizations: ${toggleLabel(state.optimize)}${C.reset}`);
372
- 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";
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 true;
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 true;
593
+ return "handled";
397
594
 
398
595
  case ".exit":
399
596
  rl.close();
400
- return true;
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 true;
635
+ return "handled";
406
636
  }
407
- return false;
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 = { 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
+ };
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 = 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();
438
694
 
439
- 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`);
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: 500,
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 = STYLED_PRIMARY;
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 = STYLED_PRIMARY;
754
+ styledPrompt = styledPrimary;
490
755
  rl.setPrompt(PRIMARY_PROMPT);
491
756
  rl.prompt();
492
757
  }
493
758
 
494
759
  // ── Line handler ──
495
- 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) => {
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
- if (handleDotCommand(trimmed, state, rl)) {
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 = STYLED_CONT;
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
- const match = grammar.match(source);
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