@creationix/rex 0.3.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +9 -6
- package/rex-cli.js +1334 -1190
- package/rex-cli.ts +268 -27
- package/rex-repl.js +1048 -1135
- package/rex-repl.ts +392 -103
- package/rex.js +290 -954
- package/rex.ohm +48 -21
- package/rex.ohm-bundle.cjs +1 -1
- package/rex.ohm-bundle.d.ts +27 -8
- package/rex.ohm-bundle.js +1 -1
- package/rex.ts +388 -218
- package/rexc-interpreter.ts +386 -88
- package/rx-cli.js +2836 -0
- package/rx-cli.ts +298 -0
package/rex-cli.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { compile, parse, parseToIR, stringify } from "./rex.ts";
|
|
2
|
-
import { evaluateSource } from "./rexc-interpreter.ts";
|
|
1
|
+
import { compile, parse, parseToIR, stringify, grammar } from "./rex.ts";
|
|
2
|
+
import { evaluateRexc, evaluateSource } from "./rexc-interpreter.ts";
|
|
3
|
+
import { highlightLine, highlightJSON, highlightAuto, setColorEnabled } from "./rex-repl.ts";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
|
|
7
|
+
type SourceSegment = { type: "expr"; value: string } | { type: "file"; path: string };
|
|
8
|
+
|
|
6
9
|
type CliOptions = {
|
|
7
|
-
|
|
8
|
-
file?: string;
|
|
10
|
+
sources: SourceSegment[];
|
|
9
11
|
out?: string;
|
|
10
12
|
compile: boolean;
|
|
11
13
|
ir: boolean;
|
|
@@ -13,15 +15,36 @@ type CliOptions = {
|
|
|
13
15
|
dedupeValues: boolean;
|
|
14
16
|
dedupeMinBytes?: number;
|
|
15
17
|
help: boolean;
|
|
18
|
+
cat: boolean;
|
|
19
|
+
showSource: boolean;
|
|
20
|
+
showExpr: boolean;
|
|
21
|
+
showIR: boolean;
|
|
22
|
+
showRexc: boolean;
|
|
23
|
+
showVars: boolean;
|
|
24
|
+
color: boolean;
|
|
25
|
+
colorExplicit: boolean;
|
|
26
|
+
showExprExplicit: boolean;
|
|
27
|
+
format: "rex" | "json";
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
function parseArgs(argv: string[]): CliOptions {
|
|
19
31
|
const options: CliOptions = {
|
|
32
|
+
sources: [],
|
|
20
33
|
compile: false,
|
|
21
34
|
ir: false,
|
|
22
35
|
minifyNames: false,
|
|
23
36
|
dedupeValues: false,
|
|
24
37
|
help: false,
|
|
38
|
+
cat: false,
|
|
39
|
+
showSource: false,
|
|
40
|
+
showExpr: true,
|
|
41
|
+
showIR: false,
|
|
42
|
+
showRexc: false,
|
|
43
|
+
showVars: false,
|
|
44
|
+
color: process.stdout.isTTY,
|
|
45
|
+
colorExplicit: false,
|
|
46
|
+
showExprExplicit: false,
|
|
47
|
+
format: "rex",
|
|
25
48
|
};
|
|
26
49
|
for (let index = 0; index < argv.length; index += 1) {
|
|
27
50
|
const arg = argv[index];
|
|
@@ -30,6 +53,58 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
30
53
|
options.help = true;
|
|
31
54
|
continue;
|
|
32
55
|
}
|
|
56
|
+
if (arg === "--cat") {
|
|
57
|
+
options.cat = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "--show-source") {
|
|
61
|
+
options.showSource = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--show-expr") {
|
|
65
|
+
options.showExpr = true;
|
|
66
|
+
options.showExprExplicit = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--no-expr") {
|
|
70
|
+
options.showExpr = false;
|
|
71
|
+
options.showExprExplicit = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--show-ir") {
|
|
75
|
+
options.showIR = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--show-rexc") {
|
|
79
|
+
options.showRexc = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--show-vars") {
|
|
83
|
+
options.showVars = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--json") {
|
|
87
|
+
options.format = "json";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--format") {
|
|
91
|
+
const value = argv[index + 1];
|
|
92
|
+
if (!value) throw new Error("Missing value for --format");
|
|
93
|
+
if (value !== "rex" && value !== "json") throw new Error("--format must be 'rex' or 'json'");
|
|
94
|
+
options.format = value;
|
|
95
|
+
index += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === "--color") {
|
|
99
|
+
options.color = true;
|
|
100
|
+
options.colorExplicit = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg === "--no-color") {
|
|
104
|
+
options.color = false;
|
|
105
|
+
options.colorExplicit = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
33
108
|
if (arg === "--compile" || arg === "-c") {
|
|
34
109
|
options.compile = true;
|
|
35
110
|
continue;
|
|
@@ -58,14 +133,14 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
58
133
|
if (arg === "--expr" || arg === "-e") {
|
|
59
134
|
const value = argv[index + 1];
|
|
60
135
|
if (!value) throw new Error("Missing value for --expr");
|
|
61
|
-
options.expr
|
|
136
|
+
options.sources.push({ type: "expr", value });
|
|
62
137
|
index += 1;
|
|
63
138
|
continue;
|
|
64
139
|
}
|
|
65
140
|
if (arg === "--file" || arg === "-f") {
|
|
66
141
|
const value = argv[index + 1];
|
|
67
142
|
if (!value) throw new Error("Missing value for --file");
|
|
68
|
-
options.file
|
|
143
|
+
options.sources.push({ type: "file", path: value });
|
|
69
144
|
index += 1;
|
|
70
145
|
continue;
|
|
71
146
|
}
|
|
@@ -78,8 +153,7 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
78
153
|
}
|
|
79
154
|
// Positional argument = file path
|
|
80
155
|
if (!arg.startsWith("-")) {
|
|
81
|
-
|
|
82
|
-
options.file = arg;
|
|
156
|
+
options.sources.push({ type: "file", path: arg });
|
|
83
157
|
continue;
|
|
84
158
|
}
|
|
85
159
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -98,15 +172,39 @@ function usage() {
|
|
|
98
172
|
" cat input.rex | rex Evaluate from stdin",
|
|
99
173
|
" rex -c input.rex Compile to rexc bytecode",
|
|
100
174
|
"",
|
|
175
|
+
" Sources are concatenated in order, so flags and files can be mixed:",
|
|
176
|
+
" rex -e 'max = 200' primes.rex Set max before running script",
|
|
177
|
+
" rex primes.rex -e '42' Run script, then evaluate 42",
|
|
178
|
+
"",
|
|
101
179
|
"Input:",
|
|
102
180
|
" <file> Evaluate/compile a Rex source file",
|
|
103
181
|
" -e, --expr <source> Evaluate/compile an inline expression",
|
|
104
182
|
" -f, --file <path> Evaluate/compile source from a file",
|
|
105
183
|
"",
|
|
184
|
+
" Multiple -e and -f flags (and positional files) can be combined.",
|
|
185
|
+
" They are concatenated in the order they appear on the command line.",
|
|
186
|
+
"",
|
|
106
187
|
"Output mode:",
|
|
107
|
-
" (default) Evaluate and output result
|
|
108
|
-
" -c, --compile
|
|
109
|
-
" --ir
|
|
188
|
+
" (default) Evaluate and output result",
|
|
189
|
+
" -c, --compile Show rexc only (same as --show-rexc --no-expr)",
|
|
190
|
+
" --ir Show IR only (same as --show-ir --no-expr)",
|
|
191
|
+
" --cat Print input with Rex highlighting",
|
|
192
|
+
" --show-source Show input source text",
|
|
193
|
+
" --show-expr Show expression result",
|
|
194
|
+
" --no-expr Hide expression result",
|
|
195
|
+
" --show-ir Show IR JSON",
|
|
196
|
+
" --show-rexc Show rexc bytecode",
|
|
197
|
+
" --show-vars Show variable state",
|
|
198
|
+
"",
|
|
199
|
+
"Output formatting:",
|
|
200
|
+
" --format <type> Output format: rex|json (default: rex)",
|
|
201
|
+
" --json Shortcut for --format json",
|
|
202
|
+
" --color Force color output",
|
|
203
|
+
" --no-color Disable color output",
|
|
204
|
+
"",
|
|
205
|
+
"TTY vs non-TTY:",
|
|
206
|
+
" TTY output prints labeled blocks when multiple outputs are selected.",
|
|
207
|
+
" Non-TTY output emits a single value (object if multiple outputs).",
|
|
110
208
|
"",
|
|
111
209
|
"Compile options:",
|
|
112
210
|
" -m, --minify-names Minify local variable names",
|
|
@@ -128,9 +226,14 @@ async function readStdin(): Promise<string> {
|
|
|
128
226
|
}
|
|
129
227
|
|
|
130
228
|
async function resolveSource(options: CliOptions): Promise<string> {
|
|
131
|
-
if (options.
|
|
132
|
-
|
|
133
|
-
|
|
229
|
+
if (options.sources.length > 0) {
|
|
230
|
+
const parts: string[] = [];
|
|
231
|
+
for (const seg of options.sources) {
|
|
232
|
+
if (seg.type === "expr") parts.push(seg.value);
|
|
233
|
+
else parts.push(await readFile(seg.path, "utf8"));
|
|
234
|
+
}
|
|
235
|
+
return parts.join("\n");
|
|
236
|
+
}
|
|
134
237
|
if (!process.stdin.isTTY) {
|
|
135
238
|
const piped = await readStdin();
|
|
136
239
|
if (piped.trim().length > 0) return piped;
|
|
@@ -138,6 +241,13 @@ async function resolveSource(options: CliOptions): Promise<string> {
|
|
|
138
241
|
throw new Error("No input provided. Use a file path, --expr, or pipe source via stdin.");
|
|
139
242
|
}
|
|
140
243
|
|
|
244
|
+
function findFirstFilePath(sources: SourceSegment[]): string | undefined {
|
|
245
|
+
for (const seg of sources) {
|
|
246
|
+
if (seg.type === "file") return seg.path;
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
141
251
|
async function loadDomainConfigFromFolder(folderPath: string): Promise<unknown | undefined> {
|
|
142
252
|
const configPath = resolve(folderPath, ".config.rex");
|
|
143
253
|
try {
|
|
@@ -149,19 +259,64 @@ async function loadDomainConfigFromFolder(folderPath: string): Promise<unknown |
|
|
|
149
259
|
}
|
|
150
260
|
|
|
151
261
|
async function resolveDomainConfig(options: CliOptions): Promise<unknown | undefined> {
|
|
152
|
-
const
|
|
262
|
+
const filePath = findFirstFilePath(options.sources);
|
|
263
|
+
const baseFolder = filePath ? dirname(resolve(filePath)) : process.cwd();
|
|
153
264
|
return loadDomainConfigFromFolder(baseFolder);
|
|
154
265
|
}
|
|
155
266
|
|
|
267
|
+
function formatJson(value: unknown, indent = 2): string {
|
|
268
|
+
const normalized = normalizeJsonValue(value, false);
|
|
269
|
+
const text = JSON.stringify(normalized, null, indent);
|
|
270
|
+
return text ?? "null";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeJsonValue(value: unknown, inArray: boolean): unknown {
|
|
274
|
+
if (value === undefined) return inArray ? null : undefined;
|
|
275
|
+
if (value === null || typeof value !== "object") return value;
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
return value.map((item) => normalizeJsonValue(item, true));
|
|
278
|
+
}
|
|
279
|
+
const out: Record<string, unknown> = {};
|
|
280
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
281
|
+
const normalized = normalizeJsonValue(val, false);
|
|
282
|
+
if (normalized !== undefined) out[key] = normalized;
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isRexSource(source: string): boolean {
|
|
288
|
+
try {
|
|
289
|
+
return grammar.match(source).succeeded();
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
156
295
|
async function main() {
|
|
157
296
|
const options = parseArgs(process.argv.slice(2));
|
|
158
297
|
if (options.help) {
|
|
159
298
|
console.log(usage());
|
|
160
299
|
return;
|
|
161
300
|
}
|
|
301
|
+
setColorEnabled(options.color);
|
|
302
|
+
if (options.cat) {
|
|
303
|
+
const source = await resolveSource(options);
|
|
304
|
+
const hasRexc = options.sources.some((seg) => seg.type === "file" && seg.path.endsWith(".rexc"));
|
|
305
|
+
const hasRex = options.sources.some((seg) => seg.type === "file" && seg.path.endsWith(".rex"));
|
|
306
|
+
const hint = hasRexc && !hasRex ? "rexc" : hasRex && !hasRexc ? "rex" : undefined;
|
|
307
|
+
const output = (process.stdout.isTTY && options.color)
|
|
308
|
+
? highlightAuto(source, hint)
|
|
309
|
+
: source;
|
|
310
|
+
if (options.out) {
|
|
311
|
+
await writeFile(options.out, `${output}\n`, "utf8");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log(output);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
162
317
|
|
|
163
318
|
// No source provided on a TTY → launch interactive REPL
|
|
164
|
-
const hasSource = options.
|
|
319
|
+
const hasSource = options.sources.length > 0 || !process.stdin.isTTY;
|
|
165
320
|
if (!hasSource && !options.compile && !options.ir) {
|
|
166
321
|
const { startRepl } = await import("./rex-repl.ts");
|
|
167
322
|
await startRepl();
|
|
@@ -169,21 +324,107 @@ async function main() {
|
|
|
169
324
|
}
|
|
170
325
|
|
|
171
326
|
const source = await resolveSource(options);
|
|
327
|
+
const sourceIsRex = isRexSource(source);
|
|
172
328
|
|
|
173
|
-
|
|
329
|
+
if (options.compile) {
|
|
330
|
+
if (!sourceIsRex) throw new Error("--compile requires Rex source");
|
|
331
|
+
options.showRexc = true;
|
|
332
|
+
if (!options.showExprExplicit) options.showExpr = false;
|
|
333
|
+
}
|
|
174
334
|
if (options.ir) {
|
|
175
|
-
|
|
176
|
-
|
|
335
|
+
if (!sourceIsRex) throw new Error("--ir requires Rex source");
|
|
336
|
+
options.showIR = true;
|
|
337
|
+
if (!options.showExprExplicit) options.showExpr = false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const outputFlags = [options.showSource, options.showExpr, options.showIR, options.showRexc, options.showVars].filter(Boolean).length;
|
|
341
|
+
if (outputFlags === 0) {
|
|
342
|
+
throw new Error("No output selected. Use --show-source, --show-expr, --show-ir, --show-rexc, or --show-vars.");
|
|
343
|
+
}
|
|
344
|
+
const humanMode = process.stdout.isTTY && options.color;
|
|
345
|
+
type OutputKey = "source" | "ir" | "rexc" | "vars" | "result";
|
|
346
|
+
const outputs: Partial<Record<OutputKey, unknown>> = {};
|
|
347
|
+
if (options.showSource) outputs.source = source;
|
|
348
|
+
if (!sourceIsRex && options.showIR) {
|
|
349
|
+
throw new Error("--show-ir is only available for Rex source");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (options.showIR) {
|
|
353
|
+
outputs.ir = parseToIR(source);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (options.showRexc) {
|
|
177
357
|
const domainConfig = await resolveDomainConfig(options);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
358
|
+
outputs.rexc = sourceIsRex
|
|
359
|
+
? compile(source, {
|
|
360
|
+
minifyNames: options.minifyNames,
|
|
361
|
+
dedupeValues: options.dedupeValues,
|
|
362
|
+
dedupeMinBytes: options.dedupeMinBytes,
|
|
363
|
+
domainConfig,
|
|
364
|
+
})
|
|
365
|
+
: source;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (options.showExpr || options.showVars) {
|
|
369
|
+
const result = sourceIsRex ? evaluateSource(source) : evaluateRexc(source);
|
|
370
|
+
if (options.showExpr) outputs.result = result.value;
|
|
371
|
+
if (options.showVars) outputs.vars = result.state.vars;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const order: OutputKey[] = ["source", "ir", "rexc", "vars", "result"];
|
|
375
|
+
const selected = order.filter((key) => outputs[key] !== undefined);
|
|
376
|
+
|
|
377
|
+
function formatValue(value: unknown, kind: OutputKey): string {
|
|
378
|
+
if (kind === "source") {
|
|
379
|
+
const raw = String(value ?? "");
|
|
380
|
+
if (options.format === "json") {
|
|
381
|
+
const json = formatJson(raw, 2);
|
|
382
|
+
return humanMode && options.color ? highlightJSON(json) : json;
|
|
383
|
+
}
|
|
384
|
+
return humanMode && options.color ? highlightAuto(raw) : raw;
|
|
385
|
+
}
|
|
386
|
+
if (kind === "rexc" && humanMode) {
|
|
387
|
+
const raw = String(value ?? "");
|
|
388
|
+
return options.color ? highlightAuto(raw, "rexc") : raw;
|
|
389
|
+
}
|
|
390
|
+
if (options.format === "json") {
|
|
391
|
+
const raw = formatJson(value, 2);
|
|
392
|
+
return humanMode && options.color ? highlightJSON(raw) : raw;
|
|
393
|
+
}
|
|
394
|
+
if (kind === "rexc") {
|
|
395
|
+
const raw = stringify(String(value ?? ""));
|
|
396
|
+
return humanMode && options.color ? highlightLine(raw) : raw;
|
|
397
|
+
}
|
|
398
|
+
const raw = stringify(value);
|
|
399
|
+
return humanMode && options.color ? highlightLine(raw) : raw;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let output: string;
|
|
403
|
+
if (humanMode) {
|
|
404
|
+
const header = options.color ? "\x1b[90m" : "";
|
|
405
|
+
const reset = options.color ? "\x1b[0m" : "";
|
|
406
|
+
const lines: string[] = [];
|
|
407
|
+
for (const key of order) {
|
|
408
|
+
const value = outputs[key];
|
|
409
|
+
if (value === undefined) continue;
|
|
410
|
+
lines.push(`${header}${key}:${reset} ${formatValue(value, key)}`);
|
|
411
|
+
}
|
|
412
|
+
output = lines.join("\n");
|
|
184
413
|
} else {
|
|
185
|
-
|
|
186
|
-
|
|
414
|
+
let value: unknown;
|
|
415
|
+
if (selected.length === 1) {
|
|
416
|
+
const only = selected[0];
|
|
417
|
+
if (!only) throw new Error("No output selected.");
|
|
418
|
+
value = outputs[only];
|
|
419
|
+
} else {
|
|
420
|
+
const out: Record<string, unknown> = {};
|
|
421
|
+
for (const key of order) {
|
|
422
|
+
const v = outputs[key];
|
|
423
|
+
if (v !== undefined) out[key] = v;
|
|
424
|
+
}
|
|
425
|
+
value = out;
|
|
426
|
+
}
|
|
427
|
+
output = options.format === "json" ? formatJson(value, 2) : stringify(value);
|
|
187
428
|
}
|
|
188
429
|
|
|
189
430
|
if (options.out) {
|