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.
@@ -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 };