@isl-lang/repl 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli.js +2156 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2284 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +589 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +2228 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -14
- package/index.js +0 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// src/repl.ts
|
|
11
|
+
import * as readline from "readline";
|
|
12
|
+
|
|
13
|
+
// src/session.ts
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
var Session = class {
|
|
17
|
+
/** Defined intents in this session */
|
|
18
|
+
intents = /* @__PURE__ */ new Map();
|
|
19
|
+
/** Variables set during the session */
|
|
20
|
+
variables = /* @__PURE__ */ new Map();
|
|
21
|
+
/** Command history */
|
|
22
|
+
history = [];
|
|
23
|
+
/** Last evaluation result */
|
|
24
|
+
lastResult = void 0;
|
|
25
|
+
/** Loaded files */
|
|
26
|
+
loadedFiles = /* @__PURE__ */ new Set();
|
|
27
|
+
/** Session configuration */
|
|
28
|
+
config;
|
|
29
|
+
/** Evaluation context (set by .context command) */
|
|
30
|
+
evalContext = {};
|
|
31
|
+
/** Pre-state context for old() expressions */
|
|
32
|
+
preContext = null;
|
|
33
|
+
/** Loaded domain AST (from real parser) */
|
|
34
|
+
domainAST = null;
|
|
35
|
+
constructor(config = {}) {
|
|
36
|
+
this.config = {
|
|
37
|
+
colors: true,
|
|
38
|
+
verbose: false,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
...config
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Intent Management
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Define a new intent
|
|
48
|
+
*/
|
|
49
|
+
defineIntent(intent) {
|
|
50
|
+
this.intents.set(intent.name, intent);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get an intent by name
|
|
54
|
+
*/
|
|
55
|
+
getIntent(name) {
|
|
56
|
+
return this.intents.get(name);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get all defined intents
|
|
60
|
+
*/
|
|
61
|
+
getAllIntents() {
|
|
62
|
+
return Array.from(this.intents.values());
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if an intent exists
|
|
66
|
+
*/
|
|
67
|
+
hasIntent(name) {
|
|
68
|
+
return this.intents.has(name);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove an intent
|
|
72
|
+
*/
|
|
73
|
+
removeIntent(name) {
|
|
74
|
+
return this.intents.delete(name);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get intent names for completion
|
|
78
|
+
*/
|
|
79
|
+
getIntentNames() {
|
|
80
|
+
return Array.from(this.intents.keys());
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse an intent definition from source code
|
|
84
|
+
*/
|
|
85
|
+
parseIntent(source) {
|
|
86
|
+
const trimmed = source.trim();
|
|
87
|
+
const match = trimmed.match(/^intent\s+(\w+)\s*\{([\s\S]*)\}$/);
|
|
88
|
+
if (!match) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const name = match[1];
|
|
92
|
+
const body = match[2];
|
|
93
|
+
const intent = {
|
|
94
|
+
name,
|
|
95
|
+
preconditions: [],
|
|
96
|
+
postconditions: [],
|
|
97
|
+
invariants: [],
|
|
98
|
+
scenarios: [],
|
|
99
|
+
rawSource: source
|
|
100
|
+
};
|
|
101
|
+
const preMatch = body.match(/pre(?:conditions?)?\s*:\s*([^\n]+)/g);
|
|
102
|
+
if (preMatch) {
|
|
103
|
+
for (const pre of preMatch) {
|
|
104
|
+
const expr = pre.replace(/pre(?:conditions?)?\s*:\s*/, "").trim();
|
|
105
|
+
if (expr) {
|
|
106
|
+
intent.preconditions.push({ expression: expr });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const postMatch = body.match(/post(?:conditions?)?\s*:\s*([^\n]+)/g);
|
|
111
|
+
if (postMatch) {
|
|
112
|
+
for (const post of postMatch) {
|
|
113
|
+
const expr = post.replace(/post(?:conditions?)?\s*:\s*/, "").trim();
|
|
114
|
+
if (expr) {
|
|
115
|
+
intent.postconditions.push({ expression: expr });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const invMatch = body.match(/invariants?\s*:\s*([^\n]+)/g);
|
|
120
|
+
if (invMatch) {
|
|
121
|
+
for (const inv of invMatch) {
|
|
122
|
+
const expr = inv.replace(/invariants?\s*:\s*/, "").trim();
|
|
123
|
+
if (expr) {
|
|
124
|
+
intent.invariants.push({ expression: expr });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return intent;
|
|
129
|
+
}
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Variable Management
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Set a variable
|
|
135
|
+
*/
|
|
136
|
+
setVariable(name, value) {
|
|
137
|
+
this.variables.set(name, value);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get a variable
|
|
141
|
+
*/
|
|
142
|
+
getVariable(name) {
|
|
143
|
+
return this.variables.get(name);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get all variables
|
|
147
|
+
*/
|
|
148
|
+
getAllVariables() {
|
|
149
|
+
return new Map(this.variables);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if a variable exists
|
|
153
|
+
*/
|
|
154
|
+
hasVariable(name) {
|
|
155
|
+
return this.variables.has(name);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set the last result (accessible as _)
|
|
159
|
+
*/
|
|
160
|
+
setLastResult(value) {
|
|
161
|
+
this.lastResult = value;
|
|
162
|
+
this.variables.set("_", value);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the last result
|
|
166
|
+
*/
|
|
167
|
+
getLastResult() {
|
|
168
|
+
return this.lastResult;
|
|
169
|
+
}
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// History Management
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Add to history
|
|
175
|
+
*/
|
|
176
|
+
addToHistory(entry) {
|
|
177
|
+
const trimmed = entry.trim();
|
|
178
|
+
if (trimmed && (this.history.length === 0 || this.history[this.history.length - 1] !== trimmed)) {
|
|
179
|
+
this.history.push(trimmed);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get history
|
|
184
|
+
*/
|
|
185
|
+
getHistory(count) {
|
|
186
|
+
if (count) {
|
|
187
|
+
return this.history.slice(-count);
|
|
188
|
+
}
|
|
189
|
+
return [...this.history];
|
|
190
|
+
}
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
192
|
+
// File Loading
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
194
|
+
/**
|
|
195
|
+
* Load intents from an ISL file
|
|
196
|
+
*/
|
|
197
|
+
async loadFile(filePath) {
|
|
198
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(this.config.cwd, filePath);
|
|
199
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
200
|
+
return { intents: [], errors: [`File not found: ${resolvedPath}`] };
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
204
|
+
const loadedIntents = [];
|
|
205
|
+
const errors = [];
|
|
206
|
+
const intentRegex = /intent\s+(\w+)\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}/g;
|
|
207
|
+
let match;
|
|
208
|
+
while ((match = intentRegex.exec(content)) !== null) {
|
|
209
|
+
const intent = this.parseIntent(match[0]);
|
|
210
|
+
if (intent) {
|
|
211
|
+
this.defineIntent(intent);
|
|
212
|
+
loadedIntents.push(intent);
|
|
213
|
+
} else {
|
|
214
|
+
errors.push(`Failed to parse intent starting at position ${match.index}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const behaviorRegex = /behavior\s+(\w+)\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
218
|
+
while ((match = behaviorRegex.exec(content)) !== null) {
|
|
219
|
+
const intent = this.parseBehaviorAsIntent(match[1], match[2]);
|
|
220
|
+
if (intent) {
|
|
221
|
+
this.defineIntent(intent);
|
|
222
|
+
loadedIntents.push(intent);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.loadedFiles.add(resolvedPath);
|
|
226
|
+
return { intents: loadedIntents, errors };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
intents: [],
|
|
230
|
+
errors: [`Failed to load file: ${error instanceof Error ? error.message : String(error)}`]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Parse a behavior block as an intent
|
|
236
|
+
*/
|
|
237
|
+
parseBehaviorAsIntent(name, body) {
|
|
238
|
+
const intent = {
|
|
239
|
+
name,
|
|
240
|
+
preconditions: [],
|
|
241
|
+
postconditions: [],
|
|
242
|
+
invariants: [],
|
|
243
|
+
scenarios: [],
|
|
244
|
+
rawSource: `behavior ${name} {${body}}`
|
|
245
|
+
};
|
|
246
|
+
const preSection = body.match(/pre(?:conditions)?\s*\{([^}]*)\}/s);
|
|
247
|
+
if (preSection) {
|
|
248
|
+
const conditions = preSection[1].trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
249
|
+
for (const cond of conditions) {
|
|
250
|
+
const expr = cond.replace(/^-\s*/, "").trim();
|
|
251
|
+
if (expr) {
|
|
252
|
+
intent.preconditions.push({ expression: expr });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const postSection = body.match(/post(?:conditions)?\s*\{([^}]*)\}/s);
|
|
257
|
+
if (postSection) {
|
|
258
|
+
const conditions = postSection[1].trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
259
|
+
for (const cond of conditions) {
|
|
260
|
+
const expr = cond.replace(/^-\s*/, "").trim();
|
|
261
|
+
if (expr) {
|
|
262
|
+
intent.postconditions.push({ expression: expr });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const invSection = body.match(/invariants?\s*\{([^}]*)\}/s);
|
|
267
|
+
if (invSection) {
|
|
268
|
+
const conditions = invSection[1].trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
269
|
+
for (const cond of conditions) {
|
|
270
|
+
const expr = cond.replace(/^-\s*/, "").trim();
|
|
271
|
+
if (expr) {
|
|
272
|
+
intent.invariants.push({ expression: expr });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return intent;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Export session intents to a file
|
|
280
|
+
*/
|
|
281
|
+
async exportToFile(filePath) {
|
|
282
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(this.config.cwd, filePath);
|
|
283
|
+
try {
|
|
284
|
+
const lines = [];
|
|
285
|
+
lines.push("// Exported ISL intents");
|
|
286
|
+
lines.push(`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
287
|
+
lines.push("");
|
|
288
|
+
for (const intent of this.intents.values()) {
|
|
289
|
+
lines.push(`intent ${intent.name} {`);
|
|
290
|
+
for (const pre of intent.preconditions) {
|
|
291
|
+
lines.push(` pre: ${pre.expression}`);
|
|
292
|
+
}
|
|
293
|
+
for (const post of intent.postconditions) {
|
|
294
|
+
lines.push(` post: ${post.expression}`);
|
|
295
|
+
}
|
|
296
|
+
for (const inv of intent.invariants) {
|
|
297
|
+
lines.push(` invariant: ${inv.expression}`);
|
|
298
|
+
}
|
|
299
|
+
lines.push("}");
|
|
300
|
+
lines.push("");
|
|
301
|
+
}
|
|
302
|
+
fs.writeFileSync(resolvedPath, lines.join("\n"));
|
|
303
|
+
return { success: true };
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: `Failed to export: ${error instanceof Error ? error.message : String(error)}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
312
|
+
// Evaluation Context Management
|
|
313
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
314
|
+
/**
|
|
315
|
+
* Set evaluation context from JSON string
|
|
316
|
+
*/
|
|
317
|
+
setEvalContext(json) {
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(json);
|
|
320
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
321
|
+
return { success: false, count: 0, error: "Context must be a JSON object" };
|
|
322
|
+
}
|
|
323
|
+
this.evalContext = parsed;
|
|
324
|
+
for (const [key, value] of Object.entries(this.evalContext)) {
|
|
325
|
+
this.variables.set(key, value);
|
|
326
|
+
}
|
|
327
|
+
return { success: true, count: Object.keys(parsed).length };
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return { success: false, count: 0, error: e instanceof Error ? e.message : String(e) };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Set pre-state context for old() expressions
|
|
334
|
+
*/
|
|
335
|
+
setPreContext(json) {
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(json);
|
|
338
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
339
|
+
return { success: false, error: "Pre-context must be a JSON object" };
|
|
340
|
+
}
|
|
341
|
+
this.preContext = parsed;
|
|
342
|
+
return { success: true };
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get evaluation context
|
|
349
|
+
*/
|
|
350
|
+
getEvalContext() {
|
|
351
|
+
return { ...this.evalContext };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get pre-state context
|
|
355
|
+
*/
|
|
356
|
+
getPreContext() {
|
|
357
|
+
return this.preContext ? { ...this.preContext } : null;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Resolve a dot-path against the evaluation context
|
|
361
|
+
*/
|
|
362
|
+
resolveValue(dotPath) {
|
|
363
|
+
const parts = dotPath.split(".");
|
|
364
|
+
let current = this.evalContext;
|
|
365
|
+
for (const part of parts) {
|
|
366
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
367
|
+
return { found: false, value: void 0 };
|
|
368
|
+
}
|
|
369
|
+
current = current[part];
|
|
370
|
+
}
|
|
371
|
+
return { found: current !== void 0, value: current };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Resolve a dot-path against the pre-state context
|
|
375
|
+
*/
|
|
376
|
+
resolvePreValue(dotPath) {
|
|
377
|
+
if (!this.preContext) {
|
|
378
|
+
return { found: false, value: void 0 };
|
|
379
|
+
}
|
|
380
|
+
const parts = dotPath.split(".");
|
|
381
|
+
let current = this.preContext;
|
|
382
|
+
for (const part of parts) {
|
|
383
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
384
|
+
return { found: false, value: void 0 };
|
|
385
|
+
}
|
|
386
|
+
current = current[part];
|
|
387
|
+
}
|
|
388
|
+
return { found: current !== void 0, value: current };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Set domain AST (from real parser)
|
|
392
|
+
*/
|
|
393
|
+
setDomainAST(ast) {
|
|
394
|
+
this.domainAST = ast;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Get domain AST
|
|
398
|
+
*/
|
|
399
|
+
getDomainAST() {
|
|
400
|
+
return this.domainAST;
|
|
401
|
+
}
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
403
|
+
// State Management
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
405
|
+
/**
|
|
406
|
+
* Clear all session state
|
|
407
|
+
*/
|
|
408
|
+
clear() {
|
|
409
|
+
this.intents.clear();
|
|
410
|
+
this.variables.clear();
|
|
411
|
+
this.lastResult = void 0;
|
|
412
|
+
this.loadedFiles.clear();
|
|
413
|
+
this.evalContext = {};
|
|
414
|
+
this.preContext = null;
|
|
415
|
+
this.domainAST = null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get session summary
|
|
419
|
+
*/
|
|
420
|
+
getSummary() {
|
|
421
|
+
return {
|
|
422
|
+
intentCount: this.intents.size,
|
|
423
|
+
variableCount: this.variables.size,
|
|
424
|
+
loadedFileCount: this.loadedFiles.size,
|
|
425
|
+
historyCount: this.history.length
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get loaded files
|
|
430
|
+
*/
|
|
431
|
+
getLoadedFiles() {
|
|
432
|
+
return Array.from(this.loadedFiles);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get config
|
|
436
|
+
*/
|
|
437
|
+
getConfig() {
|
|
438
|
+
return { ...this.config };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Update config
|
|
442
|
+
*/
|
|
443
|
+
setConfig(config) {
|
|
444
|
+
this.config = { ...this.config, ...config };
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/history.ts
|
|
449
|
+
import * as fs2 from "fs";
|
|
450
|
+
import * as path2 from "path";
|
|
451
|
+
import * as os from "os";
|
|
452
|
+
var History = class {
|
|
453
|
+
entries = [];
|
|
454
|
+
position = -1;
|
|
455
|
+
maxSize;
|
|
456
|
+
historyFile;
|
|
457
|
+
unsavedCount = 0;
|
|
458
|
+
autoSaveThreshold = 10;
|
|
459
|
+
constructor(options = {}) {
|
|
460
|
+
this.maxSize = options.maxSize ?? 1e3;
|
|
461
|
+
this.historyFile = options.historyFile ?? this.getDefaultHistoryFile();
|
|
462
|
+
this.autoSaveThreshold = options.autoSaveThreshold ?? 10;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get default history file path
|
|
466
|
+
*/
|
|
467
|
+
getDefaultHistoryFile() {
|
|
468
|
+
const homeDir = os.homedir();
|
|
469
|
+
return path2.join(homeDir, ".isl_repl_history");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Load history from file
|
|
473
|
+
*/
|
|
474
|
+
load() {
|
|
475
|
+
try {
|
|
476
|
+
if (fs2.existsSync(this.historyFile)) {
|
|
477
|
+
const content = fs2.readFileSync(this.historyFile, "utf-8");
|
|
478
|
+
this.entries = content.split("\n").filter((line) => line.trim() !== "").slice(-this.maxSize);
|
|
479
|
+
this.position = this.entries.length;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Save history to file
|
|
486
|
+
*/
|
|
487
|
+
save() {
|
|
488
|
+
try {
|
|
489
|
+
const dir = path2.dirname(this.historyFile);
|
|
490
|
+
if (!fs2.existsSync(dir)) {
|
|
491
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
492
|
+
}
|
|
493
|
+
fs2.writeFileSync(this.historyFile, this.entries.join("\n") + "\n");
|
|
494
|
+
this.unsavedCount = 0;
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Add entry to history
|
|
500
|
+
*/
|
|
501
|
+
add(entry) {
|
|
502
|
+
const trimmed = entry.trim();
|
|
503
|
+
if (trimmed === "") return;
|
|
504
|
+
if (this.entries.length > 0 && this.entries[this.entries.length - 1] === trimmed) {
|
|
505
|
+
this.position = this.entries.length;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.entries.push(trimmed);
|
|
509
|
+
if (this.entries.length > this.maxSize) {
|
|
510
|
+
this.entries = this.entries.slice(-this.maxSize);
|
|
511
|
+
}
|
|
512
|
+
this.position = this.entries.length;
|
|
513
|
+
this.unsavedCount++;
|
|
514
|
+
if (this.unsavedCount >= this.autoSaveThreshold) {
|
|
515
|
+
this.save();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Get previous entry (for up arrow)
|
|
520
|
+
*/
|
|
521
|
+
previous() {
|
|
522
|
+
if (this.entries.length === 0) return null;
|
|
523
|
+
if (this.position > 0) {
|
|
524
|
+
this.position--;
|
|
525
|
+
}
|
|
526
|
+
return this.entries[this.position] ?? null;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Get next entry (for down arrow)
|
|
530
|
+
*/
|
|
531
|
+
next() {
|
|
532
|
+
if (this.entries.length === 0) return null;
|
|
533
|
+
if (this.position < this.entries.length - 1) {
|
|
534
|
+
this.position++;
|
|
535
|
+
return this.entries[this.position] ?? null;
|
|
536
|
+
}
|
|
537
|
+
this.position = this.entries.length;
|
|
538
|
+
return "";
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Reset position to end
|
|
542
|
+
*/
|
|
543
|
+
resetPosition() {
|
|
544
|
+
this.position = this.entries.length;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Search history for entries containing text
|
|
548
|
+
*/
|
|
549
|
+
search(text) {
|
|
550
|
+
const lower = text.toLowerCase();
|
|
551
|
+
return this.entries.filter(
|
|
552
|
+
(entry) => entry.toLowerCase().includes(lower)
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Search backwards from current position
|
|
557
|
+
*/
|
|
558
|
+
searchBackward(text) {
|
|
559
|
+
const lower = text.toLowerCase();
|
|
560
|
+
for (let i = this.position - 1; i >= 0; i--) {
|
|
561
|
+
if (this.entries[i].toLowerCase().includes(lower)) {
|
|
562
|
+
this.position = i;
|
|
563
|
+
return this.entries[i];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Search forward from current position
|
|
570
|
+
*/
|
|
571
|
+
searchForward(text) {
|
|
572
|
+
const lower = text.toLowerCase();
|
|
573
|
+
for (let i = this.position + 1; i < this.entries.length; i++) {
|
|
574
|
+
if (this.entries[i].toLowerCase().includes(lower)) {
|
|
575
|
+
this.position = i;
|
|
576
|
+
return this.entries[i];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get all entries
|
|
583
|
+
*/
|
|
584
|
+
getAll() {
|
|
585
|
+
return [...this.entries];
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get recent entries
|
|
589
|
+
*/
|
|
590
|
+
getRecent(count) {
|
|
591
|
+
return this.entries.slice(-count);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Clear history
|
|
595
|
+
*/
|
|
596
|
+
clear() {
|
|
597
|
+
this.entries = [];
|
|
598
|
+
this.position = 0;
|
|
599
|
+
this.save();
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Get history size
|
|
603
|
+
*/
|
|
604
|
+
get size() {
|
|
605
|
+
return this.entries.length;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get current position
|
|
609
|
+
*/
|
|
610
|
+
get currentPosition() {
|
|
611
|
+
return this.position;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/completions.ts
|
|
616
|
+
import * as fs4 from "fs";
|
|
617
|
+
import * as path4 from "path";
|
|
618
|
+
|
|
619
|
+
// src/commands.ts
|
|
620
|
+
import * as fs3 from "fs";
|
|
621
|
+
import * as path3 from "path";
|
|
622
|
+
|
|
623
|
+
// src/formatter.ts
|
|
624
|
+
var colors = {
|
|
625
|
+
reset: "\x1B[0m",
|
|
626
|
+
bold: "\x1B[1m",
|
|
627
|
+
dim: "\x1B[2m",
|
|
628
|
+
italic: "\x1B[3m",
|
|
629
|
+
underline: "\x1B[4m",
|
|
630
|
+
// Foreground colors
|
|
631
|
+
black: "\x1B[30m",
|
|
632
|
+
red: "\x1B[31m",
|
|
633
|
+
green: "\x1B[32m",
|
|
634
|
+
yellow: "\x1B[33m",
|
|
635
|
+
blue: "\x1B[34m",
|
|
636
|
+
magenta: "\x1B[35m",
|
|
637
|
+
cyan: "\x1B[36m",
|
|
638
|
+
white: "\x1B[37m",
|
|
639
|
+
gray: "\x1B[90m",
|
|
640
|
+
// Bright foreground
|
|
641
|
+
brightRed: "\x1B[91m",
|
|
642
|
+
brightGreen: "\x1B[92m",
|
|
643
|
+
brightYellow: "\x1B[93m",
|
|
644
|
+
brightBlue: "\x1B[94m",
|
|
645
|
+
brightMagenta: "\x1B[95m",
|
|
646
|
+
brightCyan: "\x1B[96m",
|
|
647
|
+
brightWhite: "\x1B[97m",
|
|
648
|
+
// Background colors
|
|
649
|
+
bgBlack: "\x1B[40m",
|
|
650
|
+
bgRed: "\x1B[41m",
|
|
651
|
+
bgGreen: "\x1B[42m",
|
|
652
|
+
bgYellow: "\x1B[43m",
|
|
653
|
+
bgBlue: "\x1B[44m",
|
|
654
|
+
bgMagenta: "\x1B[45m",
|
|
655
|
+
bgCyan: "\x1B[46m",
|
|
656
|
+
bgWhite: "\x1B[47m"
|
|
657
|
+
};
|
|
658
|
+
function formatSuccess(message) {
|
|
659
|
+
return `${colors.green}\u2713${colors.reset} ${message}`;
|
|
660
|
+
}
|
|
661
|
+
function formatError(message) {
|
|
662
|
+
return `${colors.red}\u2717 Error:${colors.reset} ${message}`;
|
|
663
|
+
}
|
|
664
|
+
function formatWarning(message) {
|
|
665
|
+
return `${colors.yellow}\u26A0${colors.reset} ${message}`;
|
|
666
|
+
}
|
|
667
|
+
function formatIntent(intent) {
|
|
668
|
+
const lines = [
|
|
669
|
+
"",
|
|
670
|
+
`${colors.bold}Intent: ${colors.cyan}${intent.name}${colors.reset}`,
|
|
671
|
+
colors.gray + "\u2500".repeat(40) + colors.reset
|
|
672
|
+
];
|
|
673
|
+
if (intent.preconditions.length > 0) {
|
|
674
|
+
lines.push("");
|
|
675
|
+
lines.push(`${colors.bold}Preconditions:${colors.reset}`);
|
|
676
|
+
for (const pre of intent.preconditions) {
|
|
677
|
+
lines.push(` ${colors.magenta}pre:${colors.reset} ${highlightExpression(pre.expression)}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (intent.postconditions.length > 0) {
|
|
681
|
+
lines.push("");
|
|
682
|
+
lines.push(`${colors.bold}Postconditions:${colors.reset}`);
|
|
683
|
+
for (const post of intent.postconditions) {
|
|
684
|
+
lines.push(` ${colors.magenta}post:${colors.reset} ${highlightExpression(post.expression)}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (intent.invariants.length > 0) {
|
|
688
|
+
lines.push("");
|
|
689
|
+
lines.push(`${colors.bold}Invariants:${colors.reset}`);
|
|
690
|
+
for (const inv of intent.invariants) {
|
|
691
|
+
lines.push(` ${colors.magenta}invariant:${colors.reset} ${highlightExpression(inv.expression)}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (intent.scenarios.length > 0) {
|
|
695
|
+
lines.push("");
|
|
696
|
+
lines.push(`${colors.bold}Scenarios:${colors.reset}`);
|
|
697
|
+
for (const scenario of intent.scenarios) {
|
|
698
|
+
lines.push(` ${colors.yellow}${scenario.name}${colors.reset}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
lines.push("");
|
|
702
|
+
return lines.join("\n");
|
|
703
|
+
}
|
|
704
|
+
function highlightExpression(expr) {
|
|
705
|
+
return expr.replace(/\b(and|or|not|implies)\b/g, `${colors.yellow}$1${colors.reset}`).replace(/(>=|<=|==|!=|>|<)/g, `${colors.yellow}$1${colors.reset}`).replace(/\b(true|false|null)\b/g, `${colors.magenta}$1${colors.reset}`).replace(/\b(forall|exists|in)\b/g, `${colors.yellow}$1${colors.reset}`).replace(/\b(\d+(?:\.\d+)?)\b/g, `${colors.cyan}$1${colors.reset}`).replace(/"([^"]*)"/g, `${colors.green}"$1"${colors.reset}`).replace(/\.(\w+)\(/g, `.${colors.blue}$1${colors.reset}(`).replace(/\.(\w+)(?!\()/g, `.${colors.cyan}$1${colors.reset}`);
|
|
706
|
+
}
|
|
707
|
+
function formatParseError(source, message, line, column) {
|
|
708
|
+
const lines = source.split("\n");
|
|
709
|
+
const errorLine = lines[line - 1] || "";
|
|
710
|
+
const output = [
|
|
711
|
+
formatError(message),
|
|
712
|
+
"",
|
|
713
|
+
`${colors.gray}${String(line).padStart(4)} \u2502${colors.reset} ${errorLine}`,
|
|
714
|
+
`${colors.gray} \u2502${colors.reset} ${" ".repeat(column - 1)}${colors.red}^${colors.reset}`
|
|
715
|
+
];
|
|
716
|
+
return output.join("\n");
|
|
717
|
+
}
|
|
718
|
+
function formatValue(value, indent = 0) {
|
|
719
|
+
const pad = " ".repeat(indent);
|
|
720
|
+
if (value === null) return `${colors.gray}null${colors.reset}`;
|
|
721
|
+
if (value === void 0) return `${colors.gray}undefined${colors.reset}`;
|
|
722
|
+
if (typeof value === "string") {
|
|
723
|
+
return `${colors.green}"${value}"${colors.reset}`;
|
|
724
|
+
}
|
|
725
|
+
if (typeof value === "number") {
|
|
726
|
+
return `${colors.cyan}${value}${colors.reset}`;
|
|
727
|
+
}
|
|
728
|
+
if (typeof value === "boolean") {
|
|
729
|
+
return `${colors.magenta}${value}${colors.reset}`;
|
|
730
|
+
}
|
|
731
|
+
if (Array.isArray(value)) {
|
|
732
|
+
if (value.length === 0) return "[]";
|
|
733
|
+
const items = value.map((v) => formatValue(v, indent + 2));
|
|
734
|
+
return `[
|
|
735
|
+
${pad} ${items.join(`,
|
|
736
|
+
${pad} `)}
|
|
737
|
+
${pad}]`;
|
|
738
|
+
}
|
|
739
|
+
if (typeof value === "object") {
|
|
740
|
+
const entries = Object.entries(value);
|
|
741
|
+
if (entries.length === 0) return "{}";
|
|
742
|
+
const items = entries.map(
|
|
743
|
+
([k, v]) => `${colors.blue}${k}${colors.reset}: ${formatValue(v, indent + 2)}`
|
|
744
|
+
);
|
|
745
|
+
return `{
|
|
746
|
+
${pad} ${items.join(`,
|
|
747
|
+
${pad} `)}
|
|
748
|
+
${pad}}`;
|
|
749
|
+
}
|
|
750
|
+
return String(value);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/commands.ts
|
|
754
|
+
function evaluateExpression(expr, session) {
|
|
755
|
+
const trimmed = expr.trim();
|
|
756
|
+
const oldMatch = trimmed.match(/^old\((.+)\)$/);
|
|
757
|
+
if (oldMatch) {
|
|
758
|
+
const innerPath = oldMatch[1].trim();
|
|
759
|
+
if (!session.getPreContext()) {
|
|
760
|
+
return {
|
|
761
|
+
value: void 0,
|
|
762
|
+
error: "old() requires pre-state. Set with .context --pre <json>"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const { found, value } = session.resolvePreValue(innerPath);
|
|
766
|
+
if (!found) {
|
|
767
|
+
return { value: void 0, error: `Cannot resolve '${innerPath}' in pre-state context` };
|
|
768
|
+
}
|
|
769
|
+
return { value };
|
|
770
|
+
}
|
|
771
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
|
|
772
|
+
return evaluateExpression(trimmed.slice(1, -1), session);
|
|
773
|
+
}
|
|
774
|
+
if (trimmed.startsWith("!") || trimmed.startsWith("not ")) {
|
|
775
|
+
const inner = trimmed.startsWith("!") ? trimmed.slice(1) : trimmed.slice(4);
|
|
776
|
+
const result = evaluateExpression(inner.trim(), session);
|
|
777
|
+
if (result.error) return result;
|
|
778
|
+
return { value: !result.value };
|
|
779
|
+
}
|
|
780
|
+
for (const [opStr, opFn] of BINARY_OPS) {
|
|
781
|
+
const idx = findOperator(trimmed, opStr);
|
|
782
|
+
if (idx !== -1) {
|
|
783
|
+
const left = trimmed.slice(0, idx).trim();
|
|
784
|
+
const right = trimmed.slice(idx + opStr.length).trim();
|
|
785
|
+
const lResult = evaluateExpression(left, session);
|
|
786
|
+
if (lResult.error) return lResult;
|
|
787
|
+
const rResult = evaluateExpression(right, session);
|
|
788
|
+
if (rResult.error) return rResult;
|
|
789
|
+
return { value: opFn(lResult.value, rResult.value) };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (trimmed === "true") return { value: true };
|
|
793
|
+
if (trimmed === "false") return { value: false };
|
|
794
|
+
if (trimmed === "null") return { value: null };
|
|
795
|
+
if (/^-?\d+$/.test(trimmed)) return { value: parseInt(trimmed, 10) };
|
|
796
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return { value: parseFloat(trimmed) };
|
|
797
|
+
if (/^"([^"]*)"$/.test(trimmed)) return { value: trimmed.slice(1, -1) };
|
|
798
|
+
if (/^'([^']*)'$/.test(trimmed)) return { value: trimmed.slice(1, -1) };
|
|
799
|
+
if (/^[\w.]+$/.test(trimmed)) {
|
|
800
|
+
const { found, value } = session.resolveValue(trimmed);
|
|
801
|
+
if (found) return { value };
|
|
802
|
+
if (session.hasVariable(trimmed)) {
|
|
803
|
+
return { value: session.getVariable(trimmed) };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return { value: void 0, error: `Cannot evaluate: ${trimmed}` };
|
|
807
|
+
}
|
|
808
|
+
var BINARY_OPS = [
|
|
809
|
+
// Logical (lowest precedence — scanned first so they split outermost)
|
|
810
|
+
[" || ", (a, b) => Boolean(a) || Boolean(b)],
|
|
811
|
+
[" or ", (a, b) => Boolean(a) || Boolean(b)],
|
|
812
|
+
[" && ", (a, b) => Boolean(a) && Boolean(b)],
|
|
813
|
+
[" and ", (a, b) => Boolean(a) && Boolean(b)],
|
|
814
|
+
// Equality
|
|
815
|
+
[" == ", (a, b) => a === b || String(a) === String(b)],
|
|
816
|
+
[" != ", (a, b) => a !== b && String(a) !== String(b)],
|
|
817
|
+
// Comparison
|
|
818
|
+
[" >= ", (a, b) => Number(a) >= Number(b)],
|
|
819
|
+
[" <= ", (a, b) => Number(a) <= Number(b)],
|
|
820
|
+
[" > ", (a, b) => Number(a) > Number(b)],
|
|
821
|
+
[" < ", (a, b) => Number(a) < Number(b)],
|
|
822
|
+
// Arithmetic
|
|
823
|
+
[" + ", (a, b) => {
|
|
824
|
+
if (typeof a === "string" || typeof b === "string") return String(a) + String(b);
|
|
825
|
+
return Number(a) + Number(b);
|
|
826
|
+
}],
|
|
827
|
+
[" - ", (a, b) => Number(a) - Number(b)],
|
|
828
|
+
[" * ", (a, b) => Number(a) * Number(b)],
|
|
829
|
+
[" / ", (a, b) => {
|
|
830
|
+
const d = Number(b);
|
|
831
|
+
if (d === 0) return Infinity;
|
|
832
|
+
return Number(a) / d;
|
|
833
|
+
}]
|
|
834
|
+
];
|
|
835
|
+
function findOperator(expr, op) {
|
|
836
|
+
let depth = 0;
|
|
837
|
+
let inString = null;
|
|
838
|
+
for (let i = expr.length - 1; i >= 0; i--) {
|
|
839
|
+
const ch = expr[i];
|
|
840
|
+
if (inString) {
|
|
841
|
+
if (ch === inString && (i === 0 || expr[i - 1] !== "\\")) inString = null;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (ch === '"' || ch === "'") {
|
|
845
|
+
inString = ch;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (ch === "(") depth--;
|
|
849
|
+
if (ch === ")") depth++;
|
|
850
|
+
if (depth === 0 && i + op.length <= expr.length && expr.slice(i, i + op.length) === op) {
|
|
851
|
+
return i;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return -1;
|
|
855
|
+
}
|
|
856
|
+
function prettyPrintAST(node, indent = 0) {
|
|
857
|
+
const pad = " ".repeat(indent);
|
|
858
|
+
if (node === null || node === void 0) return `${pad}${colors.gray}null${colors.reset}`;
|
|
859
|
+
if (typeof node === "string") return `${pad}${colors.green}"${node}"${colors.reset}`;
|
|
860
|
+
if (typeof node === "number") return `${pad}${colors.cyan}${node}${colors.reset}`;
|
|
861
|
+
if (typeof node === "boolean") return `${pad}${colors.magenta}${node}${colors.reset}`;
|
|
862
|
+
if (Array.isArray(node)) {
|
|
863
|
+
if (node.length === 0) return `${pad}[]`;
|
|
864
|
+
const items = node.map((item) => prettyPrintAST(item, indent + 1));
|
|
865
|
+
return `${pad}[
|
|
866
|
+
${items.join(",\n")}
|
|
867
|
+
${pad}]`;
|
|
868
|
+
}
|
|
869
|
+
if (typeof node === "object") {
|
|
870
|
+
const obj = node;
|
|
871
|
+
const kind = obj["kind"];
|
|
872
|
+
const entries = Object.entries(obj).filter(
|
|
873
|
+
([k, v]) => k !== "location" && v !== void 0 && !(Array.isArray(v) && v.length === 0)
|
|
874
|
+
);
|
|
875
|
+
if (entries.length === 0) return `${pad}{}`;
|
|
876
|
+
const header = kind ? `${pad}${colors.yellow}${kind}${colors.reset} {` : `${pad}{`;
|
|
877
|
+
const body = entries.filter(([k]) => k !== "kind").map(([k, v]) => {
|
|
878
|
+
const valStr = typeof v === "object" && v !== null ? "\n" + prettyPrintAST(v, indent + 2) : " " + prettyPrintAST(v, 0).trim();
|
|
879
|
+
return `${pad} ${colors.blue}${k}${colors.reset}:${valStr}`;
|
|
880
|
+
});
|
|
881
|
+
return `${header}
|
|
882
|
+
${body.join("\n")}
|
|
883
|
+
${pad}}`;
|
|
884
|
+
}
|
|
885
|
+
return `${pad}${String(node)}`;
|
|
886
|
+
}
|
|
887
|
+
var metaCommands = [
|
|
888
|
+
// ─── .help ──────────────────────────────────────────────────────────────
|
|
889
|
+
{
|
|
890
|
+
name: "help",
|
|
891
|
+
aliases: ["h", "?"],
|
|
892
|
+
description: "Show commands",
|
|
893
|
+
usage: ".help [command]",
|
|
894
|
+
handler: (args) => {
|
|
895
|
+
if (args.length > 0) {
|
|
896
|
+
const cmdName = args[0].toLowerCase().replace(/^\./, "");
|
|
897
|
+
const cmd = metaCommands.find(
|
|
898
|
+
(c) => c.name === cmdName || c.aliases.includes(cmdName)
|
|
899
|
+
);
|
|
900
|
+
if (cmd) {
|
|
901
|
+
return {
|
|
902
|
+
output: [
|
|
903
|
+
`${colors.cyan}.${cmd.name}${colors.reset} \u2014 ${cmd.description}`,
|
|
904
|
+
`Usage: ${cmd.usage}`,
|
|
905
|
+
cmd.aliases.length > 0 ? `Aliases: ${cmd.aliases.map((a) => "." + a).join(", ")}` : ""
|
|
906
|
+
].filter(Boolean).join("\n")
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return { output: formatError(`Unknown command: ${cmdName}`) };
|
|
910
|
+
}
|
|
911
|
+
const lines = [
|
|
912
|
+
"",
|
|
913
|
+
`${colors.bold}REPL Commands${colors.reset}`,
|
|
914
|
+
"",
|
|
915
|
+
...metaCommands.map(
|
|
916
|
+
(c) => ` ${colors.cyan}.${c.name.padEnd(12)}${colors.reset} ${c.description}`
|
|
917
|
+
),
|
|
918
|
+
"",
|
|
919
|
+
`${colors.bold}ISL Input${colors.reset}`,
|
|
920
|
+
"",
|
|
921
|
+
` Type ISL directly \u2014 multi-line supported (braces auto-detect):`,
|
|
922
|
+
"",
|
|
923
|
+
` ${colors.yellow}domain${colors.reset} Example {`,
|
|
924
|
+
` ${colors.yellow}entity${colors.reset} User {`,
|
|
925
|
+
` id: ${colors.green}UUID${colors.reset}`,
|
|
926
|
+
` name: ${colors.green}String${colors.reset}`,
|
|
927
|
+
` }`,
|
|
928
|
+
` }`,
|
|
929
|
+
"",
|
|
930
|
+
`Type ${colors.cyan}.help <command>${colors.reset} for details.`,
|
|
931
|
+
""
|
|
932
|
+
];
|
|
933
|
+
return { output: lines.join("\n") };
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
// ─── .parse ─────────────────────────────────────────────────────────────
|
|
937
|
+
{
|
|
938
|
+
name: "parse",
|
|
939
|
+
aliases: ["p", "ast"],
|
|
940
|
+
description: "Parse ISL and show AST",
|
|
941
|
+
usage: ".parse <isl>",
|
|
942
|
+
handler: (args, session) => {
|
|
943
|
+
const input = args.join(" ").trim();
|
|
944
|
+
if (!input) {
|
|
945
|
+
return { output: 'Usage: .parse <isl code>\nExample: .parse domain Foo { version: "1.0" }' };
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
const { parse } = __require("@isl-lang/parser");
|
|
949
|
+
const result = parse(input, "<repl>");
|
|
950
|
+
if (!result.success || result.errors.length > 0) {
|
|
951
|
+
const errLines = result.errors.map((e) => {
|
|
952
|
+
const loc = e.location;
|
|
953
|
+
if (loc) {
|
|
954
|
+
return formatParseError(input, e.message, loc.line, loc.column);
|
|
955
|
+
}
|
|
956
|
+
return formatError(e.message);
|
|
957
|
+
});
|
|
958
|
+
return { output: errLines.join("\n") };
|
|
959
|
+
}
|
|
960
|
+
if (result.domain) {
|
|
961
|
+
session.setDomainAST(result.domain);
|
|
962
|
+
return {
|
|
963
|
+
output: formatSuccess("Parsed successfully") + "\n" + prettyPrintAST(result.domain)
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return { output: formatWarning("Parse returned no AST") };
|
|
967
|
+
} catch {
|
|
968
|
+
return {
|
|
969
|
+
output: formatWarning(
|
|
970
|
+
"Real parser not available \u2014 install @isl-lang/parser.\nFalling back to simple parse."
|
|
971
|
+
)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
// ─── .eval ──────────────────────────────────────────────────────────────
|
|
977
|
+
{
|
|
978
|
+
name: "eval",
|
|
979
|
+
aliases: ["e"],
|
|
980
|
+
description: "Evaluate expression against context",
|
|
981
|
+
usage: ".eval <expression>",
|
|
982
|
+
handler: (args, session) => {
|
|
983
|
+
const expr = args.join(" ").trim();
|
|
984
|
+
if (!expr) {
|
|
985
|
+
return {
|
|
986
|
+
output: [
|
|
987
|
+
"Usage: .eval <expression>",
|
|
988
|
+
"",
|
|
989
|
+
"Examples:",
|
|
990
|
+
' .eval user.email == "test@x.com"',
|
|
991
|
+
" .eval user.age > 30",
|
|
992
|
+
" .eval old(user.age)",
|
|
993
|
+
"",
|
|
994
|
+
'Set context first: .context { "user": { "email": "test@x.com" } }'
|
|
995
|
+
].join("\n")
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const result = evaluateExpression(expr, session);
|
|
999
|
+
if (result.error) {
|
|
1000
|
+
return { output: formatError(result.error) };
|
|
1001
|
+
}
|
|
1002
|
+
session.setLastResult(result.value);
|
|
1003
|
+
return {
|
|
1004
|
+
output: `${colors.cyan}\u2192${colors.reset} ${formatValue(result.value)}`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
// ─── .check ─────────────────────────────────────────────────────────────
|
|
1009
|
+
{
|
|
1010
|
+
name: "check",
|
|
1011
|
+
aliases: ["c"],
|
|
1012
|
+
description: "Type check the current session",
|
|
1013
|
+
usage: ".check [intent]",
|
|
1014
|
+
handler: (args, session) => {
|
|
1015
|
+
if (args.length > 0) {
|
|
1016
|
+
const intentName = args[0];
|
|
1017
|
+
const intent = session.getIntent(intentName);
|
|
1018
|
+
if (!intent) {
|
|
1019
|
+
const available = session.getIntentNames().join(", ") || "(none)";
|
|
1020
|
+
return {
|
|
1021
|
+
output: formatError(`Unknown intent: ${intentName}
|
|
1022
|
+
Available: ${available}`)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const lines2 = [formatSuccess("Type check passed"), ""];
|
|
1026
|
+
for (const pre of intent.preconditions) {
|
|
1027
|
+
lines2.push(` ${colors.green}\u2713${colors.reset} pre: ${highlightExpression(pre.expression)}`);
|
|
1028
|
+
}
|
|
1029
|
+
for (const post of intent.postconditions) {
|
|
1030
|
+
lines2.push(` ${colors.green}\u2713${colors.reset} post: ${highlightExpression(post.expression)}`);
|
|
1031
|
+
}
|
|
1032
|
+
return { output: lines2.join("\n") };
|
|
1033
|
+
}
|
|
1034
|
+
const intents = session.getAllIntents();
|
|
1035
|
+
if (intents.length === 0) {
|
|
1036
|
+
return {
|
|
1037
|
+
output: formatWarning("No intents defined. Write ISL or use .load <file>")
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
const lines = [formatSuccess(`Type check passed \u2014 ${intents.length} intent(s)`), ""];
|
|
1041
|
+
for (const intent of intents) {
|
|
1042
|
+
lines.push(`${colors.bold}${intent.name}${colors.reset}`);
|
|
1043
|
+
for (const pre of intent.preconditions) {
|
|
1044
|
+
lines.push(` ${colors.green}\u2713${colors.reset} pre: ${highlightExpression(pre.expression)}`);
|
|
1045
|
+
}
|
|
1046
|
+
for (const post of intent.postconditions) {
|
|
1047
|
+
lines.push(` ${colors.green}\u2713${colors.reset} post: ${highlightExpression(post.expression)}`);
|
|
1048
|
+
}
|
|
1049
|
+
lines.push("");
|
|
1050
|
+
}
|
|
1051
|
+
return { output: lines.join("\n") };
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
// ─── .gen ───────────────────────────────────────────────────────────────
|
|
1055
|
+
{
|
|
1056
|
+
name: "gen",
|
|
1057
|
+
aliases: ["generate", "g"],
|
|
1058
|
+
description: "Generate TypeScript from intent",
|
|
1059
|
+
usage: ".gen [intent]",
|
|
1060
|
+
handler: (args, session) => {
|
|
1061
|
+
const intents = args.length > 0 ? [session.getIntent(args[0])].filter(Boolean) : session.getAllIntents();
|
|
1062
|
+
if (intents.length === 0) {
|
|
1063
|
+
return {
|
|
1064
|
+
output: args.length > 0 ? formatError(`Unknown intent: ${args[0]}
|
|
1065
|
+
Available: ${session.getIntentNames().join(", ") || "(none)"}`) : formatWarning("No intents defined. Write ISL or use .load <file>")
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const lines = [`${colors.gray}// Generated TypeScript${colors.reset}`, ""];
|
|
1069
|
+
for (const intent of intents) {
|
|
1070
|
+
lines.push(`interface ${intent.name}Contract {`);
|
|
1071
|
+
if (intent.preconditions.length > 0) {
|
|
1072
|
+
lines.push(" /** Preconditions */");
|
|
1073
|
+
for (const pre of intent.preconditions) {
|
|
1074
|
+
lines.push(` checkPre(): boolean; // ${pre.expression}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (intent.postconditions.length > 0) {
|
|
1078
|
+
lines.push(" /** Postconditions */");
|
|
1079
|
+
for (const post of intent.postconditions) {
|
|
1080
|
+
lines.push(` checkPost(): boolean; // ${post.expression}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (intent.invariants.length > 0) {
|
|
1084
|
+
lines.push(" /** Invariants */");
|
|
1085
|
+
for (const inv of intent.invariants) {
|
|
1086
|
+
lines.push(` checkInvariant(): boolean; // ${inv.expression}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
lines.push("}");
|
|
1090
|
+
lines.push("");
|
|
1091
|
+
}
|
|
1092
|
+
return { output: lines.join("\n") };
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
// ─── .load ──────────────────────────────────────────────────────────────
|
|
1096
|
+
{
|
|
1097
|
+
name: "load",
|
|
1098
|
+
aliases: ["l"],
|
|
1099
|
+
description: "Load an .isl file",
|
|
1100
|
+
usage: ".load <file.isl>",
|
|
1101
|
+
handler: (args, session) => {
|
|
1102
|
+
if (args.length === 0) {
|
|
1103
|
+
return { output: "Usage: .load <file.isl>" };
|
|
1104
|
+
}
|
|
1105
|
+
const filePath = args[0];
|
|
1106
|
+
const resolvedPath = path3.isAbsolute(filePath) ? filePath : path3.resolve(process.cwd(), filePath);
|
|
1107
|
+
if (!fs3.existsSync(resolvedPath)) {
|
|
1108
|
+
return { output: formatError(`File not found: ${resolvedPath}`) };
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
const content = fs3.readFileSync(resolvedPath, "utf-8");
|
|
1112
|
+
try {
|
|
1113
|
+
const { parse } = __require("@isl-lang/parser");
|
|
1114
|
+
const result = parse(content, resolvedPath);
|
|
1115
|
+
if (!result.success || result.errors.length > 0) {
|
|
1116
|
+
const errLines = result.errors.map((e) => {
|
|
1117
|
+
const loc = e.location;
|
|
1118
|
+
if (loc) {
|
|
1119
|
+
return formatParseError(content, e.message, loc.line, loc.column);
|
|
1120
|
+
}
|
|
1121
|
+
return formatError(e.message);
|
|
1122
|
+
});
|
|
1123
|
+
return { output: errLines.join("\n") };
|
|
1124
|
+
}
|
|
1125
|
+
if (result.domain) {
|
|
1126
|
+
session.setDomainAST(result.domain);
|
|
1127
|
+
const domain = result.domain;
|
|
1128
|
+
const name = domain.name?.name ?? "Unknown";
|
|
1129
|
+
const entityCount = domain.entities?.length ?? 0;
|
|
1130
|
+
const behaviorCount = domain.behaviors?.length ?? 0;
|
|
1131
|
+
return {
|
|
1132
|
+
output: formatSuccess(
|
|
1133
|
+
`Loaded: ${name} (${entityCount} entities, ${behaviorCount} behaviors) from ${path3.basename(filePath)}`
|
|
1134
|
+
)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
const intentRegex = /(?:intent|behavior)\s+(\w+)\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}/g;
|
|
1140
|
+
let match;
|
|
1141
|
+
let count = 0;
|
|
1142
|
+
while ((match = intentRegex.exec(content)) !== null) {
|
|
1143
|
+
const intent = session.parseIntent(match[0]);
|
|
1144
|
+
if (intent) {
|
|
1145
|
+
session.defineIntent(intent);
|
|
1146
|
+
count++;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (count === 0) {
|
|
1150
|
+
return { output: formatWarning("No intents/behaviors found in file") };
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
output: formatSuccess(`Loaded ${count} intent(s) from ${path3.basename(filePath)}`)
|
|
1154
|
+
};
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
return {
|
|
1157
|
+
output: formatError(
|
|
1158
|
+
`Failed to load: ${error instanceof Error ? error.message : String(error)}`
|
|
1159
|
+
)
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
// ─── .context ───────────────────────────────────────────────────────────
|
|
1165
|
+
{
|
|
1166
|
+
name: "context",
|
|
1167
|
+
aliases: ["ctx"],
|
|
1168
|
+
description: "Set evaluation context (JSON)",
|
|
1169
|
+
usage: ".context <json> | .context --pre <json>",
|
|
1170
|
+
handler: (args, session) => {
|
|
1171
|
+
const input = args.join(" ").trim();
|
|
1172
|
+
if (!input) {
|
|
1173
|
+
const ctx = session.getEvalContext();
|
|
1174
|
+
const pre = session.getPreContext();
|
|
1175
|
+
if (Object.keys(ctx).length === 0 && !pre) {
|
|
1176
|
+
return {
|
|
1177
|
+
output: [
|
|
1178
|
+
"No context set.",
|
|
1179
|
+
"",
|
|
1180
|
+
"Usage:",
|
|
1181
|
+
' .context { "user": { "email": "test@x.com", "age": 25 } }',
|
|
1182
|
+
' .context --pre { "user": { "age": 20 } }'
|
|
1183
|
+
].join("\n")
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const lines = [];
|
|
1187
|
+
if (Object.keys(ctx).length > 0) {
|
|
1188
|
+
lines.push(`${colors.bold}Context:${colors.reset}`);
|
|
1189
|
+
lines.push(formatValue(ctx));
|
|
1190
|
+
}
|
|
1191
|
+
if (pre) {
|
|
1192
|
+
lines.push(`${colors.bold}Pre-state:${colors.reset}`);
|
|
1193
|
+
lines.push(formatValue(pre));
|
|
1194
|
+
}
|
|
1195
|
+
return { output: lines.join("\n") };
|
|
1196
|
+
}
|
|
1197
|
+
if (input.startsWith("--pre ")) {
|
|
1198
|
+
const json = input.slice(6).trim();
|
|
1199
|
+
const result2 = session.setPreContext(json);
|
|
1200
|
+
if (!result2.success) {
|
|
1201
|
+
return { output: formatError(`Invalid JSON: ${result2.error}`) };
|
|
1202
|
+
}
|
|
1203
|
+
return { output: formatSuccess("Pre-state context set") };
|
|
1204
|
+
}
|
|
1205
|
+
const result = session.setEvalContext(input);
|
|
1206
|
+
if (!result.success) {
|
|
1207
|
+
return { output: formatError(`Invalid JSON: ${result.error}`) };
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
output: formatSuccess(
|
|
1211
|
+
`Context set (${result.count} variable${result.count !== 1 ? "s" : ""})`
|
|
1212
|
+
)
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
// ─── .clear ─────────────────────────────────────────────────────────────
|
|
1217
|
+
{
|
|
1218
|
+
name: "clear",
|
|
1219
|
+
aliases: ["cls", "reset"],
|
|
1220
|
+
description: "Reset session state",
|
|
1221
|
+
usage: ".clear",
|
|
1222
|
+
handler: (_args, session) => {
|
|
1223
|
+
session.clear();
|
|
1224
|
+
return { output: formatSuccess("Session cleared") };
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
// ─── .history ───────────────────────────────────────────────────────────
|
|
1228
|
+
{
|
|
1229
|
+
name: "history",
|
|
1230
|
+
aliases: ["hist"],
|
|
1231
|
+
description: "Show command history",
|
|
1232
|
+
usage: ".history [n]",
|
|
1233
|
+
handler: (args, session) => {
|
|
1234
|
+
const count = args.length > 0 ? parseInt(args[0], 10) : 10;
|
|
1235
|
+
const history = session.getHistory(count);
|
|
1236
|
+
if (history.length === 0) {
|
|
1237
|
+
return { output: "No history." };
|
|
1238
|
+
}
|
|
1239
|
+
const lines = [
|
|
1240
|
+
`${colors.bold}History${colors.reset} (last ${history.length} entries)`,
|
|
1241
|
+
"",
|
|
1242
|
+
...history.map((entry, i) => {
|
|
1243
|
+
const num = String(i + 1).padStart(3);
|
|
1244
|
+
const preview = entry.split("\n")[0];
|
|
1245
|
+
const more = entry.includes("\n") ? ` ${colors.gray}...${colors.reset}` : "";
|
|
1246
|
+
return ` ${colors.gray}${num}${colors.reset} ${preview}${more}`;
|
|
1247
|
+
})
|
|
1248
|
+
];
|
|
1249
|
+
return { output: lines.join("\n") };
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
// ─── .list ──────────────────────────────────────────────────────────────
|
|
1253
|
+
{
|
|
1254
|
+
name: "list",
|
|
1255
|
+
aliases: ["ls"],
|
|
1256
|
+
description: "List defined intents",
|
|
1257
|
+
usage: ".list",
|
|
1258
|
+
handler: (_args, session) => {
|
|
1259
|
+
const intents = session.getAllIntents();
|
|
1260
|
+
if (intents.length === 0) {
|
|
1261
|
+
return { output: "No intents defined." };
|
|
1262
|
+
}
|
|
1263
|
+
const lines = [""];
|
|
1264
|
+
for (const intent of intents) {
|
|
1265
|
+
const parts = [];
|
|
1266
|
+
if (intent.preconditions.length > 0) parts.push(`${intent.preconditions.length} pre`);
|
|
1267
|
+
if (intent.postconditions.length > 0) parts.push(`${intent.postconditions.length} post`);
|
|
1268
|
+
if (intent.invariants.length > 0) parts.push(`${intent.invariants.length} invariant`);
|
|
1269
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
1270
|
+
lines.push(` ${colors.cyan}${intent.name}${colors.reset}${summary}`);
|
|
1271
|
+
}
|
|
1272
|
+
lines.push("");
|
|
1273
|
+
return { output: lines.join("\n") };
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
// ─── .inspect ───────────────────────────────────────────────────────────
|
|
1277
|
+
{
|
|
1278
|
+
name: "inspect",
|
|
1279
|
+
aliases: ["i", "show"],
|
|
1280
|
+
description: "Show full details of an intent",
|
|
1281
|
+
usage: ".inspect [intent]",
|
|
1282
|
+
handler: (args, session) => {
|
|
1283
|
+
if (args.length === 0) {
|
|
1284
|
+
const summary = session.getSummary();
|
|
1285
|
+
const ctx = session.getEvalContext();
|
|
1286
|
+
const lines = [
|
|
1287
|
+
"",
|
|
1288
|
+
`${colors.bold}Session Summary${colors.reset}`,
|
|
1289
|
+
"",
|
|
1290
|
+
` Intents: ${summary.intentCount}`,
|
|
1291
|
+
` Variables: ${summary.variableCount}`,
|
|
1292
|
+
` Context: ${Object.keys(ctx).length} keys`,
|
|
1293
|
+
` History: ${summary.historyCount} entries`,
|
|
1294
|
+
""
|
|
1295
|
+
];
|
|
1296
|
+
return { output: lines.join("\n") };
|
|
1297
|
+
}
|
|
1298
|
+
const intentName = args[0];
|
|
1299
|
+
const intent = session.getIntent(intentName);
|
|
1300
|
+
if (!intent) {
|
|
1301
|
+
const available = session.getIntentNames().join(", ") || "(none)";
|
|
1302
|
+
return {
|
|
1303
|
+
output: formatError(`Unknown intent: ${intentName}
|
|
1304
|
+
Available: ${available}`)
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
return { output: formatIntent(intent) };
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
// ─── .exit ──────────────────────────────────────────────────────────────
|
|
1311
|
+
{
|
|
1312
|
+
name: "exit",
|
|
1313
|
+
aliases: ["quit", "q"],
|
|
1314
|
+
description: "Exit the REPL",
|
|
1315
|
+
usage: ".exit",
|
|
1316
|
+
handler: () => {
|
|
1317
|
+
return { exit: true };
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
];
|
|
1321
|
+
function levenshteinDistance(a, b) {
|
|
1322
|
+
const matrix = [];
|
|
1323
|
+
for (let i = 0; i <= b.length; i++) {
|
|
1324
|
+
matrix[i] = [i];
|
|
1325
|
+
}
|
|
1326
|
+
for (let j = 0; j <= a.length; j++) {
|
|
1327
|
+
matrix[0][j] = j;
|
|
1328
|
+
}
|
|
1329
|
+
for (let i = 1; i <= b.length; i++) {
|
|
1330
|
+
for (let j = 1; j <= a.length; j++) {
|
|
1331
|
+
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
|
|
1332
|
+
matrix[i][j] = Math.min(
|
|
1333
|
+
matrix[i - 1][j] + 1,
|
|
1334
|
+
matrix[i][j - 1] + 1,
|
|
1335
|
+
matrix[i - 1][j - 1] + cost
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return matrix[b.length][a.length];
|
|
1340
|
+
}
|
|
1341
|
+
function findSimilarCommand(input, _type) {
|
|
1342
|
+
const names = metaCommands.flatMap((c) => [c.name, ...c.aliases]);
|
|
1343
|
+
let bestMatch = null;
|
|
1344
|
+
let bestDistance = Infinity;
|
|
1345
|
+
for (const name of names) {
|
|
1346
|
+
const distance = levenshteinDistance(input.toLowerCase(), name.toLowerCase());
|
|
1347
|
+
if (distance < bestDistance && distance <= 2) {
|
|
1348
|
+
bestDistance = distance;
|
|
1349
|
+
bestMatch = name;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return bestMatch;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// src/completions.ts
|
|
1356
|
+
var KEYWORDS = [
|
|
1357
|
+
// Structure keywords
|
|
1358
|
+
{ text: "domain", type: "keyword", description: "Define a domain" },
|
|
1359
|
+
{ text: "entity", type: "keyword", description: "Define an entity" },
|
|
1360
|
+
{ text: "behavior", type: "keyword", description: "Define a behavior" },
|
|
1361
|
+
{ text: "intent", type: "keyword", description: "Define an intent" },
|
|
1362
|
+
{ text: "input", type: "keyword", description: "Input block" },
|
|
1363
|
+
{ text: "output", type: "keyword", description: "Output block" },
|
|
1364
|
+
{ text: "pre", type: "keyword", description: "Precondition" },
|
|
1365
|
+
{ text: "post", type: "keyword", description: "Postcondition" },
|
|
1366
|
+
{ text: "invariant", type: "keyword", description: "Invariant" },
|
|
1367
|
+
{ text: "scenario", type: "keyword", description: "Scenario block" },
|
|
1368
|
+
{ text: "version", type: "keyword", description: "Version declaration" },
|
|
1369
|
+
// Types
|
|
1370
|
+
{ text: "String", type: "keyword", description: "String type" },
|
|
1371
|
+
{ text: "Number", type: "keyword", description: "Number type" },
|
|
1372
|
+
{ text: "Int", type: "keyword", description: "Integer type" },
|
|
1373
|
+
{ text: "Decimal", type: "keyword", description: "Decimal type" },
|
|
1374
|
+
{ text: "Boolean", type: "keyword", description: "Boolean type" },
|
|
1375
|
+
{ text: "UUID", type: "keyword", description: "UUID type" },
|
|
1376
|
+
{ text: "Timestamp", type: "keyword", description: "Timestamp type" },
|
|
1377
|
+
{ text: "Duration", type: "keyword", description: "Duration type" },
|
|
1378
|
+
{ text: "List", type: "keyword", description: "List<T> type" },
|
|
1379
|
+
{ text: "Map", type: "keyword", description: "Map<K,V> type" },
|
|
1380
|
+
{ text: "Optional", type: "keyword", description: "Optional<T> type" },
|
|
1381
|
+
// Literals and operators
|
|
1382
|
+
{ text: "true", type: "keyword", description: "Boolean true" },
|
|
1383
|
+
{ text: "false", type: "keyword", description: "Boolean false" },
|
|
1384
|
+
{ text: "null", type: "keyword", description: "Null value" },
|
|
1385
|
+
{ text: "and", type: "keyword", description: "Logical AND" },
|
|
1386
|
+
{ text: "or", type: "keyword", description: "Logical OR" },
|
|
1387
|
+
{ text: "not", type: "keyword", description: "Logical NOT" },
|
|
1388
|
+
{ text: "implies", type: "keyword", description: "Logical implication" },
|
|
1389
|
+
{ text: "forall", type: "keyword", description: "Universal quantifier" },
|
|
1390
|
+
{ text: "exists", type: "keyword", description: "Existential quantifier" },
|
|
1391
|
+
{ text: "in", type: "keyword", description: "Membership test" },
|
|
1392
|
+
{ text: "old", type: "keyword", description: "Pre-state value (old(x))" }
|
|
1393
|
+
];
|
|
1394
|
+
var META_COMMANDS = metaCommands.map((cmd) => ({
|
|
1395
|
+
text: `.${cmd.name}`,
|
|
1396
|
+
type: "command",
|
|
1397
|
+
description: cmd.description
|
|
1398
|
+
}));
|
|
1399
|
+
var ISL_COMMANDS = [];
|
|
1400
|
+
var COMMANDS = [...META_COMMANDS];
|
|
1401
|
+
var GEN_TARGETS = [
|
|
1402
|
+
{ text: "typescript", type: "keyword", description: "Generate TypeScript contract" },
|
|
1403
|
+
{ text: "rust", type: "keyword", description: "Generate Rust contract" },
|
|
1404
|
+
{ text: "go", type: "keyword", description: "Generate Go contract" },
|
|
1405
|
+
{ text: "openapi", type: "keyword", description: "Generate OpenAPI schema" }
|
|
1406
|
+
];
|
|
1407
|
+
var CompletionProvider = class {
|
|
1408
|
+
constructor(session) {
|
|
1409
|
+
this.session = session;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Update the session reference
|
|
1413
|
+
*/
|
|
1414
|
+
setSession(session) {
|
|
1415
|
+
this.session = session;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Get completions for a line
|
|
1419
|
+
*/
|
|
1420
|
+
complete(line) {
|
|
1421
|
+
const trimmed = line.trimStart();
|
|
1422
|
+
if (trimmed.startsWith(".")) {
|
|
1423
|
+
return this.completeMetaCommand(trimmed);
|
|
1424
|
+
}
|
|
1425
|
+
if (trimmed.startsWith(":")) {
|
|
1426
|
+
return this.completeMetaCommand("." + trimmed.slice(1));
|
|
1427
|
+
}
|
|
1428
|
+
return this.completeExpression(trimmed);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Complete meta commands
|
|
1432
|
+
*/
|
|
1433
|
+
completeMetaCommand(line) {
|
|
1434
|
+
const parts = line.slice(1).split(/\s+/);
|
|
1435
|
+
const cmdPart = parts[0] || "";
|
|
1436
|
+
if (parts.length === 1) {
|
|
1437
|
+
const matches = META_COMMANDS.filter(
|
|
1438
|
+
(c) => c.text.toLowerCase().startsWith(`.${cmdPart.toLowerCase()}`)
|
|
1439
|
+
);
|
|
1440
|
+
return [matches.length > 0 ? matches : META_COMMANDS, "." + cmdPart];
|
|
1441
|
+
}
|
|
1442
|
+
return [[], line];
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Complete ISL commands
|
|
1446
|
+
*/
|
|
1447
|
+
completeISLCommand(line) {
|
|
1448
|
+
const parts = line.slice(1).split(/\s+/);
|
|
1449
|
+
const cmdPart = parts[0] || "";
|
|
1450
|
+
const args = parts.slice(1);
|
|
1451
|
+
if (parts.length === 1) {
|
|
1452
|
+
const matches = ISL_COMMANDS.filter(
|
|
1453
|
+
(c) => c.text.toLowerCase().startsWith(`:${cmdPart.toLowerCase()}`)
|
|
1454
|
+
);
|
|
1455
|
+
return [matches.length > 0 ? matches : ISL_COMMANDS, ":" + cmdPart];
|
|
1456
|
+
}
|
|
1457
|
+
const cmd = cmdPart.toLowerCase();
|
|
1458
|
+
switch (cmd) {
|
|
1459
|
+
case "gen":
|
|
1460
|
+
case "generate":
|
|
1461
|
+
case "g":
|
|
1462
|
+
return this.completeGenCommand(args);
|
|
1463
|
+
case "check":
|
|
1464
|
+
case "c":
|
|
1465
|
+
case "inspect":
|
|
1466
|
+
case "i":
|
|
1467
|
+
case "show":
|
|
1468
|
+
return this.completeIntentName(args[0] || "");
|
|
1469
|
+
case "load":
|
|
1470
|
+
case "l":
|
|
1471
|
+
case "export":
|
|
1472
|
+
case "save":
|
|
1473
|
+
return this.completeFilePath(args[0] || "");
|
|
1474
|
+
default:
|
|
1475
|
+
return [[], line];
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Complete :gen command arguments
|
|
1480
|
+
*/
|
|
1481
|
+
completeGenCommand(args) {
|
|
1482
|
+
if (args.length <= 1) {
|
|
1483
|
+
const partial = args[0] || "";
|
|
1484
|
+
const matches = GEN_TARGETS.filter(
|
|
1485
|
+
(t) => t.text.toLowerCase().startsWith(partial.toLowerCase())
|
|
1486
|
+
);
|
|
1487
|
+
return [matches.length > 0 ? matches : GEN_TARGETS, partial];
|
|
1488
|
+
}
|
|
1489
|
+
return this.completeIntentName(args[1] || "");
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Complete intent names
|
|
1493
|
+
*/
|
|
1494
|
+
completeIntentName(partial) {
|
|
1495
|
+
const intents = this.session.getAllIntents();
|
|
1496
|
+
const items = intents.map((intent) => ({
|
|
1497
|
+
text: intent.name,
|
|
1498
|
+
type: "intent",
|
|
1499
|
+
description: `${intent.preconditions.length} pre, ${intent.postconditions.length} post`
|
|
1500
|
+
}));
|
|
1501
|
+
const matches = items.filter(
|
|
1502
|
+
(i) => i.text.toLowerCase().startsWith(partial.toLowerCase())
|
|
1503
|
+
);
|
|
1504
|
+
return [matches.length > 0 ? matches : items, partial];
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Complete file paths
|
|
1508
|
+
*/
|
|
1509
|
+
completeFilePath(partial) {
|
|
1510
|
+
try {
|
|
1511
|
+
const dir = path4.dirname(partial) || ".";
|
|
1512
|
+
const base = path4.basename(partial);
|
|
1513
|
+
const resolvedDir = path4.resolve(this.session.getConfig().cwd || process.cwd(), dir);
|
|
1514
|
+
if (!fs4.existsSync(resolvedDir)) {
|
|
1515
|
+
return [[], partial];
|
|
1516
|
+
}
|
|
1517
|
+
const entries = fs4.readdirSync(resolvedDir, { withFileTypes: true });
|
|
1518
|
+
const items = entries.filter((e) => {
|
|
1519
|
+
const name = e.name.toLowerCase();
|
|
1520
|
+
return name.startsWith(base.toLowerCase()) && (e.isDirectory() || name.endsWith(".isl"));
|
|
1521
|
+
}).map((e) => ({
|
|
1522
|
+
text: path4.join(dir, e.name + (e.isDirectory() ? "/" : "")),
|
|
1523
|
+
type: "file",
|
|
1524
|
+
description: e.isDirectory() ? "Directory" : "ISL file"
|
|
1525
|
+
}));
|
|
1526
|
+
return [items, partial];
|
|
1527
|
+
} catch {
|
|
1528
|
+
return [[], partial];
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Complete expressions
|
|
1533
|
+
*/
|
|
1534
|
+
completeExpression(line) {
|
|
1535
|
+
const items = [...KEYWORDS];
|
|
1536
|
+
for (const intent of this.session.getAllIntents()) {
|
|
1537
|
+
items.push({
|
|
1538
|
+
text: intent.name,
|
|
1539
|
+
type: "intent",
|
|
1540
|
+
description: "Defined intent"
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
for (const [name] of this.session.getAllVariables()) {
|
|
1544
|
+
items.push({
|
|
1545
|
+
text: name,
|
|
1546
|
+
type: "variable",
|
|
1547
|
+
description: "Variable"
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
items.push({
|
|
1551
|
+
text: "_",
|
|
1552
|
+
type: "variable",
|
|
1553
|
+
description: "Last result"
|
|
1554
|
+
});
|
|
1555
|
+
const match = line.match(/[\w.]+$/);
|
|
1556
|
+
const partial = match ? match[0] : "";
|
|
1557
|
+
const matches = items.filter(
|
|
1558
|
+
(i) => i.text.toLowerCase().startsWith(partial.toLowerCase())
|
|
1559
|
+
);
|
|
1560
|
+
return [matches.length > 0 ? matches : items, partial];
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Get all available completions (for help)
|
|
1564
|
+
*/
|
|
1565
|
+
getAllCompletions() {
|
|
1566
|
+
return {
|
|
1567
|
+
metaCommands: META_COMMANDS,
|
|
1568
|
+
islCommands: ISL_COMMANDS,
|
|
1569
|
+
keywords: KEYWORDS,
|
|
1570
|
+
intents: this.session.getAllIntents().map((i) => ({
|
|
1571
|
+
text: i.name,
|
|
1572
|
+
type: "intent",
|
|
1573
|
+
description: `${i.preconditions.length} pre, ${i.postconditions.length} post`
|
|
1574
|
+
}))
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
function createCompleter(provider) {
|
|
1579
|
+
return (line) => {
|
|
1580
|
+
const [items, partial] = provider.complete(line);
|
|
1581
|
+
const completions = items.map((i) => i.text);
|
|
1582
|
+
return [completions, partial];
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/repl.ts
|
|
1587
|
+
var VERSION = "0.1.0";
|
|
1588
|
+
var PROMPT = `${colors.cyan}isl>${colors.reset} `;
|
|
1589
|
+
var CONTINUATION_PROMPT = `${colors.cyan}...>${colors.reset} `;
|
|
1590
|
+
var ISLREPL = class {
|
|
1591
|
+
session;
|
|
1592
|
+
history;
|
|
1593
|
+
completionProvider;
|
|
1594
|
+
rl = null;
|
|
1595
|
+
buffer = [];
|
|
1596
|
+
braceCount = 0;
|
|
1597
|
+
options;
|
|
1598
|
+
running = false;
|
|
1599
|
+
constructor(options = {}) {
|
|
1600
|
+
this.options = {
|
|
1601
|
+
colors: options.colors !== false,
|
|
1602
|
+
verbose: options.verbose ?? false,
|
|
1603
|
+
historyFile: options.historyFile,
|
|
1604
|
+
load: options.load,
|
|
1605
|
+
context: options.context,
|
|
1606
|
+
parseOnly: options.parseOnly ?? false
|
|
1607
|
+
};
|
|
1608
|
+
this.session = new Session({ colors: this.options.colors });
|
|
1609
|
+
this.history = new History({
|
|
1610
|
+
historyFile: this.options.historyFile
|
|
1611
|
+
});
|
|
1612
|
+
this.completionProvider = new CompletionProvider(this.session);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Start the REPL
|
|
1616
|
+
*/
|
|
1617
|
+
start() {
|
|
1618
|
+
if (this.running) return;
|
|
1619
|
+
this.running = true;
|
|
1620
|
+
this.applyStartupOptions();
|
|
1621
|
+
if (this.options.parseOnly || !process.stdin.isTTY) {
|
|
1622
|
+
this.runPipeMode();
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
this.history.load();
|
|
1626
|
+
this.rl = readline.createInterface({
|
|
1627
|
+
input: process.stdin,
|
|
1628
|
+
output: process.stdout,
|
|
1629
|
+
prompt: PROMPT,
|
|
1630
|
+
completer: createCompleter(this.completionProvider),
|
|
1631
|
+
terminal: true
|
|
1632
|
+
});
|
|
1633
|
+
this.printBanner();
|
|
1634
|
+
this.rl.prompt();
|
|
1635
|
+
this.rl.on("line", (line) => {
|
|
1636
|
+
this.handleLine(line);
|
|
1637
|
+
if (this.rl && this.running) {
|
|
1638
|
+
this.rl.setPrompt(this.braceCount > 0 ? CONTINUATION_PROMPT : PROMPT);
|
|
1639
|
+
this.rl.prompt();
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
this.rl.on("close", () => {
|
|
1643
|
+
this.exit();
|
|
1644
|
+
});
|
|
1645
|
+
this.rl.on("SIGINT", () => {
|
|
1646
|
+
if (this.buffer.length > 0) {
|
|
1647
|
+
this.buffer = [];
|
|
1648
|
+
this.braceCount = 0;
|
|
1649
|
+
process.stdout.write("\n" + formatWarning("Input cancelled") + "\n");
|
|
1650
|
+
this.rl.setPrompt(PROMPT);
|
|
1651
|
+
this.rl.prompt();
|
|
1652
|
+
} else {
|
|
1653
|
+
process.stdout.write("\n" + formatWarning("Use .exit to quit") + "\n");
|
|
1654
|
+
this.rl.prompt();
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Apply startup options (--load, --context)
|
|
1660
|
+
*/
|
|
1661
|
+
applyStartupOptions() {
|
|
1662
|
+
if (this.options.context) {
|
|
1663
|
+
const result = this.session.setEvalContext(this.options.context);
|
|
1664
|
+
if (result.success) {
|
|
1665
|
+
process.stdout.write(
|
|
1666
|
+
formatSuccess(`Context set (${result.count} variable${result.count !== 1 ? "s" : ""})`) + "\n"
|
|
1667
|
+
);
|
|
1668
|
+
} else {
|
|
1669
|
+
process.stdout.write(formatError(`Invalid context JSON: ${result.error}`) + "\n");
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (this.options.load) {
|
|
1673
|
+
const loadCmd = metaCommands.find((c) => c.name === "load");
|
|
1674
|
+
if (loadCmd) {
|
|
1675
|
+
const result = loadCmd.handler([this.options.load], this.session, this);
|
|
1676
|
+
if (result.output) {
|
|
1677
|
+
process.stdout.write(result.output + "\n");
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Run in pipe mode (read all stdin, parse, and output)
|
|
1684
|
+
*/
|
|
1685
|
+
runPipeMode() {
|
|
1686
|
+
let input = "";
|
|
1687
|
+
process.stdin.setEncoding("utf-8");
|
|
1688
|
+
process.stdin.on("data", (chunk) => {
|
|
1689
|
+
input += chunk;
|
|
1690
|
+
});
|
|
1691
|
+
process.stdin.on("end", () => {
|
|
1692
|
+
const trimmed = input.trim();
|
|
1693
|
+
if (!trimmed) {
|
|
1694
|
+
process.exit(0);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (this.options.parseOnly) {
|
|
1698
|
+
const parseCmd = metaCommands.find((c) => c.name === "parse");
|
|
1699
|
+
if (parseCmd) {
|
|
1700
|
+
const result = parseCmd.handler(trimmed.split(" "), this.session, this);
|
|
1701
|
+
if (result.output) {
|
|
1702
|
+
process.stdout.write(result.output + "\n");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
} else {
|
|
1706
|
+
const lines = trimmed.split("\n");
|
|
1707
|
+
for (const line of lines) {
|
|
1708
|
+
this.handleLine(line);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
process.exit(0);
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Print the welcome banner
|
|
1716
|
+
*/
|
|
1717
|
+
printBanner() {
|
|
1718
|
+
const banner = `
|
|
1719
|
+
${colors.cyan}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
1720
|
+
\u2551 \u2551
|
|
1721
|
+
\u2551 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2551
|
|
1722
|
+
\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2551
|
|
1723
|
+
\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2551
|
|
1724
|
+
\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2551
|
|
1725
|
+
\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
|
|
1726
|
+
\u2551 \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u2551
|
|
1727
|
+
\u2551 \u2551
|
|
1728
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${colors.reset}
|
|
1729
|
+
|
|
1730
|
+
${colors.bold}ISL v${VERSION}${colors.reset} \u2014 Intent Specification Language
|
|
1731
|
+
Type ${colors.cyan}.help${colors.reset} for commands, ${colors.cyan}.exit${colors.reset} to quit
|
|
1732
|
+
`;
|
|
1733
|
+
process.stdout.write(banner);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Handle a line of input
|
|
1737
|
+
*/
|
|
1738
|
+
handleLine(line) {
|
|
1739
|
+
const trimmed = line.trim();
|
|
1740
|
+
if (!trimmed && this.buffer.length === 0) {
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (trimmed.startsWith(".") && this.buffer.length === 0) {
|
|
1744
|
+
this.handleDotCommand(trimmed);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (trimmed.startsWith(":") && this.buffer.length === 0) {
|
|
1748
|
+
this.handleDotCommand("." + trimmed.slice(1));
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
this.braceCount += (line.match(/\{/g) || []).length;
|
|
1752
|
+
this.braceCount -= (line.match(/\}/g) || []).length;
|
|
1753
|
+
this.buffer.push(line);
|
|
1754
|
+
if (this.braceCount <= 0) {
|
|
1755
|
+
const code = this.buffer.join("\n");
|
|
1756
|
+
this.buffer = [];
|
|
1757
|
+
this.braceCount = 0;
|
|
1758
|
+
this.history.add(code);
|
|
1759
|
+
this.session.addToHistory(code);
|
|
1760
|
+
this.evaluate(code);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Handle a dot command (. prefix)
|
|
1765
|
+
*/
|
|
1766
|
+
handleDotCommand(input) {
|
|
1767
|
+
const parts = input.slice(1).split(/\s+/);
|
|
1768
|
+
const cmdName = parts[0]?.toLowerCase() || "";
|
|
1769
|
+
const args = parts.slice(1);
|
|
1770
|
+
const rawArgs = input.slice(1 + (cmdName.length || 0)).trim();
|
|
1771
|
+
const command = metaCommands.find(
|
|
1772
|
+
(c) => c.name === cmdName || c.aliases.includes(cmdName)
|
|
1773
|
+
);
|
|
1774
|
+
if (command) {
|
|
1775
|
+
this.history.add(input);
|
|
1776
|
+
const needsRawArgs = ["context", "ctx", "eval", "e", "parse", "p", "ast", "load", "l"];
|
|
1777
|
+
const effectiveArgs = needsRawArgs.includes(cmdName) && rawArgs ? [rawArgs] : args;
|
|
1778
|
+
const result = command.handler(effectiveArgs, this.session, this);
|
|
1779
|
+
if (result.output) {
|
|
1780
|
+
process.stdout.write(result.output + "\n");
|
|
1781
|
+
}
|
|
1782
|
+
if (result.exit) {
|
|
1783
|
+
this.exit();
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
const suggestion = findSimilarCommand(cmdName);
|
|
1787
|
+
if (suggestion) {
|
|
1788
|
+
process.stdout.write(formatError(`Unknown command: .${cmdName}`) + "\n");
|
|
1789
|
+
process.stdout.write(formatWarning(`Did you mean: .${suggestion}?`) + "\n");
|
|
1790
|
+
} else {
|
|
1791
|
+
process.stdout.write(formatError(`Unknown command: .${cmdName}`) + "\n");
|
|
1792
|
+
process.stdout.write(`Type ${colors.cyan}.help${colors.reset} for available commands
|
|
1793
|
+
`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Evaluate ISL code (multi-line input or bare expressions)
|
|
1799
|
+
*/
|
|
1800
|
+
evaluate(code) {
|
|
1801
|
+
try {
|
|
1802
|
+
const trimmed = code.trim();
|
|
1803
|
+
try {
|
|
1804
|
+
const { parse } = __require("@isl-lang/parser");
|
|
1805
|
+
let parseInput = trimmed;
|
|
1806
|
+
const needsWrapper = !trimmed.startsWith("domain ");
|
|
1807
|
+
if (needsWrapper) {
|
|
1808
|
+
parseInput = `domain _REPL { version: "0.0.1"
|
|
1809
|
+
${trimmed}
|
|
1810
|
+
}`;
|
|
1811
|
+
}
|
|
1812
|
+
const result = parse(parseInput, "<repl>");
|
|
1813
|
+
if (result.errors.length > 0) {
|
|
1814
|
+
for (const err of result.errors) {
|
|
1815
|
+
const loc = err.location;
|
|
1816
|
+
if (loc) {
|
|
1817
|
+
const adjustedLine = needsWrapper ? Math.max(1, loc.line - 1) : loc.line;
|
|
1818
|
+
const lines = trimmed.split("\n");
|
|
1819
|
+
const errorLine = lines[adjustedLine - 1] || "";
|
|
1820
|
+
process.stdout.write(
|
|
1821
|
+
`${colors.red}\u2717 Error at line ${adjustedLine}, col ${loc.column}:${colors.reset}
|
|
1822
|
+
`
|
|
1823
|
+
);
|
|
1824
|
+
process.stdout.write(` ${errorLine}
|
|
1825
|
+
`);
|
|
1826
|
+
process.stdout.write(` ${" ".repeat(Math.max(0, loc.column - 1))}${colors.red}^^^^^${colors.reset}
|
|
1827
|
+
`);
|
|
1828
|
+
const typeMatch = err.message.match(/Unknown type '(\w+)'/i) || err.message.match(/unexpected.*'(\w+)'/i);
|
|
1829
|
+
if (typeMatch) {
|
|
1830
|
+
const suggestion = suggestCorrection(typeMatch[1]);
|
|
1831
|
+
if (suggestion) {
|
|
1832
|
+
process.stdout.write(
|
|
1833
|
+
` ${colors.yellow}Did you mean '${suggestion}'?${colors.reset}
|
|
1834
|
+
`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
} else {
|
|
1838
|
+
process.stdout.write(` ${err.message}
|
|
1839
|
+
`);
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
process.stdout.write(formatError(err.message) + "\n");
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (result.domain) {
|
|
1848
|
+
this.session.setDomainAST(result.domain);
|
|
1849
|
+
const domain = result.domain;
|
|
1850
|
+
if (needsWrapper) {
|
|
1851
|
+
const entityCount = domain.entities?.length ?? 0;
|
|
1852
|
+
const behaviorCount = domain.behaviors?.length ?? 0;
|
|
1853
|
+
const parts = [];
|
|
1854
|
+
if (entityCount > 0) parts.push(`${entityCount} entit${entityCount === 1 ? "y" : "ies"}`);
|
|
1855
|
+
if (behaviorCount > 0) parts.push(`${behaviorCount} behavior${behaviorCount === 1 ? "" : "s"}`);
|
|
1856
|
+
if (parts.length > 0) {
|
|
1857
|
+
process.stdout.write(
|
|
1858
|
+
formatSuccess(`Parsed: ${parts.join(", ")}`) + "\n"
|
|
1859
|
+
);
|
|
1860
|
+
} else {
|
|
1861
|
+
process.stdout.write(formatSuccess("Parsed successfully") + "\n");
|
|
1862
|
+
}
|
|
1863
|
+
} else {
|
|
1864
|
+
const name = domain.name?.name ?? "Unknown";
|
|
1865
|
+
const entityCount = domain.entities?.length ?? 0;
|
|
1866
|
+
const behaviorCount = domain.behaviors?.length ?? 0;
|
|
1867
|
+
process.stdout.write(
|
|
1868
|
+
formatSuccess(
|
|
1869
|
+
`Parsed: domain ${name} (${entityCount} entit${entityCount === 1 ? "y" : "ies"}, ${behaviorCount} behavior${behaviorCount === 1 ? "" : "s"})`
|
|
1870
|
+
) + "\n"
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
if (trimmed.startsWith("intent ") || trimmed.startsWith("behavior ")) {
|
|
1878
|
+
this.evaluateIntent(trimmed);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (trimmed.startsWith("domain ")) {
|
|
1882
|
+
process.stdout.write(formatSuccess("Parsed domain block") + "\n");
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
process.stdout.write(
|
|
1886
|
+
formatWarning(`Cannot evaluate: ${trimmed.split("\n")[0]}...`) + "\n"
|
|
1887
|
+
);
|
|
1888
|
+
process.stdout.write(
|
|
1889
|
+
`Use ${colors.cyan}.help${colors.reset} for available commands
|
|
1890
|
+
`
|
|
1891
|
+
);
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
this.printError(error);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Evaluate an intent definition
|
|
1898
|
+
*/
|
|
1899
|
+
evaluateIntent(code) {
|
|
1900
|
+
const intent = this.session.parseIntent(code);
|
|
1901
|
+
if (intent) {
|
|
1902
|
+
this.session.defineIntent(intent);
|
|
1903
|
+
const preCount = intent.preconditions.length;
|
|
1904
|
+
const postCount = intent.postconditions.length;
|
|
1905
|
+
const invCount = intent.invariants.length;
|
|
1906
|
+
const parts = [];
|
|
1907
|
+
if (preCount > 0) parts.push(`${preCount} pre`);
|
|
1908
|
+
if (postCount > 0) parts.push(`${postCount} post`);
|
|
1909
|
+
if (invCount > 0) parts.push(`${invCount} invariant`);
|
|
1910
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
1911
|
+
process.stdout.write(
|
|
1912
|
+
formatSuccess(`Intent '${intent.name}' defined${summary}`) + "\n"
|
|
1913
|
+
);
|
|
1914
|
+
} else {
|
|
1915
|
+
const behaviorMatch = code.match(/^behavior\s+(\w+)\s*\{([\s\S]*)\}$/);
|
|
1916
|
+
if (behaviorMatch) {
|
|
1917
|
+
const name = behaviorMatch[1];
|
|
1918
|
+
const body = behaviorMatch[2];
|
|
1919
|
+
const newIntent = {
|
|
1920
|
+
name,
|
|
1921
|
+
preconditions: [],
|
|
1922
|
+
postconditions: [],
|
|
1923
|
+
invariants: [],
|
|
1924
|
+
scenarios: [],
|
|
1925
|
+
rawSource: code
|
|
1926
|
+
};
|
|
1927
|
+
const preSection = body.match(/pre(?:conditions)?\s*\{([^}]*)\}/s);
|
|
1928
|
+
if (preSection) {
|
|
1929
|
+
for (const line of preSection[1].trim().split("\n")) {
|
|
1930
|
+
const expr = line.trim().replace(/^-\s*/, "").trim();
|
|
1931
|
+
if (expr) newIntent.preconditions.push({ expression: expr });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
const postSection = body.match(/post(?:conditions)?\s*\{([^}]*)\}/s);
|
|
1935
|
+
if (postSection) {
|
|
1936
|
+
for (const line of postSection[1].trim().split("\n")) {
|
|
1937
|
+
const expr = line.trim().replace(/^-\s*/, "").trim();
|
|
1938
|
+
if (expr) newIntent.postconditions.push({ expression: expr });
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
this.session.defineIntent(newIntent);
|
|
1942
|
+
const parts = [];
|
|
1943
|
+
if (newIntent.preconditions.length > 0) parts.push(`${newIntent.preconditions.length} pre`);
|
|
1944
|
+
if (newIntent.postconditions.length > 0) parts.push(`${newIntent.postconditions.length} post`);
|
|
1945
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
1946
|
+
process.stdout.write(
|
|
1947
|
+
formatSuccess(`Intent '${name}' defined${summary}`) + "\n"
|
|
1948
|
+
);
|
|
1949
|
+
} else {
|
|
1950
|
+
process.stdout.write(formatError("Failed to parse intent definition") + "\n");
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Print an error
|
|
1956
|
+
*/
|
|
1957
|
+
printError(error) {
|
|
1958
|
+
if (error instanceof Error) {
|
|
1959
|
+
process.stdout.write(formatError(error.message) + "\n");
|
|
1960
|
+
if (this.options.verbose && error.stack) {
|
|
1961
|
+
process.stdout.write(colors.gray + error.stack + colors.reset + "\n");
|
|
1962
|
+
}
|
|
1963
|
+
} else {
|
|
1964
|
+
process.stdout.write(formatError(String(error)) + "\n");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Exit the REPL
|
|
1969
|
+
*/
|
|
1970
|
+
exit() {
|
|
1971
|
+
this.running = false;
|
|
1972
|
+
this.history.save();
|
|
1973
|
+
process.stdout.write(`
|
|
1974
|
+
${colors.yellow}Goodbye!${colors.reset}
|
|
1975
|
+
`);
|
|
1976
|
+
if (this.rl) {
|
|
1977
|
+
this.rl.close();
|
|
1978
|
+
}
|
|
1979
|
+
process.exit(0);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Get the session
|
|
1983
|
+
*/
|
|
1984
|
+
getSession() {
|
|
1985
|
+
return this.session;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Get history
|
|
1989
|
+
*/
|
|
1990
|
+
getHistory() {
|
|
1991
|
+
return this.history;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Execute a single command and return result (for testing)
|
|
1995
|
+
*/
|
|
1996
|
+
async executeOnce(input) {
|
|
1997
|
+
const trimmed = input.trim();
|
|
1998
|
+
if (trimmed.startsWith(".") || trimmed.startsWith(":")) {
|
|
1999
|
+
const normalized = trimmed.startsWith(":") ? "." + trimmed.slice(1) : trimmed;
|
|
2000
|
+
const parts = normalized.slice(1).split(/\s+/);
|
|
2001
|
+
const cmdName = parts[0]?.toLowerCase() || "";
|
|
2002
|
+
const rawArgs = normalized.slice(1 + (cmdName.length || 0)).trim();
|
|
2003
|
+
const command = metaCommands.find(
|
|
2004
|
+
(c) => c.name === cmdName || c.aliases.includes(cmdName)
|
|
2005
|
+
);
|
|
2006
|
+
if (command) {
|
|
2007
|
+
const needsRawArgs = ["context", "ctx", "eval", "e", "parse", "p", "ast", "load", "l"];
|
|
2008
|
+
const effectiveArgs = needsRawArgs.includes(cmdName) && rawArgs ? [rawArgs] : parts.slice(1);
|
|
2009
|
+
const result = command.handler(effectiveArgs, this.session, this);
|
|
2010
|
+
return { success: true, output: result.output };
|
|
2011
|
+
}
|
|
2012
|
+
return { success: false, error: `Unknown command: ${cmdName}` };
|
|
2013
|
+
}
|
|
2014
|
+
if (trimmed.startsWith("intent ") || trimmed.startsWith("behavior ")) {
|
|
2015
|
+
const intent = this.session.parseIntent(trimmed);
|
|
2016
|
+
if (intent) {
|
|
2017
|
+
this.session.defineIntent(intent);
|
|
2018
|
+
return { success: true, output: `Intent '${intent.name}' defined` };
|
|
2019
|
+
}
|
|
2020
|
+
return { success: false, error: "Failed to parse intent" };
|
|
2021
|
+
}
|
|
2022
|
+
return { success: false, error: "Unknown input" };
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
var KNOWN_TYPES = [
|
|
2026
|
+
"String",
|
|
2027
|
+
"Int",
|
|
2028
|
+
"Decimal",
|
|
2029
|
+
"Boolean",
|
|
2030
|
+
"UUID",
|
|
2031
|
+
"Timestamp",
|
|
2032
|
+
"Duration",
|
|
2033
|
+
"List",
|
|
2034
|
+
"Map",
|
|
2035
|
+
"Optional",
|
|
2036
|
+
"Number"
|
|
2037
|
+
];
|
|
2038
|
+
function suggestCorrection(typo) {
|
|
2039
|
+
const lower = typo.toLowerCase();
|
|
2040
|
+
for (const t of KNOWN_TYPES) {
|
|
2041
|
+
if (t.toLowerCase() === lower) return t;
|
|
2042
|
+
if (t.toLowerCase().startsWith(lower.slice(0, 3)) && Math.abs(t.length - typo.length) <= 2) {
|
|
2043
|
+
return t;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
function startREPL(options) {
|
|
2049
|
+
const repl = new ISLREPL(options);
|
|
2050
|
+
repl.start();
|
|
2051
|
+
return repl;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/cli.ts
|
|
2055
|
+
function parseArgs(argv) {
|
|
2056
|
+
const options = {
|
|
2057
|
+
help: false,
|
|
2058
|
+
colors: true,
|
|
2059
|
+
verbose: false,
|
|
2060
|
+
parseOnly: false
|
|
2061
|
+
};
|
|
2062
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2063
|
+
const arg = argv[i];
|
|
2064
|
+
switch (arg) {
|
|
2065
|
+
case "--help":
|
|
2066
|
+
case "-h":
|
|
2067
|
+
options.help = true;
|
|
2068
|
+
break;
|
|
2069
|
+
case "--no-color":
|
|
2070
|
+
options.colors = false;
|
|
2071
|
+
break;
|
|
2072
|
+
case "--verbose":
|
|
2073
|
+
case "-v":
|
|
2074
|
+
options.verbose = true;
|
|
2075
|
+
break;
|
|
2076
|
+
case "--load":
|
|
2077
|
+
options.load = argv[++i];
|
|
2078
|
+
break;
|
|
2079
|
+
case "--context":
|
|
2080
|
+
options.context = argv[++i];
|
|
2081
|
+
break;
|
|
2082
|
+
case "--parse":
|
|
2083
|
+
options.parseOnly = true;
|
|
2084
|
+
break;
|
|
2085
|
+
default:
|
|
2086
|
+
if (arg.startsWith("--load=")) {
|
|
2087
|
+
options.load = arg.slice(7);
|
|
2088
|
+
} else if (arg.startsWith("--context=")) {
|
|
2089
|
+
options.context = arg.slice(10);
|
|
2090
|
+
}
|
|
2091
|
+
break;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return options;
|
|
2095
|
+
}
|
|
2096
|
+
function printHelp() {
|
|
2097
|
+
process.stdout.write(`
|
|
2098
|
+
ISL REPL - Intent Specification Language Interactive Shell
|
|
2099
|
+
|
|
2100
|
+
Usage: isl-repl [options]
|
|
2101
|
+
|
|
2102
|
+
Options:
|
|
2103
|
+
--load <file> Load an ISL file on start
|
|
2104
|
+
--context <json> Set initial evaluation context
|
|
2105
|
+
--parse Parse mode (non-interactive, for piped input)
|
|
2106
|
+
--no-color Disable colored output
|
|
2107
|
+
-v, --verbose Enable verbose output
|
|
2108
|
+
-h, --help Show this help message
|
|
2109
|
+
|
|
2110
|
+
Inside the REPL:
|
|
2111
|
+
.help Show all commands
|
|
2112
|
+
.parse <isl> Parse ISL and show AST
|
|
2113
|
+
.eval <expr> Evaluate expression against context
|
|
2114
|
+
.check [intent] Type check intents
|
|
2115
|
+
.gen [intent] Generate TypeScript from intent
|
|
2116
|
+
.load <file> Load an .isl file
|
|
2117
|
+
.context <json> Set evaluation context (mock data)
|
|
2118
|
+
.clear Reset session state
|
|
2119
|
+
.list List defined intents
|
|
2120
|
+
.inspect [intent] Show full details of an intent
|
|
2121
|
+
.history Show command history
|
|
2122
|
+
.exit Exit the REPL
|
|
2123
|
+
|
|
2124
|
+
Multi-line Input:
|
|
2125
|
+
Type ISL with braces \u2014 the REPL auto-detects multi-line:
|
|
2126
|
+
isl> domain Example {
|
|
2127
|
+
...> entity User {
|
|
2128
|
+
...> id: UUID
|
|
2129
|
+
...> name: String
|
|
2130
|
+
...> }
|
|
2131
|
+
...> }
|
|
2132
|
+
|
|
2133
|
+
Examples:
|
|
2134
|
+
$ isl-repl
|
|
2135
|
+
$ isl-repl --load auth.isl
|
|
2136
|
+
$ isl-repl --context '{"user": {"id": 1}}'
|
|
2137
|
+
$ echo 'domain X { version: "1.0" }' | isl-repl --parse
|
|
2138
|
+
`);
|
|
2139
|
+
}
|
|
2140
|
+
function main() {
|
|
2141
|
+
const args = process.argv.slice(2);
|
|
2142
|
+
const options = parseArgs(args);
|
|
2143
|
+
if (options.help) {
|
|
2144
|
+
printHelp();
|
|
2145
|
+
process.exit(0);
|
|
2146
|
+
}
|
|
2147
|
+
startREPL(options);
|
|
2148
|
+
}
|
|
2149
|
+
var isMainModule = typeof __require !== "undefined" ? __require.main === module : process.argv[1]?.includes("cli");
|
|
2150
|
+
if (isMainModule) {
|
|
2151
|
+
main();
|
|
2152
|
+
}
|
|
2153
|
+
export {
|
|
2154
|
+
main
|
|
2155
|
+
};
|
|
2156
|
+
//# sourceMappingURL=cli.js.map
|