dirac-lang 0.1.21 → 0.1.23
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/dist/{chunk-OS7DII2I.js → chunk-APWJJXNI.js} +138 -13
- package/dist/{chunk-JRNDLSQJ.js → chunk-RGS2XK6T.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{interpreter-WSRBK3KI.js → interpreter-UZDVASAW.js} +1 -1
- package/dist/test-runner.js +67 -9
- package/package.json +1 -1
- package/src/runtime/interpreter.ts +10 -0
- package/src/tags/environment.ts +21 -0
- package/src/tags/eval.ts +3 -0
- package/src/tags/index.ts +4 -0
- package/src/tags/input.ts +182 -0
- package/src/test-runner.ts +83 -9
- package/test-input-debug.di +19 -0
- package/tests/environment-basic.test.di +22 -0
- package/tests/environment-nonexistent.test.di +5 -0
- package/tests/environment-with-defvar.test.di +15 -0
- package/tests/input-file-all.test.di +5 -0
- package/tests/input-file-line.test.di +15 -0
- package/tests/input-file-loop.test.di +12 -0
- package/tests/input-stdin-all.test.di +8 -0
- package/tests/input-stdin-all.txt +1 -0
- package/tests/test-input.txt +3 -0
|
@@ -419,12 +419,12 @@ async function executeIf(session, element) {
|
|
|
419
419
|
const condition = await evaluatePredicate(session, conditionElement);
|
|
420
420
|
if (condition) {
|
|
421
421
|
if (thenElement) {
|
|
422
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
422
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
423
423
|
await integrateChildren2(session, thenElement);
|
|
424
424
|
}
|
|
425
425
|
} else {
|
|
426
426
|
if (elseElement) {
|
|
427
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
427
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
428
428
|
await integrateChildren2(session, elseElement);
|
|
429
429
|
}
|
|
430
430
|
}
|
|
@@ -437,7 +437,7 @@ async function evaluatePredicate(session, predicateElement) {
|
|
|
437
437
|
return await evaluateCondition(session, predicateElement);
|
|
438
438
|
}
|
|
439
439
|
const outputLengthBefore = session.output.length;
|
|
440
|
-
const { integrate: integrate2 } = await import("./interpreter-
|
|
440
|
+
const { integrate: integrate2 } = await import("./interpreter-UZDVASAW.js");
|
|
441
441
|
await integrate2(session, predicateElement);
|
|
442
442
|
const newOutputChunks = session.output.slice(outputLengthBefore);
|
|
443
443
|
const result = newOutputChunks.join("").trim();
|
|
@@ -460,11 +460,11 @@ async function evaluateCondition(session, condElement) {
|
|
|
460
460
|
}
|
|
461
461
|
const outputLengthBefore = session.output.length;
|
|
462
462
|
const args = [];
|
|
463
|
-
const { integrate: integrate2 } = await import("./interpreter-
|
|
463
|
+
const { integrate: integrate2 } = await import("./interpreter-UZDVASAW.js");
|
|
464
464
|
for (const child of condElement.children) {
|
|
465
465
|
if (child.tag.toLowerCase() === "arg") {
|
|
466
466
|
const argOutputStart = session.output.length;
|
|
467
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
467
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
468
468
|
await integrateChildren2(session, child);
|
|
469
469
|
const newChunks = session.output.slice(argOutputStart);
|
|
470
470
|
const argValue = newChunks.join("");
|
|
@@ -879,16 +879,17 @@ ${expr}
|
|
|
879
879
|
for (const v of session.variables) {
|
|
880
880
|
context[v.name] = v.value;
|
|
881
881
|
}
|
|
882
|
-
const { default:
|
|
882
|
+
const { default: fs4 } = await import("fs");
|
|
883
883
|
const { default: path2 } = await import("path");
|
|
884
884
|
const { fileURLToPath } = await import("url");
|
|
885
|
-
context.fs =
|
|
885
|
+
context.fs = fs4;
|
|
886
886
|
context.path = path2;
|
|
887
887
|
context.__dirname = process.cwd();
|
|
888
888
|
context.getParams = () => {
|
|
889
889
|
const params = session.parameterStack[session.parameterStack.length - 1];
|
|
890
890
|
return params && params[0] ? params[0] : null;
|
|
891
891
|
};
|
|
892
|
+
context.session = session;
|
|
892
893
|
let result;
|
|
893
894
|
const func = new AsyncFunction(...Object.keys(context), expr);
|
|
894
895
|
result = await func(...Object.values(context));
|
|
@@ -1281,7 +1282,7 @@ async function executeTagCheck(session, element) {
|
|
|
1281
1282
|
const executeTag = correctedTag || tagName;
|
|
1282
1283
|
console.error(`[tag-check] Executing <${executeTag}/> as all checks passed and execute=true.`);
|
|
1283
1284
|
const elementToExecute = correctedTag ? { ...child, tag: correctedTag } : child;
|
|
1284
|
-
const { integrate: integrate2 } = await import("./interpreter-
|
|
1285
|
+
const { integrate: integrate2 } = await import("./interpreter-UZDVASAW.js");
|
|
1285
1286
|
await integrate2(session, elementToExecute);
|
|
1286
1287
|
}
|
|
1287
1288
|
}
|
|
@@ -1290,7 +1291,7 @@ async function executeTagCheck(session, element) {
|
|
|
1290
1291
|
// src/tags/throw.ts
|
|
1291
1292
|
async function executeThrow(session, element) {
|
|
1292
1293
|
const exceptionName = element.attributes?.name || "exception";
|
|
1293
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
1294
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
1294
1295
|
const exceptionDom = {
|
|
1295
1296
|
tag: "exception-content",
|
|
1296
1297
|
attributes: { name: exceptionName },
|
|
@@ -1303,7 +1304,7 @@ async function executeThrow(session, element) {
|
|
|
1303
1304
|
// src/tags/try.ts
|
|
1304
1305
|
async function executeTry(session, element) {
|
|
1305
1306
|
setExceptionBoundary(session);
|
|
1306
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
1307
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
1307
1308
|
await integrateChildren2(session, element);
|
|
1308
1309
|
unsetExceptionBoundary(session);
|
|
1309
1310
|
}
|
|
@@ -1313,7 +1314,7 @@ async function executeCatch(session, element) {
|
|
|
1313
1314
|
const exceptionName = element.attributes?.name || "exception";
|
|
1314
1315
|
const caughtCount = lookupException(session, exceptionName);
|
|
1315
1316
|
if (caughtCount > 0) {
|
|
1316
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
1317
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
1317
1318
|
await integrateChildren2(session, element);
|
|
1318
1319
|
}
|
|
1319
1320
|
flushCurrentException(session);
|
|
@@ -1322,7 +1323,7 @@ async function executeCatch(session, element) {
|
|
|
1322
1323
|
// src/tags/exception.ts
|
|
1323
1324
|
async function executeException(session, element) {
|
|
1324
1325
|
const exceptions = getCurrentExceptions(session);
|
|
1325
|
-
const { integrateChildren: integrateChildren2 } = await import("./interpreter-
|
|
1326
|
+
const { integrateChildren: integrateChildren2 } = await import("./interpreter-UZDVASAW.js");
|
|
1326
1327
|
for (const exceptionDom of exceptions) {
|
|
1327
1328
|
await integrateChildren2(session, exceptionDom);
|
|
1328
1329
|
}
|
|
@@ -1462,7 +1463,7 @@ async function executeForeach(session, element) {
|
|
|
1462
1463
|
const parser2 = new DiracParser2();
|
|
1463
1464
|
try {
|
|
1464
1465
|
const fromElement = parser2.parse(fromAttr);
|
|
1465
|
-
const { integrate: integrate2 } = await import("./interpreter-
|
|
1466
|
+
const { integrate: integrate2 } = await import("./interpreter-UZDVASAW.js");
|
|
1466
1467
|
await integrate2(session, fromElement);
|
|
1467
1468
|
} catch (e) {
|
|
1468
1469
|
session.output = savedOutput;
|
|
@@ -1594,6 +1595,124 @@ function extractAttrFromXml(xml, attrName) {
|
|
|
1594
1595
|
return match ? match[1] : "";
|
|
1595
1596
|
}
|
|
1596
1597
|
|
|
1598
|
+
// src/tags/environment.ts
|
|
1599
|
+
async function executeEnvironment(session, element) {
|
|
1600
|
+
const name = element.attributes.name;
|
|
1601
|
+
if (!name) {
|
|
1602
|
+
throw new Error("<environment> requires name attribute");
|
|
1603
|
+
}
|
|
1604
|
+
const value = process.env[name] || "";
|
|
1605
|
+
emit(session, value);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/tags/input.ts
|
|
1609
|
+
import * as fs3 from "fs";
|
|
1610
|
+
import * as readline from "readline";
|
|
1611
|
+
var fileReaders = /* @__PURE__ */ new Map();
|
|
1612
|
+
var fileIterators = /* @__PURE__ */ new Map();
|
|
1613
|
+
async function executeInput(session, element) {
|
|
1614
|
+
const source = element.attributes.source;
|
|
1615
|
+
const mode = element.attributes.mode || "all";
|
|
1616
|
+
const pathAttr = element.attributes.path;
|
|
1617
|
+
if (!source) {
|
|
1618
|
+
throw new Error("<input> requires source attribute (stdin or file)");
|
|
1619
|
+
}
|
|
1620
|
+
if (source === "file" && !pathAttr) {
|
|
1621
|
+
throw new Error('<input source="file"> requires path attribute');
|
|
1622
|
+
}
|
|
1623
|
+
let value = "";
|
|
1624
|
+
if (source === "stdin") {
|
|
1625
|
+
if (mode === "all") {
|
|
1626
|
+
value = await readAllStdin();
|
|
1627
|
+
} else if (mode === "line") {
|
|
1628
|
+
value = await readLineStdin();
|
|
1629
|
+
} else {
|
|
1630
|
+
throw new Error(`<input> invalid mode: ${mode}. Use 'all' or 'line'`);
|
|
1631
|
+
}
|
|
1632
|
+
} else if (source === "file") {
|
|
1633
|
+
const path2 = substituteAttribute(session, pathAttr);
|
|
1634
|
+
if (mode === "all") {
|
|
1635
|
+
value = fs3.readFileSync(path2, "utf-8");
|
|
1636
|
+
} else if (mode === "line") {
|
|
1637
|
+
value = await readLineFromFile(path2);
|
|
1638
|
+
} else {
|
|
1639
|
+
throw new Error(`<input> invalid mode: ${mode}. Use 'all' or 'line'`);
|
|
1640
|
+
}
|
|
1641
|
+
} else {
|
|
1642
|
+
throw new Error(`<input> invalid source: ${source}. Use 'stdin' or 'file'`);
|
|
1643
|
+
}
|
|
1644
|
+
emit(session, value);
|
|
1645
|
+
}
|
|
1646
|
+
async function readAllStdin() {
|
|
1647
|
+
process.stdin.removeAllListeners("data");
|
|
1648
|
+
process.stdin.removeAllListeners("end");
|
|
1649
|
+
process.stdin.removeAllListeners("error");
|
|
1650
|
+
return new Promise((resolve2, reject) => {
|
|
1651
|
+
const chunks = [];
|
|
1652
|
+
const onData = (chunk) => {
|
|
1653
|
+
chunks.push(chunk);
|
|
1654
|
+
};
|
|
1655
|
+
const onEnd = () => {
|
|
1656
|
+
const result = Buffer.concat(chunks).toString("utf-8");
|
|
1657
|
+
process.stdin.removeListener("data", onData);
|
|
1658
|
+
process.stdin.removeListener("end", onEnd);
|
|
1659
|
+
process.stdin.removeListener("error", onError);
|
|
1660
|
+
resolve2(result);
|
|
1661
|
+
};
|
|
1662
|
+
const onError = (err) => {
|
|
1663
|
+
process.stdin.removeListener("data", onData);
|
|
1664
|
+
process.stdin.removeListener("end", onEnd);
|
|
1665
|
+
process.stdin.removeListener("error", onError);
|
|
1666
|
+
reject(err);
|
|
1667
|
+
};
|
|
1668
|
+
process.stdin.on("data", onData);
|
|
1669
|
+
process.stdin.on("end", onEnd);
|
|
1670
|
+
process.stdin.on("error", onError);
|
|
1671
|
+
process.stdin.resume();
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
var stdinReader = null;
|
|
1675
|
+
var stdinIterator = null;
|
|
1676
|
+
async function readLineStdin() {
|
|
1677
|
+
if (!stdinReader) {
|
|
1678
|
+
stdinReader = readline.createInterface({
|
|
1679
|
+
input: process.stdin,
|
|
1680
|
+
output: process.stdout,
|
|
1681
|
+
terminal: false
|
|
1682
|
+
});
|
|
1683
|
+
stdinIterator = stdinReader[Symbol.asyncIterator]();
|
|
1684
|
+
}
|
|
1685
|
+
const result = await stdinIterator.next();
|
|
1686
|
+
if (result.done) {
|
|
1687
|
+
stdinReader.close();
|
|
1688
|
+
stdinReader = null;
|
|
1689
|
+
stdinIterator = null;
|
|
1690
|
+
return "";
|
|
1691
|
+
}
|
|
1692
|
+
return result.value;
|
|
1693
|
+
}
|
|
1694
|
+
async function readLineFromFile(path2) {
|
|
1695
|
+
if (!fileReaders.has(path2)) {
|
|
1696
|
+
const fileStream = fs3.createReadStream(path2);
|
|
1697
|
+
const rl = readline.createInterface({
|
|
1698
|
+
input: fileStream,
|
|
1699
|
+
crlfDelay: Infinity
|
|
1700
|
+
});
|
|
1701
|
+
fileReaders.set(path2, rl);
|
|
1702
|
+
fileIterators.set(path2, rl[Symbol.asyncIterator]());
|
|
1703
|
+
}
|
|
1704
|
+
const iterator = fileIterators.get(path2);
|
|
1705
|
+
const result = await iterator.next();
|
|
1706
|
+
if (result.done) {
|
|
1707
|
+
const reader = fileReaders.get(path2);
|
|
1708
|
+
reader.close();
|
|
1709
|
+
fileReaders.delete(path2);
|
|
1710
|
+
fileIterators.delete(path2);
|
|
1711
|
+
return "";
|
|
1712
|
+
}
|
|
1713
|
+
return result.value;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1597
1716
|
// src/runtime/interpreter.ts
|
|
1598
1717
|
async function integrate(session, element) {
|
|
1599
1718
|
if (session.limits.currentDepth >= session.limits.maxDepth) {
|
|
@@ -1686,6 +1805,12 @@ async function integrate(session, element) {
|
|
|
1686
1805
|
case "attr":
|
|
1687
1806
|
await executeAttr(session, element);
|
|
1688
1807
|
break;
|
|
1808
|
+
case "environment":
|
|
1809
|
+
await executeEnvironment(session, element);
|
|
1810
|
+
break;
|
|
1811
|
+
case "input":
|
|
1812
|
+
await executeInput(session, element);
|
|
1813
|
+
break;
|
|
1689
1814
|
case "require_module":
|
|
1690
1815
|
await executeRequireModule(session, element);
|
|
1691
1816
|
break;
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
execute
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-RGS2XK6T.js";
|
|
5
|
+
import "./chunk-APWJJXNI.js";
|
|
6
6
|
import "./chunk-HRHAMPOB.js";
|
|
7
7
|
import "./chunk-E7PWEMZA.js";
|
|
8
8
|
import "./chunk-52ED23DR.js";
|
|
@@ -13,7 +13,7 @@ import "dotenv/config";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "dirac-lang",
|
|
16
|
-
version: "0.1.
|
|
16
|
+
version: "0.1.23",
|
|
17
17
|
description: "LLM-Augmented Declarative Execution",
|
|
18
18
|
type: "module",
|
|
19
19
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
createLLMAdapter,
|
|
3
3
|
execute,
|
|
4
4
|
executeUserCommand
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RGS2XK6T.js";
|
|
6
6
|
import {
|
|
7
7
|
integrate
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-APWJJXNI.js";
|
|
9
9
|
import {
|
|
10
10
|
DiracParser
|
|
11
11
|
} from "./chunk-HRHAMPOB.js";
|
package/dist/test-runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
integrate
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-APWJJXNI.js";
|
|
4
4
|
import {
|
|
5
5
|
DiracParser
|
|
6
6
|
} from "./chunk-HRHAMPOB.js";
|
|
@@ -55,17 +55,75 @@ var TestRunner = class {
|
|
|
55
55
|
passed: false
|
|
56
56
|
};
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
const stdinFile = filePath.replace(".test.di", ".txt");
|
|
59
|
+
let stdinData;
|
|
60
|
+
if (fs.existsSync(stdinFile)) {
|
|
61
|
+
stdinData = fs.readFileSync(stdinFile, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
if (stdinData !== void 0) {
|
|
64
|
+
const originalOn = process.stdin.on.bind(process.stdin);
|
|
65
|
+
const originalResume = process.stdin.resume.bind(process.stdin);
|
|
66
|
+
const originalRemoveAllListeners = process.stdin.removeAllListeners.bind(process.stdin);
|
|
67
|
+
const originalRemoveListener = process.stdin.removeListener.bind(process.stdin);
|
|
68
|
+
const listeners = {};
|
|
69
|
+
process.stdin.on = function(event, callback) {
|
|
70
|
+
if (!listeners[event]) listeners[event] = [];
|
|
71
|
+
listeners[event].push(callback);
|
|
72
|
+
return this;
|
|
73
|
+
};
|
|
74
|
+
process.stdin.removeAllListeners = function(event) {
|
|
75
|
+
if (event) {
|
|
76
|
+
delete listeners[event];
|
|
77
|
+
} else {
|
|
78
|
+
Object.keys(listeners).forEach((k) => delete listeners[k]);
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
};
|
|
82
|
+
process.stdin.removeListener = function(event, callback) {
|
|
83
|
+
if (listeners[event]) {
|
|
84
|
+
listeners[event] = listeners[event].filter((fn) => fn !== callback);
|
|
85
|
+
}
|
|
86
|
+
return this;
|
|
87
|
+
};
|
|
88
|
+
process.stdin.resume = function() {
|
|
89
|
+
setImmediate(() => {
|
|
90
|
+
if (listeners["data"]) {
|
|
91
|
+
listeners["data"].forEach((fn) => fn(Buffer.from(stdinData, "utf-8")));
|
|
92
|
+
}
|
|
93
|
+
setImmediate(() => {
|
|
94
|
+
if (listeners["end"]) {
|
|
95
|
+
listeners["end"].forEach((fn) => fn());
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
return this;
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const parser = new DiracParser();
|
|
103
|
+
const ast = parser.parse(cleanContent);
|
|
104
|
+
const session = createSession({ ...this.config, filePath });
|
|
105
|
+
await integrate(session, ast);
|
|
106
|
+
const output = getOutput(session);
|
|
107
|
+
result.actualOutput = output;
|
|
108
|
+
} finally {
|
|
109
|
+
process.stdin.on = originalOn;
|
|
110
|
+
process.stdin.resume = originalResume;
|
|
111
|
+
process.stdin.removeAllListeners = originalRemoveAllListeners;
|
|
112
|
+
process.stdin.removeListener = originalRemoveListener;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const parser = new DiracParser();
|
|
116
|
+
const ast = parser.parse(cleanContent);
|
|
117
|
+
const session = createSession({ ...this.config, filePath });
|
|
118
|
+
await integrate(session, ast);
|
|
119
|
+
const output = getOutput(session);
|
|
120
|
+
result.actualOutput = output;
|
|
121
|
+
}
|
|
64
122
|
if (metadata.expectError) {
|
|
65
123
|
result.passed = false;
|
|
66
|
-
result.error = `Expected error but test succeeded. Output: ${
|
|
124
|
+
result.error = `Expected error but test succeeded. Output: ${result.actualOutput}`;
|
|
67
125
|
} else if (metadata.expected !== void 0) {
|
|
68
|
-
const normalizedActual =
|
|
126
|
+
const normalizedActual = result.actualOutput.replace(/\s+/g, " ").trim();
|
|
69
127
|
const normalizedExpected = metadata.expected.replace(/\s+/g, " ").trim();
|
|
70
128
|
if (normalizedActual === normalizedExpected) {
|
|
71
129
|
result.passed = true;
|
package/package.json
CHANGED
|
@@ -29,6 +29,8 @@ import { executeTestIf } from '../tags/test-if.js';
|
|
|
29
29
|
import { executeAvailableSubroutines } from '../tags/available-subroutines.js';
|
|
30
30
|
import { executeForeach } from '../tags/foreach.js';
|
|
31
31
|
import { executeAttr } from '../tags/attr.js';
|
|
32
|
+
import { executeEnvironment } from '../tags/environment.js';
|
|
33
|
+
import { executeInput } from '../tags/input.js';
|
|
32
34
|
|
|
33
35
|
export async function integrate(session: DiracSession, element: DiracElement): Promise<void> {
|
|
34
36
|
// Check execution limits
|
|
@@ -152,6 +154,14 @@ export async function integrate(session: DiracSession, element: DiracElement): P
|
|
|
152
154
|
case 'attr':
|
|
153
155
|
await executeAttr(session, element);
|
|
154
156
|
break;
|
|
157
|
+
|
|
158
|
+
case 'environment':
|
|
159
|
+
await executeEnvironment(session, element);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'input':
|
|
163
|
+
await executeInput(session, element);
|
|
164
|
+
break;
|
|
155
165
|
|
|
156
166
|
case 'require_module':
|
|
157
167
|
await executeRequireModule(session, element);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <environment> tag - read environment variables
|
|
3
|
+
* Usage: <environment name="VAR_NAME" />
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DiracSession, DiracElement } from '../types/index.js';
|
|
7
|
+
import { emit } from '../runtime/session.js';
|
|
8
|
+
|
|
9
|
+
export async function executeEnvironment(session: DiracSession, element: DiracElement): Promise<void> {
|
|
10
|
+
const name = element.attributes.name;
|
|
11
|
+
|
|
12
|
+
if (!name) {
|
|
13
|
+
throw new Error('<environment> requires name attribute');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get environment variable value
|
|
17
|
+
const value = process.env[name] || '';
|
|
18
|
+
|
|
19
|
+
// Emit the value to output
|
|
20
|
+
emit(session, value);
|
|
21
|
+
}
|
package/src/tags/eval.ts
CHANGED
|
@@ -48,6 +48,9 @@ export async function executeEval(session: DiracSession, element: DiracElement):
|
|
|
48
48
|
return params && params[0] ? params[0] : null;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// Add session object for orchestration (reserved identifier in eval context)
|
|
52
|
+
context.session = session;
|
|
53
|
+
|
|
51
54
|
let result: any;
|
|
52
55
|
// Execute as async function to support top-level await
|
|
53
56
|
const func = new AsyncFunction(...Object.keys(context), expr);
|
package/src/tags/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import * as ifTag from './if';
|
|
|
15
15
|
import * as importTag from './import';
|
|
16
16
|
import * as mongodb from './mongodb';
|
|
17
17
|
import * as requireModule from './require_module';
|
|
18
|
+
import * as environment from './environment';
|
|
19
|
+
import * as input from './input';
|
|
18
20
|
|
|
19
21
|
export default {
|
|
20
22
|
...system,
|
|
@@ -34,4 +36,6 @@ export default {
|
|
|
34
36
|
...importTag,
|
|
35
37
|
...mongodb,
|
|
36
38
|
...requireModule,
|
|
39
|
+
...environment,
|
|
40
|
+
...input,
|
|
37
41
|
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <input> tag - read from stdin or file
|
|
3
|
+
* Usage:
|
|
4
|
+
* <input source="stdin" mode="all" />
|
|
5
|
+
* <input source="stdin" mode="line" />
|
|
6
|
+
* <input source="file" path="file.txt" mode="all" />
|
|
7
|
+
* <input source="file" path="file.txt" mode="line" />
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DiracSession, DiracElement } from '../types/index.js';
|
|
11
|
+
import { emit, substituteAttribute } from '../runtime/session.js';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as readline from 'readline';
|
|
14
|
+
|
|
15
|
+
// Store file readers for line-by-line reading
|
|
16
|
+
const fileReaders = new Map<string, readline.Interface>();
|
|
17
|
+
const fileIterators = new Map<string, AsyncIterator<string>>();
|
|
18
|
+
|
|
19
|
+
export async function executeInput(session: DiracSession, element: DiracElement): Promise<void> {
|
|
20
|
+
const source = element.attributes.source;
|
|
21
|
+
const mode = element.attributes.mode || 'all';
|
|
22
|
+
const pathAttr = element.attributes.path;
|
|
23
|
+
|
|
24
|
+
if (!source) {
|
|
25
|
+
throw new Error('<input> requires source attribute (stdin or file)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (source === 'file' && !pathAttr) {
|
|
29
|
+
throw new Error('<input source="file"> requires path attribute');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let value = '';
|
|
33
|
+
|
|
34
|
+
if (source === 'stdin') {
|
|
35
|
+
if (mode === 'all') {
|
|
36
|
+
// Read all stdin at once
|
|
37
|
+
value = await readAllStdin();
|
|
38
|
+
} else if (mode === 'line') {
|
|
39
|
+
// Read one line from stdin
|
|
40
|
+
value = await readLineStdin();
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(`<input> invalid mode: ${mode}. Use 'all' or 'line'`);
|
|
43
|
+
}
|
|
44
|
+
} else if (source === 'file') {
|
|
45
|
+
const path = substituteAttribute(session, pathAttr);
|
|
46
|
+
|
|
47
|
+
if (mode === 'all') {
|
|
48
|
+
// Read entire file
|
|
49
|
+
value = fs.readFileSync(path, 'utf-8');
|
|
50
|
+
} else if (mode === 'line') {
|
|
51
|
+
// Read one line from file
|
|
52
|
+
value = await readLineFromFile(path);
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`<input> invalid mode: ${mode}. Use 'all' or 'line'`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`<input> invalid source: ${source}. Use 'stdin' or 'file'`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Emit the value to output
|
|
61
|
+
emit(session, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read all stdin content at once
|
|
66
|
+
*/
|
|
67
|
+
async function readAllStdin(): Promise<string> {
|
|
68
|
+
// Remove any existing listeners to avoid conflicts
|
|
69
|
+
process.stdin.removeAllListeners('data');
|
|
70
|
+
process.stdin.removeAllListeners('end');
|
|
71
|
+
process.stdin.removeAllListeners('error');
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const chunks: Buffer[] = [];
|
|
75
|
+
|
|
76
|
+
const onData = (chunk: Buffer) => {
|
|
77
|
+
chunks.push(chunk);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onEnd = () => {
|
|
81
|
+
const result = Buffer.concat(chunks).toString('utf-8');
|
|
82
|
+
|
|
83
|
+
// Clean up listeners
|
|
84
|
+
process.stdin.removeListener('data', onData);
|
|
85
|
+
process.stdin.removeListener('end', onEnd);
|
|
86
|
+
process.stdin.removeListener('error', onError);
|
|
87
|
+
|
|
88
|
+
resolve(result);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onError = (err: Error) => {
|
|
92
|
+
// Clean up listeners
|
|
93
|
+
process.stdin.removeListener('data', onData);
|
|
94
|
+
process.stdin.removeListener('end', onEnd);
|
|
95
|
+
process.stdin.removeListener('error', onError);
|
|
96
|
+
|
|
97
|
+
reject(err);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
process.stdin.on('data', onData);
|
|
101
|
+
process.stdin.on('end', onEnd);
|
|
102
|
+
process.stdin.on('error', onError);
|
|
103
|
+
|
|
104
|
+
process.stdin.resume();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read one line from stdin
|
|
110
|
+
*/
|
|
111
|
+
let stdinReader: readline.Interface | null = null;
|
|
112
|
+
let stdinIterator: AsyncIterator<string> | null = null;
|
|
113
|
+
|
|
114
|
+
async function readLineStdin(): Promise<string> {
|
|
115
|
+
if (!stdinReader) {
|
|
116
|
+
stdinReader = readline.createInterface({
|
|
117
|
+
input: process.stdin,
|
|
118
|
+
output: process.stdout,
|
|
119
|
+
terminal: false
|
|
120
|
+
});
|
|
121
|
+
stdinIterator = stdinReader[Symbol.asyncIterator]();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = await stdinIterator!.next();
|
|
125
|
+
|
|
126
|
+
if (result.done) {
|
|
127
|
+
// EOF reached, close reader
|
|
128
|
+
stdinReader.close();
|
|
129
|
+
stdinReader = null;
|
|
130
|
+
stdinIterator = null;
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result.value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read one line from a file
|
|
139
|
+
*/
|
|
140
|
+
async function readLineFromFile(path: string): Promise<string> {
|
|
141
|
+
// Create reader if it doesn't exist for this file
|
|
142
|
+
if (!fileReaders.has(path)) {
|
|
143
|
+
const fileStream = fs.createReadStream(path);
|
|
144
|
+
const rl = readline.createInterface({
|
|
145
|
+
input: fileStream,
|
|
146
|
+
crlfDelay: Infinity
|
|
147
|
+
});
|
|
148
|
+
fileReaders.set(path, rl);
|
|
149
|
+
fileIterators.set(path, rl[Symbol.asyncIterator]());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const iterator = fileIterators.get(path)!;
|
|
153
|
+
const result = await iterator.next();
|
|
154
|
+
|
|
155
|
+
if (result.done) {
|
|
156
|
+
// EOF reached, close and cleanup
|
|
157
|
+
const reader = fileReaders.get(path)!;
|
|
158
|
+
reader.close();
|
|
159
|
+
fileReaders.delete(path);
|
|
160
|
+
fileIterators.delete(path);
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result.value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clean up all open file readers (can be called at end of session)
|
|
169
|
+
*/
|
|
170
|
+
export function cleanupInputReaders(): void {
|
|
171
|
+
if (stdinReader) {
|
|
172
|
+
stdinReader.close();
|
|
173
|
+
stdinReader = null;
|
|
174
|
+
stdinIterator = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const reader of fileReaders.values()) {
|
|
178
|
+
reader.close();
|
|
179
|
+
}
|
|
180
|
+
fileReaders.clear();
|
|
181
|
+
fileIterators.clear();
|
|
182
|
+
}
|
package/src/test-runner.ts
CHANGED
|
@@ -81,22 +81,96 @@ export class TestRunner {
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
// Check if there's a stdin input file for this test
|
|
85
|
+
const stdinFile = filePath.replace('.test.di', '.txt');
|
|
86
|
+
let stdinData: string | undefined;
|
|
87
|
+
if (fs.existsSync(stdinFile)) {
|
|
88
|
+
stdinData = fs.readFileSync(stdinFile, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If stdin data exists, we need to mock stdin
|
|
92
|
+
// Since we can't replace process.stdin directly, we'll use a workaround:
|
|
93
|
+
// temporarily store data in a place the input tag can access it
|
|
94
|
+
if (stdinData !== undefined) {
|
|
95
|
+
// Save original methods
|
|
96
|
+
const originalOn = process.stdin.on.bind(process.stdin);
|
|
97
|
+
const originalResume = process.stdin.resume.bind(process.stdin);
|
|
98
|
+
const originalRemoveAllListeners = process.stdin.removeAllListeners.bind(process.stdin);
|
|
99
|
+
const originalRemoveListener = process.stdin.removeListener.bind(process.stdin);
|
|
100
|
+
|
|
101
|
+
// Mock stdin methods
|
|
102
|
+
const listeners: { [key: string]: Function[] } = {};
|
|
103
|
+
|
|
104
|
+
(process.stdin as any).on = function(event: string, callback: Function) {
|
|
105
|
+
if (!listeners[event]) listeners[event] = [];
|
|
106
|
+
listeners[event].push(callback);
|
|
107
|
+
return this;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
(process.stdin as any).removeAllListeners = function(event?: string) {
|
|
111
|
+
if (event) {
|
|
112
|
+
delete listeners[event];
|
|
113
|
+
} else {
|
|
114
|
+
Object.keys(listeners).forEach(k => delete listeners[k]);
|
|
115
|
+
}
|
|
116
|
+
return this;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
(process.stdin as any).removeListener = function(event: string, callback: Function) {
|
|
120
|
+
if (listeners[event]) {
|
|
121
|
+
listeners[event] = listeners[event].filter(fn => fn !== callback);
|
|
122
|
+
}
|
|
123
|
+
return this;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
(process.stdin as any).resume = function() {
|
|
127
|
+
// Simulate data event
|
|
128
|
+
setImmediate(() => {
|
|
129
|
+
if (listeners['data']) {
|
|
130
|
+
listeners['data'].forEach(fn => fn(Buffer.from(stdinData!, 'utf-8')));
|
|
131
|
+
}
|
|
132
|
+
// Simulate end event
|
|
133
|
+
setImmediate(() => {
|
|
134
|
+
if (listeners['end']) {
|
|
135
|
+
listeners['end'].forEach(fn => fn());
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
return this;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const parser = new DiracParser();
|
|
144
|
+
const ast = parser.parse(cleanContent);
|
|
145
|
+
const session = createSession({ ...this.config, filePath });
|
|
146
|
+
await integrate(session, ast);
|
|
147
|
+
const output = getOutput(session);
|
|
148
|
+
result.actualOutput = output;
|
|
149
|
+
} finally {
|
|
150
|
+
// Restore original methods
|
|
151
|
+
(process.stdin as any).on = originalOn;
|
|
152
|
+
(process.stdin as any).resume = originalResume;
|
|
153
|
+
(process.stdin as any).removeAllListeners = originalRemoveAllListeners;
|
|
154
|
+
(process.stdin as any).removeListener = originalRemoveListener;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Normal test without stdin
|
|
158
|
+
const parser = new DiracParser();
|
|
159
|
+
const ast = parser.parse(cleanContent);
|
|
160
|
+
const session = createSession({ ...this.config, filePath });
|
|
161
|
+
await integrate(session, ast);
|
|
162
|
+
const output = getOutput(session);
|
|
163
|
+
result.actualOutput = output;
|
|
164
|
+
}
|
|
91
165
|
|
|
92
166
|
if (metadata.expectError) {
|
|
93
167
|
// Expected an error but got success
|
|
94
168
|
result.passed = false;
|
|
95
|
-
result.error = `Expected error but test succeeded. Output: ${
|
|
169
|
+
result.error = `Expected error but test succeeded. Output: ${result.actualOutput}`;
|
|
96
170
|
} else if (metadata.expected !== undefined) {
|
|
97
171
|
// Check if output matches expected
|
|
98
172
|
// Normalize whitespace for comparison (collapse multiple spaces/newlines)
|
|
99
|
-
const normalizedActual =
|
|
173
|
+
const normalizedActual = result.actualOutput!.replace(/\s+/g, ' ').trim();
|
|
100
174
|
const normalizedExpected = metadata.expected.replace(/\s+/g, ' ').trim();
|
|
101
175
|
|
|
102
176
|
if (normalizedActual === normalizedExpected) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<dirac>
|
|
2
|
+
<defvar name="counter" value="0" />
|
|
3
|
+
|
|
4
|
+
<loop count="4">
|
|
5
|
+
<defvar name="line">
|
|
6
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
7
|
+
</defvar>
|
|
8
|
+
|
|
9
|
+
<test-if test="$line != ''">
|
|
10
|
+
<assign name="counter">
|
|
11
|
+
<eval><variable name="counter" /> + 1</eval>
|
|
12
|
+
</assign>
|
|
13
|
+
<variable name="counter" />
|
|
14
|
+
<output>:</output>
|
|
15
|
+
<variable name="line" />
|
|
16
|
+
<output>|</output>
|
|
17
|
+
</test-if>
|
|
18
|
+
</loop>
|
|
19
|
+
</dirac>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- TEST: environment_basic -->
|
|
2
|
+
<!-- EXPECT: HOME exists: true USER exists: true -->
|
|
3
|
+
<dirac>
|
|
4
|
+
<defvar name="home"><environment name="HOME" /></defvar>
|
|
5
|
+
<defvar name="user"><environment name="USER" /></defvar>
|
|
6
|
+
|
|
7
|
+
<output>HOME exists: </output>
|
|
8
|
+
<test-if test="$home != ''">
|
|
9
|
+
<output>true</output>
|
|
10
|
+
</test-if>
|
|
11
|
+
<test-if test="$home == ''">
|
|
12
|
+
<output>false</output>
|
|
13
|
+
</test-if>
|
|
14
|
+
|
|
15
|
+
<output> USER exists: </output>
|
|
16
|
+
<test-if test="$user != ''">
|
|
17
|
+
<output>true</output>
|
|
18
|
+
</test-if>
|
|
19
|
+
<test-if test="$user == ''">
|
|
20
|
+
<output>false</output>
|
|
21
|
+
</test-if>
|
|
22
|
+
</dirac>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- TEST: environment_with_defvar -->
|
|
2
|
+
<!-- EXPECT: Path exists: true -->
|
|
3
|
+
<dirac>
|
|
4
|
+
<defvar name="mypath">
|
|
5
|
+
<environment name="PATH" />
|
|
6
|
+
</defvar>
|
|
7
|
+
|
|
8
|
+
<output>Path exists: </output>
|
|
9
|
+
<test-if test="$mypath != ''">
|
|
10
|
+
<output>true</output>
|
|
11
|
+
</test-if>
|
|
12
|
+
<test-if test="$mypath == ''">
|
|
13
|
+
<output>false</output>
|
|
14
|
+
</test-if>
|
|
15
|
+
</dirac>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- TEST: input_file_line -->
|
|
2
|
+
<!-- EXPECT: First: line one Second: line two Third: line three EOF: -->
|
|
3
|
+
<dirac>
|
|
4
|
+
<output>First: </output>
|
|
5
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
6
|
+
|
|
7
|
+
<output> Second: </output>
|
|
8
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
9
|
+
|
|
10
|
+
<output> Third: </output>
|
|
11
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
12
|
+
|
|
13
|
+
<output> EOF: </output>
|
|
14
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
15
|
+
</dirac>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- TEST: input_file_with_loop -->
|
|
2
|
+
<!-- EXPECT: line one | line two | line three | -->
|
|
3
|
+
<dirac>
|
|
4
|
+
<loop count="3">
|
|
5
|
+
<defvar name="line">
|
|
6
|
+
<input source="file" path="tests/test-input.txt" mode="line" />
|
|
7
|
+
</defvar>
|
|
8
|
+
|
|
9
|
+
<variable name="line" />
|
|
10
|
+
<output> | </output>
|
|
11
|
+
</loop>
|
|
12
|
+
</dirac>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hello from stdin
|