@creationix/rex 0.1.4 → 0.3.1
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 +62 -62
- package/package.json +9 -7
- package/rex-cli.js +4077 -30
- package/rex-cli.ts +200 -0
- package/{rex-compile.js → rex-repl.js} +1679 -214
- package/rex-repl.ts +578 -0
- package/rex.js +214 -59
- package/rex.ohm +34 -3
- package/rex.ohm-bundle.cjs +1 -1
- package/rex.ohm-bundle.d.ts +8 -0
- package/rex.ohm-bundle.js +1 -1
- package/rex.ts +2579 -2422
- package/rexc-interpreter.ts +926 -848
- package/rex-compile.ts +0 -224
package/rex-repl.ts
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { grammar, stringify, parseToIR, optimizeIR, compile } from "./rex.ts";
|
|
4
|
+
import { evaluateRexc } from "./rexc-interpreter.ts";
|
|
5
|
+
|
|
6
|
+
const req = createRequire(import.meta.url);
|
|
7
|
+
const { version } = req("./package.json");
|
|
8
|
+
|
|
9
|
+
// ── ANSI helpers ──────────────────────────────────────────────
|
|
10
|
+
|
|
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
|
+
};
|
|
23
|
+
|
|
24
|
+
// ── Syntax highlighting ───────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
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;
|
|
28
|
+
|
|
29
|
+
export function highlightLine(line: string): string {
|
|
30
|
+
let result = "";
|
|
31
|
+
let lastIndex = 0;
|
|
32
|
+
TOKEN_RE.lastIndex = 0;
|
|
33
|
+
|
|
34
|
+
for (const m of line.matchAll(TOKEN_RE)) {
|
|
35
|
+
result += line.slice(lastIndex, m.index);
|
|
36
|
+
const text = m[0];
|
|
37
|
+
const g = m.groups!;
|
|
38
|
+
if (g.blockComment || g.lineComment) {
|
|
39
|
+
result += C.gray + text + C.reset;
|
|
40
|
+
} else if (g.dstring || g.sstring) {
|
|
41
|
+
result += C.green + text + C.reset;
|
|
42
|
+
} else if (g.keyword) {
|
|
43
|
+
result += C.boldBlue + text + C.reset;
|
|
44
|
+
} else if (g.literal) {
|
|
45
|
+
result += C.yellow + text + C.reset;
|
|
46
|
+
} else if (g.typePred) {
|
|
47
|
+
result += C.cyan + text + C.reset;
|
|
48
|
+
} else if (g.num) {
|
|
49
|
+
result += C.cyan + text + C.reset;
|
|
50
|
+
} else {
|
|
51
|
+
result += text;
|
|
52
|
+
}
|
|
53
|
+
lastIndex = m.index! + text.length;
|
|
54
|
+
}
|
|
55
|
+
result += line.slice(lastIndex);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Rexc highlighting ────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const REXC_DIGITS = new Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_");
|
|
62
|
+
|
|
63
|
+
export function highlightRexc(text: string): string {
|
|
64
|
+
let out = "";
|
|
65
|
+
let i = 0;
|
|
66
|
+
|
|
67
|
+
function readPrefix(): string {
|
|
68
|
+
const start = i;
|
|
69
|
+
while (i < text.length && REXC_DIGITS.has(text[i]!)) i++;
|
|
70
|
+
return text.slice(start, i);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
while (i < text.length) {
|
|
74
|
+
const ch = text[i]!;
|
|
75
|
+
|
|
76
|
+
// Whitespace — pass through
|
|
77
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
78
|
+
out += ch;
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Line comments
|
|
84
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
85
|
+
const start = i;
|
|
86
|
+
i += 2;
|
|
87
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
88
|
+
out += C.gray + text.slice(start, i) + C.reset;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Block comments
|
|
93
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
94
|
+
const start = i;
|
|
95
|
+
i += 2;
|
|
96
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
97
|
+
if (i < text.length) i += 2;
|
|
98
|
+
out += C.gray + text.slice(start, i) + C.reset;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Prefix digits
|
|
103
|
+
const prefix = readPrefix();
|
|
104
|
+
if (i >= text.length) {
|
|
105
|
+
out += prefix;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const tag = text[i]!;
|
|
109
|
+
|
|
110
|
+
switch (tag) {
|
|
111
|
+
case "+": // integer
|
|
112
|
+
case "*": // decimal (significand follows)
|
|
113
|
+
out += C.cyan + prefix + tag + C.reset;
|
|
114
|
+
i++;
|
|
115
|
+
break;
|
|
116
|
+
case ":": // symbol/key
|
|
117
|
+
out += C.dim + prefix + tag + C.reset;
|
|
118
|
+
i++;
|
|
119
|
+
break;
|
|
120
|
+
case "%": // opcode
|
|
121
|
+
out += C.boldBlue + prefix + tag + C.reset;
|
|
122
|
+
i++;
|
|
123
|
+
break;
|
|
124
|
+
case "$": // variable
|
|
125
|
+
out += C.yellow + prefix + tag + C.reset;
|
|
126
|
+
i++;
|
|
127
|
+
break;
|
|
128
|
+
case "@": // self
|
|
129
|
+
out += C.yellow + prefix + tag + C.reset;
|
|
130
|
+
i++;
|
|
131
|
+
break;
|
|
132
|
+
case "'": // ref
|
|
133
|
+
out += C.dim + prefix + tag + C.reset;
|
|
134
|
+
i++;
|
|
135
|
+
break;
|
|
136
|
+
case ",": { // string container
|
|
137
|
+
i++;
|
|
138
|
+
let len = 0;
|
|
139
|
+
for (const ch of prefix) len = len * 64 + (REXC_DIGITS.has(ch) ? "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_".indexOf(ch) : 0);
|
|
140
|
+
const content = text.slice(i, i + len);
|
|
141
|
+
i += len;
|
|
142
|
+
out += C.green + prefix + "," + content + C.reset;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "=": // assignment
|
|
146
|
+
case "~": // delete
|
|
147
|
+
out += C.red + prefix + tag + C.reset;
|
|
148
|
+
i++;
|
|
149
|
+
break;
|
|
150
|
+
case "?": // when
|
|
151
|
+
case "!": // unless
|
|
152
|
+
case "|": // or-chain
|
|
153
|
+
case "&": // and-chain
|
|
154
|
+
case ">": // for
|
|
155
|
+
case "<": // for-keys
|
|
156
|
+
case "#": // while
|
|
157
|
+
out += C.boldBlue + prefix + tag + C.reset;
|
|
158
|
+
i++;
|
|
159
|
+
break;
|
|
160
|
+
case ";": // break/continue
|
|
161
|
+
out += C.boldBlue + prefix + tag + C.reset;
|
|
162
|
+
i++;
|
|
163
|
+
break;
|
|
164
|
+
case "^": // pointer
|
|
165
|
+
out += C.dim + prefix + tag + C.reset;
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
case "(": case ")":
|
|
169
|
+
case "[": case "]":
|
|
170
|
+
case "{": case "}":
|
|
171
|
+
out += C.dim + prefix + C.reset + tag;
|
|
172
|
+
i++;
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
out += prefix + tag;
|
|
176
|
+
i++;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── JSON IR highlighting ─────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
const JSON_TOKEN_RE =
|
|
186
|
+
/(?<key>"(?:[^"\\]|\\.)*")\s*:|(?<string>"(?:[^"\\]|\\.)*")|(?<number>-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)\b|(?<bool>true|false)|(?<null>null)|(?<brace>[{}[\]])|(?<punct>[:,])/g;
|
|
187
|
+
|
|
188
|
+
export function highlightJSON(json: string): string {
|
|
189
|
+
let result = "";
|
|
190
|
+
let lastIndex = 0;
|
|
191
|
+
JSON_TOKEN_RE.lastIndex = 0;
|
|
192
|
+
|
|
193
|
+
for (const m of json.matchAll(JSON_TOKEN_RE)) {
|
|
194
|
+
result += json.slice(lastIndex, m.index);
|
|
195
|
+
const text = m[0];
|
|
196
|
+
const g = m.groups!;
|
|
197
|
+
if (g.key) {
|
|
198
|
+
result += C.cyan + g.key + C.reset + ":";
|
|
199
|
+
} else if (g.string) {
|
|
200
|
+
result += C.green + text + C.reset;
|
|
201
|
+
} else if (g.number) {
|
|
202
|
+
result += C.yellow + text + C.reset;
|
|
203
|
+
} else if (g.bool) {
|
|
204
|
+
result += C.yellow + text + C.reset;
|
|
205
|
+
} else if (g.null) {
|
|
206
|
+
result += C.dim + text + C.reset;
|
|
207
|
+
} else {
|
|
208
|
+
result += text;
|
|
209
|
+
}
|
|
210
|
+
lastIndex = m.index! + text.length;
|
|
211
|
+
}
|
|
212
|
+
result += json.slice(lastIndex);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Multi-line detection ──────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/** Strip string literals and comments, replacing them with spaces. */
|
|
219
|
+
function stripStringsAndComments(source: string): string {
|
|
220
|
+
return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, (m) =>
|
|
221
|
+
" ".repeat(m.length),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function countWord(text: string, word: string): number {
|
|
226
|
+
const re = new RegExp(`\\b${word}(?![a-zA-Z0-9_-])`, "g");
|
|
227
|
+
return (text.match(re) || []).length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function isIncomplete(buffer: string): boolean {
|
|
231
|
+
// If the grammar accepts it, it's complete.
|
|
232
|
+
try {
|
|
233
|
+
if (grammar.match(buffer).succeeded()) return false;
|
|
234
|
+
} catch {
|
|
235
|
+
// match itself shouldn't throw, but be safe
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const stripped = stripStringsAndComments(buffer);
|
|
239
|
+
|
|
240
|
+
// Unmatched brackets
|
|
241
|
+
let parens = 0, brackets = 0, braces = 0;
|
|
242
|
+
for (const ch of stripped) {
|
|
243
|
+
if (ch === "(") parens++;
|
|
244
|
+
else if (ch === ")") parens--;
|
|
245
|
+
else if (ch === "[") brackets++;
|
|
246
|
+
else if (ch === "]") brackets--;
|
|
247
|
+
else if (ch === "{") braces++;
|
|
248
|
+
else if (ch === "}") braces--;
|
|
249
|
+
}
|
|
250
|
+
if (parens > 0 || brackets > 0 || braces > 0) return true;
|
|
251
|
+
|
|
252
|
+
// Unmatched do/end or when/unless/for without end
|
|
253
|
+
const doCount = countWord(stripped, "do");
|
|
254
|
+
const endCount = countWord(stripped, "end");
|
|
255
|
+
if (doCount > endCount) return true;
|
|
256
|
+
|
|
257
|
+
// Trailing binary operator or keyword suggests continuation
|
|
258
|
+
const trimmed = buffer.trimEnd();
|
|
259
|
+
if (/[+\-*/%&|^=<>]$/.test(trimmed)) return true;
|
|
260
|
+
if (/\b(?:and|or|do|in|of)\s*$/.test(trimmed)) return true;
|
|
261
|
+
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Result & state formatting ─────────────────────────────────
|
|
266
|
+
|
|
267
|
+
function formatResult(value: unknown): string {
|
|
268
|
+
let text: string;
|
|
269
|
+
try {
|
|
270
|
+
text = stringify(value, { maxWidth: 60 });
|
|
271
|
+
} catch {
|
|
272
|
+
text = String(value);
|
|
273
|
+
}
|
|
274
|
+
return `${C.gray}→${C.reset} ${highlightLine(text)}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function formatVarState(vars: Record<string, unknown>): string {
|
|
278
|
+
const entries = Object.entries(vars);
|
|
279
|
+
if (entries.length === 0) return "";
|
|
280
|
+
|
|
281
|
+
const MAX_LINE = 70;
|
|
282
|
+
const MAX_VALUE = 30;
|
|
283
|
+
const parts: string[] = [];
|
|
284
|
+
let totalLen = 0;
|
|
285
|
+
|
|
286
|
+
for (const [key, val] of entries) {
|
|
287
|
+
let valStr: string;
|
|
288
|
+
try {
|
|
289
|
+
valStr = stringify(val, { maxWidth: MAX_VALUE });
|
|
290
|
+
} catch {
|
|
291
|
+
valStr = String(val);
|
|
292
|
+
}
|
|
293
|
+
if (valStr.length > MAX_VALUE) {
|
|
294
|
+
valStr = valStr.slice(0, MAX_VALUE - 1) + "\u2026";
|
|
295
|
+
}
|
|
296
|
+
const part = `${key} = ${valStr}`;
|
|
297
|
+
if (totalLen + part.length + 2 > MAX_LINE && parts.length > 0) {
|
|
298
|
+
parts.push("\u2026");
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
parts.push(part);
|
|
302
|
+
totalLen += part.length + 2;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return `${C.dim} ${parts.join(", ")}${C.reset}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Tab completion ────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
const KEYWORDS = [
|
|
311
|
+
"when", "unless", "while", "for", "do", "end", "in", "of",
|
|
312
|
+
"and", "or", "else", "break", "continue", "delete",
|
|
313
|
+
"self", "true", "false", "null", "undefined", "nan", "inf",
|
|
314
|
+
"string", "number", "object", "array", "boolean",
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
function completer(state: ReplState): (line: string) => [string[], string] {
|
|
318
|
+
return (line: string) => {
|
|
319
|
+
const match = line.match(/[a-zA-Z_][a-zA-Z0-9_.-]*$/);
|
|
320
|
+
const partial = match ? match[0] : "";
|
|
321
|
+
if (!partial) return [[], ""];
|
|
322
|
+
|
|
323
|
+
const varNames = Object.keys(state.vars);
|
|
324
|
+
const all = [...new Set([...KEYWORDS, ...varNames])];
|
|
325
|
+
const hits = all.filter((w) => w.startsWith(partial));
|
|
326
|
+
return [hits, partial];
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Dot commands ──────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function handleDotCommand(cmd: string, state: ReplState, rl: readline.Interface): boolean {
|
|
333
|
+
function toggleLabel(on: boolean): string {
|
|
334
|
+
return on ? `${C.green}on${C.reset}` : `${C.dim}off${C.reset}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
switch (cmd) {
|
|
338
|
+
case ".help":
|
|
339
|
+
console.log(
|
|
340
|
+
[
|
|
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",
|
|
349
|
+
"",
|
|
350
|
+
"Enter Rex expressions to evaluate them.",
|
|
351
|
+
"Multi-line: open brackets or do/end blocks continue on the next line.",
|
|
352
|
+
"Ctrl-C cancels multi-line input.",
|
|
353
|
+
"Ctrl-D exits.",
|
|
354
|
+
].join("\n"),
|
|
355
|
+
);
|
|
356
|
+
return true;
|
|
357
|
+
|
|
358
|
+
case ".ir":
|
|
359
|
+
state.showIR = !state.showIR;
|
|
360
|
+
console.log(`${C.dim} IR display: ${toggleLabel(state.showIR)}${C.reset}`);
|
|
361
|
+
return true;
|
|
362
|
+
|
|
363
|
+
case ".rexc":
|
|
364
|
+
state.showRexc = !state.showRexc;
|
|
365
|
+
console.log(`${C.dim} Rexc display: ${toggleLabel(state.showRexc)}${C.reset}`);
|
|
366
|
+
return true;
|
|
367
|
+
|
|
368
|
+
case ".opt":
|
|
369
|
+
state.optimize = !state.optimize;
|
|
370
|
+
console.log(`${C.dim} Optimizations: ${toggleLabel(state.optimize)}${C.reset}`);
|
|
371
|
+
return true;
|
|
372
|
+
|
|
373
|
+
case ".vars": {
|
|
374
|
+
const entries = Object.entries(state.vars);
|
|
375
|
+
if (entries.length === 0) {
|
|
376
|
+
console.log(`${C.dim} (no variables)${C.reset}`);
|
|
377
|
+
} else {
|
|
378
|
+
for (const [key, val] of entries) {
|
|
379
|
+
let valStr: string;
|
|
380
|
+
try {
|
|
381
|
+
valStr = stringify(val, { maxWidth: 60 });
|
|
382
|
+
} catch {
|
|
383
|
+
valStr = String(val);
|
|
384
|
+
}
|
|
385
|
+
console.log(` ${key} = ${highlightLine(valStr)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case ".clear":
|
|
392
|
+
state.vars = {};
|
|
393
|
+
state.refs = {};
|
|
394
|
+
console.log(`${C.dim} Variables cleared.${C.reset}`);
|
|
395
|
+
return true;
|
|
396
|
+
|
|
397
|
+
case ".exit":
|
|
398
|
+
rl.close();
|
|
399
|
+
return true;
|
|
400
|
+
|
|
401
|
+
default:
|
|
402
|
+
if (cmd.startsWith(".")) {
|
|
403
|
+
console.log(`${C.red} Unknown command: ${cmd}. Type .help for available commands.${C.reset}`);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── REPL state ────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
type ReplState = {
|
|
413
|
+
vars: Record<string, unknown>;
|
|
414
|
+
refs: Partial<Record<number, unknown>>;
|
|
415
|
+
showIR: boolean;
|
|
416
|
+
showRexc: boolean;
|
|
417
|
+
optimize: boolean;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// ── Gas limit for loop safety ─────────────────────────────────
|
|
421
|
+
|
|
422
|
+
const GAS_LIMIT = 10_000_000;
|
|
423
|
+
|
|
424
|
+
// ── Main REPL entry point ─────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
export async function startRepl(): Promise<void> {
|
|
427
|
+
const state: ReplState = { vars: {}, refs: {}, showIR: false, showRexc: false, optimize: false };
|
|
428
|
+
let multiLineBuffer = "";
|
|
429
|
+
|
|
430
|
+
const PRIMARY_PROMPT = "rex> ";
|
|
431
|
+
const CONT_PROMPT = "... ";
|
|
432
|
+
const STYLED_PRIMARY = `${C.boldBlue}rex${C.reset}> `;
|
|
433
|
+
const STYLED_CONT = `${C.dim}...${C.reset} `;
|
|
434
|
+
|
|
435
|
+
let currentPrompt = PRIMARY_PROMPT;
|
|
436
|
+
let styledPrompt = STYLED_PRIMARY;
|
|
437
|
+
|
|
438
|
+
console.log(`${C.boldBlue}Rex${C.reset} v${version} — type ${C.dim}.help${C.reset} for commands`);
|
|
439
|
+
|
|
440
|
+
const rl = readline.createInterface({
|
|
441
|
+
input: process.stdin,
|
|
442
|
+
output: process.stdout,
|
|
443
|
+
prompt: PRIMARY_PROMPT,
|
|
444
|
+
historySize: 500,
|
|
445
|
+
completer: completer(state),
|
|
446
|
+
terminal: true,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ── Syntax highlighting via keypress redraw ──
|
|
450
|
+
process.stdin.on("keypress", () => {
|
|
451
|
+
process.nextTick(() => {
|
|
452
|
+
// Guard: if the interface has been closed, skip
|
|
453
|
+
if (!rl.line && rl.line !== "") return;
|
|
454
|
+
|
|
455
|
+
readline.clearLine(process.stdout, 0);
|
|
456
|
+
readline.cursorTo(process.stdout, 0);
|
|
457
|
+
process.stdout.write(styledPrompt + highlightLine(rl.line));
|
|
458
|
+
readline.cursorTo(process.stdout, currentPrompt.length + rl.cursor);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ── Ctrl-L to clear screen ──
|
|
463
|
+
process.stdin.on("keypress", (_ch: string, key: readline.Key) => {
|
|
464
|
+
if (key?.ctrl && key.name === "l") {
|
|
465
|
+
readline.cursorTo(process.stdout, 0, 0);
|
|
466
|
+
readline.clearScreenDown(process.stdout);
|
|
467
|
+
rl.prompt();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ── Ctrl-C handling ──
|
|
472
|
+
rl.on("SIGINT", () => {
|
|
473
|
+
if (multiLineBuffer) {
|
|
474
|
+
multiLineBuffer = "";
|
|
475
|
+
currentPrompt = PRIMARY_PROMPT;
|
|
476
|
+
styledPrompt = STYLED_PRIMARY;
|
|
477
|
+
rl.setPrompt(PRIMARY_PROMPT);
|
|
478
|
+
process.stdout.write("\n");
|
|
479
|
+
rl.prompt();
|
|
480
|
+
} else {
|
|
481
|
+
console.log();
|
|
482
|
+
rl.close();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
function resetPrompt() {
|
|
487
|
+
currentPrompt = PRIMARY_PROMPT;
|
|
488
|
+
styledPrompt = STYLED_PRIMARY;
|
|
489
|
+
rl.setPrompt(PRIMARY_PROMPT);
|
|
490
|
+
rl.prompt();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Line handler ──
|
|
494
|
+
rl.on("line", (line: string) => {
|
|
495
|
+
const trimmed = line.trim();
|
|
496
|
+
|
|
497
|
+
// Dot commands (only when not accumulating multi-line)
|
|
498
|
+
if (!multiLineBuffer && trimmed.startsWith(".")) {
|
|
499
|
+
if (handleDotCommand(trimmed, state, rl)) {
|
|
500
|
+
rl.prompt();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Accumulate
|
|
506
|
+
multiLineBuffer += (multiLineBuffer ? "\n" : "") + line;
|
|
507
|
+
|
|
508
|
+
// Empty input
|
|
509
|
+
if (multiLineBuffer.trim() === "") {
|
|
510
|
+
multiLineBuffer = "";
|
|
511
|
+
rl.prompt();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Check for incomplete expression
|
|
516
|
+
if (isIncomplete(multiLineBuffer)) {
|
|
517
|
+
currentPrompt = CONT_PROMPT;
|
|
518
|
+
styledPrompt = STYLED_CONT;
|
|
519
|
+
rl.setPrompt(CONT_PROMPT);
|
|
520
|
+
rl.prompt();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Try to evaluate
|
|
525
|
+
const source = multiLineBuffer;
|
|
526
|
+
multiLineBuffer = "";
|
|
527
|
+
|
|
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
|
+
}
|
|
568
|
+
|
|
569
|
+
resetPrompt();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ── Exit ──
|
|
573
|
+
rl.on("close", () => {
|
|
574
|
+
process.exit(0);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
rl.prompt();
|
|
578
|
+
}
|