webmystran-wasm 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 +22 -0
- package/PATCHING.md +358 -0
- package/README.md +177 -0
- package/SOURCES.md +48 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/dist/mystran.js +14 -0
- package/dist/mystran.wasm +0 -0
- package/index.js +3 -0
- package/package.json +54 -0
- package/scripts/build.ps1 +19 -0
- package/scripts/fetch_deps.ps1 +157 -0
- package/scripts/smoke_test.ps1 +19 -0
- package/src/mystran_input.js +117 -0
- package/src/output_parser.js +57 -0
- package/src/web_mystran.js +247 -0
- package/tools/build.js +4840 -0
- package/tools/smoke_test.js +978 -0
- package/tools/wasm_runner.js +349 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function toText(value) {
|
|
2
|
+
return value == null ? "" : String(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parseErrorCodes(text) {
|
|
6
|
+
const regex = /\*ERROR\s+([0-9A-Z]+)/gi;
|
|
7
|
+
const ordered = [];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
let match = regex.exec(text);
|
|
10
|
+
while (match) {
|
|
11
|
+
const code = String(match[1] || "").toUpperCase();
|
|
12
|
+
if (code && !seen.has(code)) {
|
|
13
|
+
seen.add(code);
|
|
14
|
+
ordered.push(code);
|
|
15
|
+
}
|
|
16
|
+
match = regex.exec(text);
|
|
17
|
+
}
|
|
18
|
+
return ordered;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseMystranOutput(raw = {}) {
|
|
22
|
+
const stdout = toText(raw.stdout);
|
|
23
|
+
const stderr = toText(raw.stderr);
|
|
24
|
+
const text = stdout && stderr ? `${stdout}\n${stderr}` : `${stdout}${stderr}`;
|
|
25
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
26
|
+
|
|
27
|
+
const errorCodes = parseErrorCodes(text);
|
|
28
|
+
const warningCount = (text.match(/\*WARNING/gi) || []).length;
|
|
29
|
+
|
|
30
|
+
const hasNormalTermination = /MYSTRAN terminated normally/i.test(text);
|
|
31
|
+
const hasRuntimeAbort =
|
|
32
|
+
/fatal Fortran runtime error/i.test(text) ||
|
|
33
|
+
/RuntimeError:\s*(Aborted|unreachable|memory access out of bounds|table index is out of bounds)/i.test(text) ||
|
|
34
|
+
/Aborted\(/i.test(text);
|
|
35
|
+
const hasSolverError =
|
|
36
|
+
errorCodes.length > 0 ||
|
|
37
|
+
/PROCESSING STOPPED/i.test(text) ||
|
|
38
|
+
/CHECK F06 OUTPUT FILE/i.test(text);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
text,
|
|
42
|
+
lines,
|
|
43
|
+
stdout,
|
|
44
|
+
stderr,
|
|
45
|
+
exitCode: Number.isInteger(raw.exitCode) ? raw.exitCode : null,
|
|
46
|
+
timedOut: Boolean(raw.timedOut),
|
|
47
|
+
signal: raw.signal || null,
|
|
48
|
+
hasNormalTermination,
|
|
49
|
+
hasRuntimeAbort,
|
|
50
|
+
hasSolverError,
|
|
51
|
+
warningCount,
|
|
52
|
+
errorCodes,
|
|
53
|
+
firstErrorCode: errorCodes.length > 0 ? errorCodes[0] : null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { parseMystranOutput };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { MystranInput } from "./mystran_input.js";
|
|
2
|
+
import { parseMystranOutput } from "./output_parser.js";
|
|
3
|
+
import { WasmRunner, joinPath, toPosixPath } from "../tools/wasm_runner.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OUTPUT_EXTENSIONS = [
|
|
6
|
+
"F06",
|
|
7
|
+
"F04",
|
|
8
|
+
"PCH",
|
|
9
|
+
"OP2",
|
|
10
|
+
"NEU",
|
|
11
|
+
"ANS",
|
|
12
|
+
"BUG",
|
|
13
|
+
"ERR"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function basenamePosix(pathValue) {
|
|
17
|
+
const normalized = toPosixPath(pathValue || "");
|
|
18
|
+
const idx = normalized.lastIndexOf("/");
|
|
19
|
+
if (idx < 0) {
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
return normalized.slice(idx + 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function dirnamePosix(pathValue) {
|
|
26
|
+
const normalized = toPosixPath(pathValue || "");
|
|
27
|
+
const idx = normalized.lastIndexOf("/");
|
|
28
|
+
if (idx <= 0) {
|
|
29
|
+
return "/";
|
|
30
|
+
}
|
|
31
|
+
return normalized.slice(0, idx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getDeckBaseName(deckPath) {
|
|
35
|
+
const deckName = basenamePosix(deckPath);
|
|
36
|
+
const dot = deckName.lastIndexOf(".");
|
|
37
|
+
if (dot > 0) {
|
|
38
|
+
return deckName.slice(0, dot);
|
|
39
|
+
}
|
|
40
|
+
return deckName;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveRunPath(workDir, targetPath) {
|
|
44
|
+
const raw = toPosixPath(String(targetPath || ""));
|
|
45
|
+
if (!raw) {
|
|
46
|
+
throw new TypeError("Expected a non-empty path.");
|
|
47
|
+
}
|
|
48
|
+
if (raw.startsWith("/")) {
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
return joinPath(workDir, raw);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findOutputPath(fs, caseDir, deckBaseName, extension) {
|
|
55
|
+
const normalizedExt = String(extension || "").trim().toUpperCase();
|
|
56
|
+
if (!normalizedExt) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const exact = joinPath(caseDir, `${deckBaseName}.${normalizedExt}`);
|
|
61
|
+
if (fs.analyzePath(exact).exists) {
|
|
62
|
+
return exact;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const escapedBase = deckBaseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
const escapedExt = normalizedExt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
const pattern = new RegExp(`^${escapedBase}\\..*${escapedExt}$`, "i");
|
|
68
|
+
|
|
69
|
+
for (const entry of fs.readdir(caseDir)) {
|
|
70
|
+
if (entry === "." || entry === "..") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (pattern.test(entry)) {
|
|
74
|
+
return joinPath(caseDir, entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectOutputs(fs, caseDir, deckBaseName, extensions) {
|
|
82
|
+
const byExtension = {};
|
|
83
|
+
const files = [];
|
|
84
|
+
|
|
85
|
+
for (const ext of extensions) {
|
|
86
|
+
const outputPath = findOutputPath(fs, caseDir, deckBaseName, ext);
|
|
87
|
+
if (!outputPath) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let size = 0;
|
|
91
|
+
try {
|
|
92
|
+
size = Number(fs.stat(outputPath).size || 0);
|
|
93
|
+
} catch {
|
|
94
|
+
size = 0;
|
|
95
|
+
}
|
|
96
|
+
const normalizedExt = String(ext).toUpperCase();
|
|
97
|
+
byExtension[normalizedExt] = outputPath;
|
|
98
|
+
files.push({
|
|
99
|
+
extension: normalizedExt,
|
|
100
|
+
name: basenamePosix(outputPath),
|
|
101
|
+
path: outputPath,
|
|
102
|
+
size
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { byExtension, files };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class WebMYSTRAN {
|
|
110
|
+
constructor(runner, loadOptions = null) {
|
|
111
|
+
this._runner = runner;
|
|
112
|
+
this._loadOptions = loadOptions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static async load(options = {}) {
|
|
116
|
+
const loadOptions = {
|
|
117
|
+
exportName: "WebmystranModule",
|
|
118
|
+
...options
|
|
119
|
+
};
|
|
120
|
+
const runner = await WasmRunner.load("mystran", loadOptions);
|
|
121
|
+
return new WebMYSTRAN(runner, loadOptions);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static input(lines) {
|
|
125
|
+
return new MystranInput(lines);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static get Input() {
|
|
129
|
+
return MystranInput;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get FS() {
|
|
133
|
+
return this._runner.FS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
input(lines) {
|
|
137
|
+
return new MystranInput(lines);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
writeFile(path, data) {
|
|
141
|
+
this._runner.writeFile(path, data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
readFile(path, encoding = "utf8") {
|
|
145
|
+
return this._runner.readFile(path, encoding);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run(deckText, options = {}) {
|
|
149
|
+
const workDir = toPosixPath(options.workDir || "/work");
|
|
150
|
+
const deckFileName = String(options.deckFileName || "WEBMYSTRAN.DAT");
|
|
151
|
+
const deckPath = resolveRunPath(workDir, deckFileName);
|
|
152
|
+
|
|
153
|
+
const files = Array.isArray(options.files) ? options.files.slice() : [];
|
|
154
|
+
if (deckText != null) {
|
|
155
|
+
files.push({
|
|
156
|
+
path: deckPath,
|
|
157
|
+
data: deckText
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const stdinText =
|
|
162
|
+
options.stdinText == null ? basenamePosix(deckPath) : String(options.stdinText);
|
|
163
|
+
|
|
164
|
+
const rawRunner = this._runner.runWithStdin(stdinText, {
|
|
165
|
+
workDir,
|
|
166
|
+
files,
|
|
167
|
+
args: Array.isArray(options.args) ? options.args.slice() : []
|
|
168
|
+
});
|
|
169
|
+
const raw = {
|
|
170
|
+
stdout: rawRunner.stdout,
|
|
171
|
+
stderr: rawRunner.stderr,
|
|
172
|
+
exitCode: rawRunner.exitCode,
|
|
173
|
+
signal: null,
|
|
174
|
+
timedOut: false
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const parseOptions = options.parseOptions && typeof options.parseOptions === "object"
|
|
178
|
+
? options.parseOptions
|
|
179
|
+
: {};
|
|
180
|
+
const output = parseMystranOutput(raw, parseOptions);
|
|
181
|
+
|
|
182
|
+
const outputExtensions = Array.isArray(options.outputExtensions) && options.outputExtensions.length > 0
|
|
183
|
+
? options.outputExtensions
|
|
184
|
+
: DEFAULT_OUTPUT_EXTENSIONS;
|
|
185
|
+
const deckBaseName = getDeckBaseName(deckPath);
|
|
186
|
+
const caseDir = dirnamePosix(deckPath);
|
|
187
|
+
const outputs = collectOutputs(this._runner.FS, caseDir, deckBaseName, outputExtensions);
|
|
188
|
+
|
|
189
|
+
const f06Path = outputs.byExtension.F06;
|
|
190
|
+
if (f06Path) {
|
|
191
|
+
try {
|
|
192
|
+
const f06Text = this._runner.readFile(f06Path, "utf8");
|
|
193
|
+
output.f06HasNormalTermination = /MYSTRAN terminated normally/i.test(f06Text);
|
|
194
|
+
output.f06HasMystranEnd = /MYSTRAN END/i.test(f06Text);
|
|
195
|
+
if (output.f06HasNormalTermination) {
|
|
196
|
+
output.hasNormalTermination = true;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
output.f06HasNormalTermination = false;
|
|
200
|
+
output.f06HasMystranEnd = false;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
output.f06HasNormalTermination = false;
|
|
204
|
+
output.f06HasMystranEnd = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
workDir,
|
|
209
|
+
deckFileName: basenamePosix(deckPath),
|
|
210
|
+
deckPath,
|
|
211
|
+
raw,
|
|
212
|
+
output,
|
|
213
|
+
outputs: outputs.byExtension,
|
|
214
|
+
files: outputs.files
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
readOutput(result, extension, encoding = "utf8") {
|
|
219
|
+
if (!result || !result.outputs) {
|
|
220
|
+
throw new TypeError("readOutput expects a run result with outputs.");
|
|
221
|
+
}
|
|
222
|
+
const normalized = String(extension || "").toUpperCase();
|
|
223
|
+
const outputPath = result.outputs[normalized];
|
|
224
|
+
if (!outputPath) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return this._runner.readFile(outputPath, encoding);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async reset() {
|
|
231
|
+
if (!this._loadOptions) {
|
|
232
|
+
throw new Error("Cannot reset without load options.");
|
|
233
|
+
}
|
|
234
|
+
if (this._runner && typeof this._runner.destroy === "function") {
|
|
235
|
+
this._runner.destroy();
|
|
236
|
+
}
|
|
237
|
+
this._runner = await WasmRunner.load("mystran", this._loadOptions);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
destroy() {
|
|
241
|
+
if (this._runner && typeof this._runner.destroy === "function") {
|
|
242
|
+
this._runner.destroy();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { WebMYSTRAN };
|