@specy/x86 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +39 -0
- package/dist/index.d.mts +387 -231
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +605 -129
- package/dist/index.mjs.map +1 -1
- package/dist/wasm/nasm.d.mts +47 -0
- package/dist/wasm/nasm.mjs +2 -0
- package/dist/wasm/nasm.wasm +0 -0
- package/package.json +2 -1
- package/dist/assets/assemblers/nasm.3.00.elf +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,245 @@
|
|
|
1
|
+
import createNasm from "./wasm/nasm.mjs";
|
|
1
2
|
import blinkenlib from "./wasm/blinkenlib.js";
|
|
3
|
+
//#region src/project.ts
|
|
4
|
+
const X86_PROJECT_ROOT = "/__x86_project";
|
|
5
|
+
const stagedFiles = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
function validateX86Project(project) {
|
|
7
|
+
if (!isProjectPath(project.entry)) throw new Error(`Invalid x86 Project entry path: ${project.entry}`);
|
|
8
|
+
if (!Object.hasOwn(project.files, project.entry)) throw new Error(`x86 Project entry file not found: ${project.entry}`);
|
|
9
|
+
if (typeof project.files[project.entry] !== "string") throw new Error(`x86 Project entry must be a text file: ${project.entry}`);
|
|
10
|
+
for (const [path, contents] of Object.entries(project.files)) {
|
|
11
|
+
if (!isProjectPath(path)) throw new Error(`Invalid x86 Project file path: ${path}`);
|
|
12
|
+
if (typeof contents !== "string" && !(contents instanceof Uint8Array)) throw new Error(`Invalid x86 Project file contents: ${path}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes a Project into the emulator filesystem and selects the Entry directory as NASM's working
|
|
17
|
+
* directory. NASM consequently owns `%include` and `incbin` resolution, including paths produced by
|
|
18
|
+
* preprocessor macros.
|
|
19
|
+
*/
|
|
20
|
+
function stageX86Project(fs, project) {
|
|
21
|
+
validateX86Project(project);
|
|
22
|
+
const previous = stagedFiles.get(fs) ?? /* @__PURE__ */ new Set();
|
|
23
|
+
const current = /* @__PURE__ */ new Set();
|
|
24
|
+
fs.mkdirTree(X86_PROJECT_ROOT);
|
|
25
|
+
for (const [path, contents] of Object.entries(project.files)) {
|
|
26
|
+
const absolutePath = `${X86_PROJECT_ROOT}/${path}`;
|
|
27
|
+
const separator = absolutePath.lastIndexOf("/");
|
|
28
|
+
if (separator > 0) fs.mkdirTree(absolutePath.slice(0, separator));
|
|
29
|
+
fs.writeFile(absolutePath, contents);
|
|
30
|
+
current.add(absolutePath);
|
|
31
|
+
}
|
|
32
|
+
for (const path of previous) {
|
|
33
|
+
if (current.has(path)) continue;
|
|
34
|
+
try {
|
|
35
|
+
fs.unlink(path);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
fs.writeFile("/assembly.s", project.files[project.entry]);
|
|
39
|
+
fs.chdir(projectDirectory(project.entry));
|
|
40
|
+
stagedFiles.set(fs, current);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Points the assembler at one staged Project File and leaves that File's own directory as the
|
|
44
|
+
* working directory, so its `%include`s and `incbin`s resolve from where it was written rather
|
|
45
|
+
* than from wherever the Entry happens to live. The Entry keeps its `/assembly.s` identity, which
|
|
46
|
+
* is the path its existing diagnostics and source maps are written in terms of.
|
|
47
|
+
*/
|
|
48
|
+
function selectX86Source(fs, project, path) {
|
|
49
|
+
fs.chdir(projectDirectory(path));
|
|
50
|
+
return path === project.entry ? "/assembly.s" : `${X86_PROJECT_ROOT}/${path}`;
|
|
51
|
+
}
|
|
52
|
+
/** Maps a path emitted by NASM or DWARF back to the exact Project path. */
|
|
53
|
+
function x86ProjectSourcePath(sourcePath, project) {
|
|
54
|
+
if (!sourcePath || sourcePath === "/assembly.s" || sourcePath === "assembly.s") return project.entry;
|
|
55
|
+
const withoutLeadingSlash = (sourcePath.startsWith(`/__x86_project/`) ? sourcePath.slice(15) : sourcePath).replace(/^\/+/, "");
|
|
56
|
+
const relativeToEntry = resolvePath(parentPath(project.entry), withoutLeadingSlash);
|
|
57
|
+
if (relativeToEntry && relativeToEntry in project.files) return relativeToEntry;
|
|
58
|
+
const relativeToRoot = resolvePath("", withoutLeadingSlash);
|
|
59
|
+
if (relativeToRoot && relativeToRoot in project.files) return relativeToRoot;
|
|
60
|
+
const suffix = `/${withoutLeadingSlash}`;
|
|
61
|
+
const suffixMatches = Object.keys(project.files).filter((path) => path === withoutLeadingSlash || path.endsWith(suffix));
|
|
62
|
+
return suffixMatches.length === 1 ? suffixMatches[0] : project.entry;
|
|
63
|
+
}
|
|
64
|
+
function projectDirectory(entry) {
|
|
65
|
+
const parent = parentPath(entry);
|
|
66
|
+
return parent ? `${X86_PROJECT_ROOT}/${parent}` : X86_PROJECT_ROOT;
|
|
67
|
+
}
|
|
68
|
+
function parentPath(path) {
|
|
69
|
+
const separator = path.lastIndexOf("/");
|
|
70
|
+
return separator < 0 ? "" : path.slice(0, separator);
|
|
71
|
+
}
|
|
72
|
+
function isProjectPath(path) {
|
|
73
|
+
return typeof path === "string" && path.length > 0 && !path.includes("\\") && !path.startsWith("/") && path.split("/").every((part) => part !== "" && part !== "." && part !== "..") && !Array.from(path).some((character) => {
|
|
74
|
+
const code = character.charCodeAt(0);
|
|
75
|
+
return code <= 31 || code >= 127 && code <= 159;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function resolvePath(base, written) {
|
|
79
|
+
const parts = base ? base.split("/") : [];
|
|
80
|
+
for (const part of written.split("/")) {
|
|
81
|
+
if (!part || part === ".") continue;
|
|
82
|
+
if (part === "..") {
|
|
83
|
+
if (!parts.length) return null;
|
|
84
|
+
parts.pop();
|
|
85
|
+
} else parts.push(part);
|
|
86
|
+
}
|
|
87
|
+
return parts.join("/") || null;
|
|
88
|
+
}
|
|
89
|
+
function x86ProjectText(project, path) {
|
|
90
|
+
const contents = project.files[path];
|
|
91
|
+
return typeof contents === "string" ? contents : "";
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/translation-units.ts
|
|
95
|
+
/**
|
|
96
|
+
* Extensions that make a Project File assembler input in its own right. `.inc` is deliberately
|
|
97
|
+
* absent: it is what an include fragment is conventionally called, and a fragment assembled on
|
|
98
|
+
* its own would define its symbols a second time. A fragment under any other name is caught by
|
|
99
|
+
* the dependency scan instead.
|
|
100
|
+
*/
|
|
101
|
+
const SOURCE_EXTENSIONS = [
|
|
102
|
+
".asm",
|
|
103
|
+
".s",
|
|
104
|
+
".nasm"
|
|
105
|
+
];
|
|
106
|
+
function isX86Source(path) {
|
|
107
|
+
const lower = path.toLowerCase();
|
|
108
|
+
return SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every File that could be its own translation unit: assembled separately and linked with the
|
|
112
|
+
* rest, which is what makes `global` in one File and `extern` in another resolve to each other.
|
|
113
|
+
* The Entry comes first so it leads the link, and the rest follow in a stable order so two builds
|
|
114
|
+
* of the same Project lay out identically.
|
|
115
|
+
*/
|
|
116
|
+
function x86TranslationUnitCandidates(project) {
|
|
117
|
+
const others = Object.keys(project.files).filter((path) => path !== project.entry).filter((path) => typeof project.files[path] === "string" && isX86Source(path)).sort();
|
|
118
|
+
return [project.entry, ...others];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The Files named by one `nasm -M` run, as Project paths. A File that another source `%include`s
|
|
122
|
+
* is already part of that source: assembling it again would define every one of its symbols a
|
|
123
|
+
* second time, so the link would fail on duplicate definitions rather than on anything the author
|
|
124
|
+
* wrote. Asking NASM rather than scanning for `%include` ourselves is what keeps macro-generated
|
|
125
|
+
* and conditional includes right, since NASM is the only thing that knows which ones it took.
|
|
126
|
+
*/
|
|
127
|
+
function parseDependencyList(output, project) {
|
|
128
|
+
const separator = output.indexOf(":");
|
|
129
|
+
if (separator < 0) return [];
|
|
130
|
+
return output.slice(separator + 1).replace(/\\\r?\n/g, " ").split(/\s+/).filter(Boolean).map((written) => x86ProjectSourcePath(written, project));
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/wasm-assembler.ts
|
|
134
|
+
const OBJECT_PATH = "/program.o";
|
|
135
|
+
/**
|
|
136
|
+
* `-g -F dwarf` is what makes the source map possible: the debugger reads
|
|
137
|
+
* `.debug_line` out of the linked executable to map an address back to the line
|
|
138
|
+
* it was written on. The working directory `selectX86Source` selects is what
|
|
139
|
+
* lets NASM resolve `%include` itself.
|
|
140
|
+
*/
|
|
141
|
+
const NASM_ARGS = [
|
|
142
|
+
"-g",
|
|
143
|
+
"-F",
|
|
144
|
+
"dwarf",
|
|
145
|
+
"-felf64"
|
|
146
|
+
];
|
|
147
|
+
async function createNasmModule() {
|
|
148
|
+
let stdout = "";
|
|
149
|
+
let stderr = "";
|
|
150
|
+
return {
|
|
151
|
+
module: await createNasm({
|
|
152
|
+
noInitialRun: true,
|
|
153
|
+
print: (line) => {
|
|
154
|
+
stdout += `${line}\n`;
|
|
155
|
+
},
|
|
156
|
+
printErr: (line) => {
|
|
157
|
+
stderr += `${line}\n`;
|
|
158
|
+
}
|
|
159
|
+
}),
|
|
160
|
+
stdout: () => stdout,
|
|
161
|
+
stderr: () => stderr
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Emscripten's Node path sets `process.exitCode` from the status the module
|
|
166
|
+
* exits with, so an assembly that failed would leave the host process exiting
|
|
167
|
+
* non-zero for a reason of its own. NASM's status belongs to the caller of this
|
|
168
|
+
* function, never to the host.
|
|
169
|
+
*/
|
|
170
|
+
function callNasm(module, args) {
|
|
171
|
+
const host = globalThis.process;
|
|
172
|
+
const hostExitCode = host?.exitCode;
|
|
173
|
+
const status = module.callMain(args);
|
|
174
|
+
if (host) host.exitCode = hostExitCode;
|
|
175
|
+
return status;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Which Files the Project's candidate translation units `%include`, so that a fragment is
|
|
179
|
+
* assembled as part of whatever includes it and never a second time on its own. Every candidate
|
|
180
|
+
* is scanned before any of them is treated as a unit, because a fragment can be included by a
|
|
181
|
+
* File that sorts after it.
|
|
182
|
+
*/
|
|
183
|
+
async function scanIncludedFiles(project, candidates) {
|
|
184
|
+
const included = /* @__PURE__ */ new Set();
|
|
185
|
+
if (candidates.length < 2) return included;
|
|
186
|
+
for (const path of candidates) {
|
|
187
|
+
const { module, stdout } = await createNasmModule();
|
|
188
|
+
stageX86Project(module.FS, project);
|
|
189
|
+
const source = selectX86Source(module.FS, project, path);
|
|
190
|
+
callNasm(module, [
|
|
191
|
+
"-M",
|
|
192
|
+
...NASM_ARGS,
|
|
193
|
+
source,
|
|
194
|
+
"-o",
|
|
195
|
+
OBJECT_PATH
|
|
196
|
+
]);
|
|
197
|
+
for (const dependency of parseDependencyList(stdout(), project)) if (dependency !== path) included.add(dependency);
|
|
198
|
+
}
|
|
199
|
+
return included;
|
|
200
|
+
}
|
|
201
|
+
const nasmWasmAssembler = { async assemble(project) {
|
|
202
|
+
const candidates = x86TranslationUnitCandidates(project);
|
|
203
|
+
const included = await scanIncludedFiles(project, candidates);
|
|
204
|
+
const units = candidates.filter((path) => path === project.entry || !included.has(path));
|
|
205
|
+
let stdout = "";
|
|
206
|
+
let stderr = "";
|
|
207
|
+
let status = 0;
|
|
208
|
+
const assembled = [];
|
|
209
|
+
for (const path of units) {
|
|
210
|
+
const { module, stdout: readStdout, stderr: readStderr } = await createNasmModule();
|
|
211
|
+
stageX86Project(module.FS, project);
|
|
212
|
+
const source = selectX86Source(module.FS, project, path);
|
|
213
|
+
const unitStatus = callNasm(module, [
|
|
214
|
+
...NASM_ARGS,
|
|
215
|
+
source,
|
|
216
|
+
"-o",
|
|
217
|
+
OBJECT_PATH
|
|
218
|
+
]);
|
|
219
|
+
stdout += readStdout();
|
|
220
|
+
stderr += readStderr();
|
|
221
|
+
if (!(unitStatus === 0 && module.FS.analyzePath(OBJECT_PATH).exists)) {
|
|
222
|
+
if (status === 0) status = unitStatus === 0 ? 1 : unitStatus;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
assembled.push({
|
|
226
|
+
path,
|
|
227
|
+
object: module.FS.readFile(OBJECT_PATH)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const ok = status === 0 && assembled.length === units.length;
|
|
231
|
+
return {
|
|
232
|
+
status,
|
|
233
|
+
stdout,
|
|
234
|
+
stderr,
|
|
235
|
+
units: ok ? assembled : [],
|
|
236
|
+
object: ok ? assembled[0]?.object ?? null : null
|
|
237
|
+
};
|
|
238
|
+
} };
|
|
239
|
+
//#endregion
|
|
2
240
|
//#region src/assemblers.ts
|
|
3
241
|
const assemblerAsset = (filename) => new URL(`./assets/assemblers/${filename}`, import.meta.url);
|
|
242
|
+
const linkCommand = (objects) => `/linker ${objects.join(" ")} -o /program`;
|
|
4
243
|
const assemblers = {
|
|
5
244
|
GNU_trunk: {
|
|
6
245
|
id: "GNU_trunk",
|
|
@@ -14,7 +253,7 @@ const assemblers = {
|
|
|
14
253
|
},
|
|
15
254
|
linker: {
|
|
16
255
|
file: assemblerAsset("gnu-ld.2.43.50.elf"),
|
|
17
|
-
|
|
256
|
+
link: linkCommand
|
|
18
257
|
}
|
|
19
258
|
}
|
|
20
259
|
},
|
|
@@ -31,18 +270,13 @@ const assemblers = {
|
|
|
31
270
|
NASM_trunk: {
|
|
32
271
|
id: "NASM_trunk",
|
|
33
272
|
displayName: "Nasm",
|
|
34
|
-
description: "NASM 3.00 + GNU ld v2.43.50
|
|
273
|
+
description: "NASM 3.00 compiled to WebAssembly, + GNU ld v2.43.50 as a static MUSL binary",
|
|
35
274
|
diagnosticsParser: nasmDiagnostics,
|
|
36
|
-
binaries: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
linker: {
|
|
42
|
-
file: assemblerAsset("gnu-ld.2.43.50.elf"),
|
|
43
|
-
commands: "/linker /program.o -o /program"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
275
|
+
binaries: { linker: {
|
|
276
|
+
file: assemblerAsset("gnu-ld.2.43.50.elf"),
|
|
277
|
+
link: linkCommand
|
|
278
|
+
} },
|
|
279
|
+
wasmAssembler: nasmWasmAssembler
|
|
46
280
|
}
|
|
47
281
|
};
|
|
48
282
|
const DEFAULT_ASSEMBLER_ID = "NASM_trunk";
|
|
@@ -50,12 +284,18 @@ function nasmDiagnostics(str) {
|
|
|
50
284
|
const diagnostics = [];
|
|
51
285
|
const lines = str.split(/\r?\n/);
|
|
52
286
|
const regex = /^(.*):(\d+): (error|warning): (.*)$/;
|
|
287
|
+
const classRegex = /\s*\[-w\+([\w-]+)\]$/;
|
|
53
288
|
for (const line of lines) {
|
|
54
289
|
const match = line.match(regex);
|
|
55
|
-
if (match)
|
|
290
|
+
if (!match) continue;
|
|
291
|
+
const text = (match[4] ?? "").trim();
|
|
292
|
+
const classMatch = text.match(classRegex);
|
|
293
|
+
diagnostics.push({
|
|
56
294
|
file: match[1] || void 0,
|
|
57
295
|
line: Number.parseInt(match[2] ?? "0", 10),
|
|
58
|
-
error: (
|
|
296
|
+
error: classMatch ? text.slice(0, classMatch.index).trim() : text,
|
|
297
|
+
severity: match[3] === "warning" ? "warning" : "error",
|
|
298
|
+
...classMatch?.[1] ? { warningClass: classMatch[1] } : {}
|
|
59
299
|
});
|
|
60
300
|
}
|
|
61
301
|
return diagnostics;
|
|
@@ -63,13 +303,15 @@ function nasmDiagnostics(str) {
|
|
|
63
303
|
function gnuDiagnostics(str) {
|
|
64
304
|
const diagnostics = [];
|
|
65
305
|
const lines = str.split("\n");
|
|
66
|
-
const regex = /^(.*):(\d+): (Error: .+)$/;
|
|
306
|
+
const regex = /^(.*):(\d+): (Error|Warning): (.+)$/;
|
|
67
307
|
for (const line of lines) {
|
|
68
308
|
const match = line.match(regex);
|
|
69
|
-
if (match)
|
|
309
|
+
if (!match) continue;
|
|
310
|
+
diagnostics.push({
|
|
70
311
|
file: match[1] || void 0,
|
|
71
312
|
line: Number.parseInt(match[2] ?? "0", 10),
|
|
72
|
-
error: match[3]
|
|
313
|
+
error: `${match[3]}: ${match[4]}`,
|
|
314
|
+
severity: match[3] === "Warning" ? "warning" : "error"
|
|
73
315
|
});
|
|
74
316
|
}
|
|
75
317
|
return diagnostics;
|
|
@@ -92,7 +334,8 @@ function fasmDiagnostics(str) {
|
|
|
92
334
|
diagnostics.push({
|
|
93
335
|
file,
|
|
94
336
|
line: lineNumber,
|
|
95
|
-
error: errorMatch[0]
|
|
337
|
+
error: errorMatch[0],
|
|
338
|
+
severity: "error"
|
|
96
339
|
});
|
|
97
340
|
lineNumber = null;
|
|
98
341
|
file = void 0;
|
|
@@ -100,6 +343,71 @@ function fasmDiagnostics(str) {
|
|
|
100
343
|
}
|
|
101
344
|
return diagnostics;
|
|
102
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* What `ld` said, as diagnostics. Nothing else in the toolchain reports a symbol that no
|
|
348
|
+
* translation unit defines: NASM accepts every `extern` on the author's word, and the mistake
|
|
349
|
+
* only surfaces here. Without this the link simply failed and the build looked like it had
|
|
350
|
+
* succeeded, leaving the program unrunnable and nothing on screen to say why.
|
|
351
|
+
*
|
|
352
|
+
* `ld` writes a location as `file:line:(section+offset):`, and writes the notes that introduce an
|
|
353
|
+
* error - "in function `_start':" - as lines ending in a colon, which are skipped in favour of
|
|
354
|
+
* the error itself.
|
|
355
|
+
*/
|
|
356
|
+
function ldDiagnostics(str) {
|
|
357
|
+
const diagnostics = [];
|
|
358
|
+
const locatedRegex = /^(.*?):(\d+):(?:\([^)]*\))?:\s*(.+)$/;
|
|
359
|
+
for (const line of str.split(/\r?\n/)) {
|
|
360
|
+
const text = line.trim().replace(/^\/linker:\s*/, "");
|
|
361
|
+
if (!text || text.startsWith("$ ") || text.endsWith(":")) continue;
|
|
362
|
+
const located = text.match(locatedRegex);
|
|
363
|
+
const message = (located?.[3] ?? text).trim();
|
|
364
|
+
if (!message) continue;
|
|
365
|
+
if (message.includes("cannot find entry symbol")) continue;
|
|
366
|
+
const isWarning = message.startsWith("warning:");
|
|
367
|
+
diagnostics.push({
|
|
368
|
+
...located?.[1] ? { file: located[1] } : {},
|
|
369
|
+
line: located ? Number.parseInt(located[2] ?? "0", 10) : 1,
|
|
370
|
+
error: isWarning ? message.slice(8).trim() : message,
|
|
371
|
+
severity: isWarning ? "warning" : "error"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return diagnostics;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The one-based column a diagnostic belongs on. NASM reports a line and no
|
|
378
|
+
* column, but it names the thing it is complaining about: `msg' in "symbol
|
|
379
|
+
* `msg' not defined", `.data' in a section warning. Finding that name in the
|
|
380
|
+
* line turns a whole-line squiggle into one under the mistake itself.
|
|
381
|
+
*
|
|
382
|
+
* Falls back to the first column, which is what the caller would have used
|
|
383
|
+
* anyway, so a message that names nothing findable is no worse off.
|
|
384
|
+
*/
|
|
385
|
+
function locateDiagnosticColumn(message, line, warningClass) {
|
|
386
|
+
if (warningClass === "label-orphan") {
|
|
387
|
+
const leading = line.search(/\S/);
|
|
388
|
+
return leading < 0 ? 1 : leading + 1;
|
|
389
|
+
}
|
|
390
|
+
const quoted = message.match(/`([^'`]+)'/)?.[1];
|
|
391
|
+
if (!quoted) return 1;
|
|
392
|
+
const index = findIdentifier(line, quoted);
|
|
393
|
+
return index < 0 ? 1 : index + 1;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Where a name appears in a line as itself, rather than inside a longer one.
|
|
397
|
+
* Identifier characters on either side mean this is a different symbol that
|
|
398
|
+
* merely contains the name.
|
|
399
|
+
*/
|
|
400
|
+
function findIdentifier(line, name) {
|
|
401
|
+
const isIdentifierChar = (character) => character !== void 0 && /[\w$#@~.?]/.test(character);
|
|
402
|
+
let from = 0;
|
|
403
|
+
while (from <= line.length - name.length) {
|
|
404
|
+
const index = line.indexOf(name, from);
|
|
405
|
+
if (index < 0) return -1;
|
|
406
|
+
if (!isIdentifierChar(line[index - 1]) && !isIdentifierChar(line[index + name.length])) return index;
|
|
407
|
+
from = index + 1;
|
|
408
|
+
}
|
|
409
|
+
return -1;
|
|
410
|
+
}
|
|
103
411
|
//#endregion
|
|
104
412
|
//#region \0wasm-helpers.js
|
|
105
413
|
function loadWasmModule(sync, filepath, src, imports) {
|
|
@@ -470,85 +778,73 @@ function compareBigInt(left, right) {
|
|
|
470
778
|
return 0;
|
|
471
779
|
}
|
|
472
780
|
//#endregion
|
|
473
|
-
//#region src/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
781
|
+
//#region src/elf-symbols.ts
|
|
782
|
+
/**
|
|
783
|
+
* The entry symbol `ld` looks for when no `-e` is given. A program without it
|
|
784
|
+
* still links: `ld` warns, defaults the entry address to the start of the text
|
|
785
|
+
* segment, and the program runs from whatever happens to be there.
|
|
786
|
+
*/
|
|
787
|
+
const DEFAULT_ENTRY_SYMBOL = "_start";
|
|
788
|
+
const SYMBOL_ENTRY_SIZE = 24;
|
|
789
|
+
const SHN_UNDEF = 0;
|
|
790
|
+
const STB_LOCAL = 0;
|
|
791
|
+
/**
|
|
792
|
+
* Names every symbol an object file defines and exports, which is what decides
|
|
793
|
+
* whether the linker can find an entry point. A symbol that is merely referenced
|
|
794
|
+
* (`st_shndx` of SHN_UNDEF) is not defined here, and a local one - a label
|
|
795
|
+
* without a matching `global` directive - is invisible to the linker.
|
|
796
|
+
*/
|
|
797
|
+
function readDefinedGlobalSymbols(objectBytes) {
|
|
798
|
+
const symbolTable = findElfSection(objectBytes, ".symtab");
|
|
799
|
+
const stringTable = findElfSection(objectBytes, ".strtab");
|
|
800
|
+
if (!symbolTable || !stringTable) return [];
|
|
801
|
+
const view = new DataView(objectBytes.buffer, objectBytes.byteOffset, objectBytes.byteLength);
|
|
802
|
+
const strings = objectBytes.subarray(stringTable.offset, stringTable.offset + stringTable.size);
|
|
803
|
+
const names = [];
|
|
804
|
+
for (let offset = 0; offset + SYMBOL_ENTRY_SIZE <= symbolTable.size; offset += SYMBOL_ENTRY_SIZE) {
|
|
805
|
+
const entry = symbolTable.offset + offset;
|
|
806
|
+
const nameOffset = view.getUint32(entry, true);
|
|
807
|
+
const binding = view.getUint8(entry + 4) >> 4;
|
|
808
|
+
const sectionIndex = view.getUint16(entry + 6, true);
|
|
809
|
+
if (binding === STB_LOCAL || sectionIndex === SHN_UNDEF) continue;
|
|
810
|
+
if (nameOffset >= strings.length) continue;
|
|
811
|
+
const [name] = readNullTerminatedString(strings, nameOffset, strings.length);
|
|
812
|
+
if (name) names.push(name);
|
|
813
|
+
}
|
|
814
|
+
return names;
|
|
484
815
|
}
|
|
485
816
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
817
|
+
* The symbol a misspelling was probably meant to be, or null when nothing in the
|
|
818
|
+
* object looks close enough to say. Deliberately strict: naming the wrong symbol
|
|
819
|
+
* is worse than naming none, so this only answers for a small edit distance on a
|
|
820
|
+
* name of a similar length.
|
|
489
821
|
*/
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
for (const path of previous) {
|
|
503
|
-
if (current.has(path)) continue;
|
|
504
|
-
try {
|
|
505
|
-
fs.unlink(path);
|
|
506
|
-
} catch {}
|
|
822
|
+
function findNearestSymbol(target, candidates) {
|
|
823
|
+
let best = null;
|
|
824
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
825
|
+
for (const candidate of candidates) {
|
|
826
|
+
if (candidate === target) return null;
|
|
827
|
+
if (Math.abs(candidate.length - target.length) > 2) continue;
|
|
828
|
+
const distance = editDistance(candidate, target);
|
|
829
|
+
if (distance < bestDistance) {
|
|
830
|
+
bestDistance = distance;
|
|
831
|
+
best = candidate;
|
|
832
|
+
}
|
|
507
833
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
stagedFiles.set(fs, current);
|
|
511
|
-
}
|
|
512
|
-
/** Maps a path emitted by NASM or DWARF back to the exact Project path. */
|
|
513
|
-
function x86ProjectSourcePath(sourcePath, project) {
|
|
514
|
-
if (!sourcePath || sourcePath === "/assembly.s" || sourcePath === "assembly.s") return project.entry;
|
|
515
|
-
const withoutLeadingSlash = (sourcePath.startsWith(`/__x86_project/`) ? sourcePath.slice(15) : sourcePath).replace(/^\/+/, "");
|
|
516
|
-
const relativeToEntry = resolvePath(parentPath(project.entry), withoutLeadingSlash);
|
|
517
|
-
if (relativeToEntry && relativeToEntry in project.files) return relativeToEntry;
|
|
518
|
-
const relativeToRoot = resolvePath("", withoutLeadingSlash);
|
|
519
|
-
if (relativeToRoot && relativeToRoot in project.files) return relativeToRoot;
|
|
520
|
-
const suffix = `/${withoutLeadingSlash}`;
|
|
521
|
-
const suffixMatches = Object.keys(project.files).filter((path) => path === withoutLeadingSlash || path.endsWith(suffix));
|
|
522
|
-
return suffixMatches.length === 1 ? suffixMatches[0] : project.entry;
|
|
523
|
-
}
|
|
524
|
-
function projectDirectory(entry) {
|
|
525
|
-
const parent = parentPath(entry);
|
|
526
|
-
return parent ? `${X86_PROJECT_ROOT}/${parent}` : X86_PROJECT_ROOT;
|
|
834
|
+
const allowed = target.length <= 4 ? 1 : 2;
|
|
835
|
+
return bestDistance <= allowed ? best : null;
|
|
527
836
|
}
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
function resolvePath(base, written) {
|
|
539
|
-
const parts = base ? base.split("/") : [];
|
|
540
|
-
for (const part of written.split("/")) {
|
|
541
|
-
if (!part || part === ".") continue;
|
|
542
|
-
if (part === "..") {
|
|
543
|
-
if (!parts.length) return null;
|
|
544
|
-
parts.pop();
|
|
545
|
-
} else parts.push(part);
|
|
837
|
+
function editDistance(left, right) {
|
|
838
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
839
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
840
|
+
const current = [leftIndex];
|
|
841
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
842
|
+
const substitution = previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
843
|
+
current.push(Math.min(substitution, previous[rightIndex] + 1, current[rightIndex - 1] + 1));
|
|
844
|
+
}
|
|
845
|
+
previous = current;
|
|
546
846
|
}
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
function x86ProjectText(project, path) {
|
|
550
|
-
const contents = project.files[path];
|
|
551
|
-
return typeof contents === "string" ? contents : "";
|
|
847
|
+
return previous[right.length];
|
|
552
848
|
}
|
|
553
849
|
//#endregion
|
|
554
850
|
//#region src/types.ts
|
|
@@ -682,13 +978,20 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
682
978
|
state = "NOT_READY";
|
|
683
979
|
stopReason = null;
|
|
684
980
|
assemblerLogs = "";
|
|
981
|
+
/** The subset of `assemblerDiagnostics` that stopped the build. */
|
|
685
982
|
assemblerErrors = [];
|
|
983
|
+
/** Everything the assembler said, warnings included. */
|
|
984
|
+
assemblerDiagnostics = [];
|
|
686
985
|
callbacks;
|
|
687
986
|
scheduler;
|
|
688
987
|
stateWaiters = [];
|
|
689
988
|
stdinBytes = [];
|
|
690
989
|
sourceMap = null;
|
|
691
990
|
sourceProject = null;
|
|
991
|
+
assembleOnly = false;
|
|
992
|
+
assembledObjects = [];
|
|
993
|
+
/** What the linker said, kept apart from the assembler's log so `ld`'s own parser reads it. */
|
|
994
|
+
linkerLogs = null;
|
|
692
995
|
defaultArgc = "/program";
|
|
693
996
|
defaultArgv = "";
|
|
694
997
|
constructor(module, mode, callbacks, scheduler) {
|
|
@@ -735,7 +1038,7 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
735
1038
|
this.assemblerLogs = "";
|
|
736
1039
|
this.assemblerErrors = [];
|
|
737
1040
|
this.setState("NOT_READY");
|
|
738
|
-
await this.writeExecutable("/assembler", await readResourceBytes(this.mode.binaries.assembler.file));
|
|
1041
|
+
if (this.mode.binaries.assembler) await this.writeExecutable("/assembler", await readResourceBytes(this.mode.binaries.assembler.file));
|
|
739
1042
|
if (this.mode.binaries.linker) await this.writeExecutable("/linker", await readResourceBytes(this.mode.binaries.linker.file));
|
|
740
1043
|
this.setState("READY");
|
|
741
1044
|
}
|
|
@@ -746,41 +1049,175 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
746
1049
|
});
|
|
747
1050
|
}
|
|
748
1051
|
async compileProject(project) {
|
|
1052
|
+
return this.buildProject(project, { link: true });
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Assembles without linking, for diagnostics. The linked executable is only
|
|
1056
|
+
* needed to run or debug a program; a caller that just wants to know what is
|
|
1057
|
+
* wrong with the source pays ~450ms for a link it never looks at.
|
|
1058
|
+
*
|
|
1059
|
+
* A mode with a `wasmAssembler` never touches blink here, so checking also
|
|
1060
|
+
* leaves a loaded program - and anything paused in the debugger - alone.
|
|
1061
|
+
*/
|
|
1062
|
+
async checkProject(project) {
|
|
1063
|
+
if (this.mode.wasmAssembler) return this.checkProjectWithWasmAssembler(project);
|
|
1064
|
+
return this.buildProject(project, { link: false });
|
|
1065
|
+
}
|
|
1066
|
+
async checkProjectWithWasmAssembler(project) {
|
|
1067
|
+
const sourceProject = copyX86Project(project);
|
|
1068
|
+
const assembled = await this.mode.wasmAssembler.assemble(sourceProject);
|
|
1069
|
+
const report = assembled.stdout + assembled.stderr;
|
|
1070
|
+
return toCompileResult([...this.parseAssemblerDiagnostics(report, sourceProject), ...this.entryPointDiagnostics(assembled.units.map((unit) => unit.object), sourceProject)], report);
|
|
1071
|
+
}
|
|
1072
|
+
async buildProject(project, options) {
|
|
749
1073
|
this.assertReadyForCompile();
|
|
750
|
-
const sourceProject =
|
|
751
|
-
entry: project.entry,
|
|
752
|
-
files: Object.fromEntries(Object.entries(project.files).map(([path, contents]) => [path, contents instanceof Uint8Array ? contents.slice() : contents]))
|
|
753
|
-
};
|
|
754
|
-
stageX86Project(this.module.FS, sourceProject);
|
|
1074
|
+
const sourceProject = copyX86Project(project);
|
|
755
1075
|
this.sourceProject = sourceProject;
|
|
756
1076
|
this.stopReason = null;
|
|
757
1077
|
this.assemblerLogs = "";
|
|
758
1078
|
this.assemblerErrors = [];
|
|
1079
|
+
this.assemblerDiagnostics = [];
|
|
1080
|
+
this.assembledObjects = [];
|
|
1081
|
+
this.linkerLogs = null;
|
|
759
1082
|
this.sourceMap = null;
|
|
760
|
-
this.
|
|
1083
|
+
this.assembleOnly = !options.link;
|
|
761
1084
|
try {
|
|
762
|
-
await
|
|
763
|
-
this.
|
|
764
|
-
this.module._blinkenlib_run_fast();
|
|
765
|
-
await this.waitForState((state) => state !== "ASSEMBLING" && state !== "LINKING");
|
|
1085
|
+
if (this.mode.wasmAssembler) await this.assembleWithWasmAssembler(sourceProject, options);
|
|
1086
|
+
else await this.assembleInBlink(sourceProject);
|
|
766
1087
|
} finally {
|
|
1088
|
+
this.assembleOnly = false;
|
|
767
1089
|
this.module.FS.chdir("/");
|
|
768
1090
|
}
|
|
769
|
-
|
|
770
|
-
this.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
report: this.assemblerLogs
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
return {
|
|
777
|
-
ok: false,
|
|
778
|
-
errors: this.assemblerErrors.map((error) => ({
|
|
779
|
-
...error,
|
|
780
|
-
file: x86ProjectSourcePath(error.file, sourceProject)
|
|
1091
|
+
const diagnostics = [
|
|
1092
|
+
...this.assemblerDiagnostics.map((diagnostic) => ({
|
|
1093
|
+
...diagnostic,
|
|
1094
|
+
file: x86ProjectSourcePath(diagnostic.file, sourceProject)
|
|
781
1095
|
})),
|
|
782
|
-
|
|
783
|
-
|
|
1096
|
+
...this.entryPointDiagnostics(this.takeAssembledObjects(), sourceProject),
|
|
1097
|
+
...this.linkDiagnostics(sourceProject)
|
|
1098
|
+
];
|
|
1099
|
+
if (this.state === "PROGRAM_LOADED") this.sourceMap = this.tryReadSourceMap();
|
|
1100
|
+
return toCompileResult(diagnostics, this.assemblerLogs);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* The objects this build's assembler wrote, for the checks that read them
|
|
1104
|
+
* rather than the assembler's own words. Empty when the assembly failed, and
|
|
1105
|
+
* when the assembler writes an executable directly and never produces one.
|
|
1106
|
+
* Read once per build, so a previous build's objects can never stand in for
|
|
1107
|
+
* ones that were never written.
|
|
1108
|
+
*/
|
|
1109
|
+
takeAssembledObjects() {
|
|
1110
|
+
const objects = this.assembledObjects;
|
|
1111
|
+
this.assembledObjects = [];
|
|
1112
|
+
return objects;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* What the link had to say. A link that failed leaves no program to run, so a build that
|
|
1116
|
+
* reported nothing would hand the caller a success it cannot act on - the state stays `Ready`
|
|
1117
|
+
* and the next step or run fails with a message about the emulator rather than the mistake.
|
|
1118
|
+
* The fallback covers a linker that fails in a way its parser does not recognise, so a failed
|
|
1119
|
+
* link is always visible as an error whatever `ld` chose to say about it.
|
|
1120
|
+
*/
|
|
1121
|
+
linkDiagnostics(sourceProject) {
|
|
1122
|
+
if (this.linkerLogs === null) return [];
|
|
1123
|
+
const diagnostics = ldDiagnostics(this.linkerLogs).map((diagnostic) => ({
|
|
1124
|
+
...diagnostic,
|
|
1125
|
+
file: x86ProjectSourcePath(diagnostic.file, sourceProject)
|
|
1126
|
+
}));
|
|
1127
|
+
if (this.state === "PROGRAM_LOADED" || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return diagnostics;
|
|
1128
|
+
return [...diagnostics, {
|
|
1129
|
+
line: 1,
|
|
1130
|
+
file: sourceProject.entry,
|
|
1131
|
+
severity: "error",
|
|
1132
|
+
error: "linking failed, so there is no program to run."
|
|
1133
|
+
}];
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Nothing in the toolchain treats a missing entry point as a failure: NASM
|
|
1137
|
+
* has no opinion on it and `ld` only warns, then starts the program at the
|
|
1138
|
+
* top of its text segment, where it runs whatever happens to be first. That
|
|
1139
|
+
* is the least explicable thing a program can do to someone learning, so it
|
|
1140
|
+
* is reported as an error here and the mistake is usually a misspelling.
|
|
1141
|
+
*/
|
|
1142
|
+
entryPointDiagnostics(objects, sourceProject) {
|
|
1143
|
+
if (!objects.length || !this.mode.binaries.linker) return [];
|
|
1144
|
+
const globals = objects.flatMap((object) => readDefinedGlobalSymbols(object));
|
|
1145
|
+
if (globals.includes("_start")) return [];
|
|
1146
|
+
const nearest = findNearestSymbol(DEFAULT_ENTRY_SYMBOL, globals);
|
|
1147
|
+
const nearestHint = nearest ? ` Did you mean \`${DEFAULT_ENTRY_SYMBOL}\` where you wrote \`${nearest}\`?` : "";
|
|
1148
|
+
const exportHint = globals.length ? "" : ` Defining the label is not enough on its own: \`global ${DEFAULT_ENTRY_SYMBOL}\` is what exports it.`;
|
|
1149
|
+
return [{
|
|
1150
|
+
line: 1,
|
|
1151
|
+
file: sourceProject.entry,
|
|
1152
|
+
severity: "error",
|
|
1153
|
+
warningClass: "entry-point",
|
|
1154
|
+
error: `no \`${DEFAULT_ENTRY_SYMBOL}\` to start from.${nearestHint}${exportHint}`
|
|
1155
|
+
}];
|
|
1156
|
+
}
|
|
1157
|
+
async assembleInBlink(sourceProject) {
|
|
1158
|
+
stageX86Project(this.module.FS, sourceProject);
|
|
1159
|
+
this.setState("ASSEMBLING");
|
|
1160
|
+
await defer();
|
|
1161
|
+
this.setEmulationArgs("/assembler", this.mode.binaries.assembler.commands, "");
|
|
1162
|
+
this.module._blinkenlib_run_fast();
|
|
1163
|
+
await this.waitForState((state) => state !== "ASSEMBLING" && state !== "LINKING");
|
|
1164
|
+
if (this.assemblerErrors.length === 0) try {
|
|
1165
|
+
this.assembledObjects = [this.module.FS.readFile("/program.o")];
|
|
1166
|
+
} catch {}
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Assembles outside blink and hands the object file to the linker inside it,
|
|
1170
|
+
* which is where the state machine picks up again as if blink had produced
|
|
1171
|
+
* the object itself.
|
|
1172
|
+
*/
|
|
1173
|
+
async assembleWithWasmAssembler(sourceProject, options) {
|
|
1174
|
+
this.setState("ASSEMBLING");
|
|
1175
|
+
const assembled = await this.mode.wasmAssembler.assemble(sourceProject);
|
|
1176
|
+
this.emitAssemblerOutput(assembled.stdout, this.callbacks.stdout);
|
|
1177
|
+
this.emitAssemblerOutput(assembled.stderr, this.callbacks.stderr);
|
|
1178
|
+
this.collectAssemblerDiagnostics();
|
|
1179
|
+
if (!assembled.units.length) {
|
|
1180
|
+
this.setState("READY");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
this.assembledObjects = assembled.units.map((unit) => unit.object);
|
|
1184
|
+
if (!options.link) {
|
|
1185
|
+
this.setState("READY");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const objectPaths = assembled.units.map((_, index) => index === 0 ? "/program.o" : `/program.${index}.o`);
|
|
1189
|
+
assembled.units.forEach((unit, index) => this.writeExecutableSync(objectPaths[index], unit.object));
|
|
1190
|
+
this.beginLinking();
|
|
1191
|
+
await defer();
|
|
1192
|
+
this.setEmulationArgs("/linker", this.mode.binaries.linker?.link(objectPaths) ?? "", "");
|
|
1193
|
+
this.module._blinkenlib_run_fast();
|
|
1194
|
+
await this.waitForState((state) => state !== "LINKING");
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Starts the link, and starts a log of its own for it. The assembler's parser cannot read
|
|
1198
|
+
* `ld`'s diagnostics and `ld`'s cannot read the assembler's, so what each of them said has to
|
|
1199
|
+
* stay separable even though both reach the caller as one report.
|
|
1200
|
+
*/
|
|
1201
|
+
beginLinking() {
|
|
1202
|
+
this.linkerLogs = "";
|
|
1203
|
+
this.setState("LINKING");
|
|
1204
|
+
}
|
|
1205
|
+
collectAssemblerDiagnostics() {
|
|
1206
|
+
this.assemblerDiagnostics = this.mode.diagnosticsParser?.(this.assemblerLogs) ?? [];
|
|
1207
|
+
this.assemblerErrors = this.assemblerDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1208
|
+
}
|
|
1209
|
+
emitAssemblerOutput(text, callback) {
|
|
1210
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1211
|
+
const charCode = text.charCodeAt(index);
|
|
1212
|
+
this.assemblerLogs += String.fromCharCode(charCode);
|
|
1213
|
+
observeCallbackResult(callback(charCode));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
parseAssemblerDiagnostics(report, sourceProject) {
|
|
1217
|
+
return (this.mode.diagnosticsParser?.(report) ?? []).map((error) => ({
|
|
1218
|
+
...error,
|
|
1219
|
+
file: x86ProjectSourcePath(error.file, sourceProject)
|
|
1220
|
+
}));
|
|
784
1221
|
}
|
|
785
1222
|
loadElf(data) {
|
|
786
1223
|
if (this.state === "NOT_READY") throw new Error("Blink runtime is not ready");
|
|
@@ -961,7 +1398,10 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
961
1398
|
this.module.blinkenlibSetRunControls(BigInt(limit), (options.breakpointAddresses ?? []).map((address) => address.toString()));
|
|
962
1399
|
}
|
|
963
1400
|
collectAssemblerLog(charCode) {
|
|
964
|
-
if (this.state
|
|
1401
|
+
if (this.state !== "ASSEMBLING" && this.state !== "LINKING") return;
|
|
1402
|
+
const character = String.fromCharCode(charCode);
|
|
1403
|
+
this.assemblerLogs += character;
|
|
1404
|
+
if (this.state === "LINKING" && this.linkerLogs !== null) this.linkerLogs += character;
|
|
965
1405
|
}
|
|
966
1406
|
handleSignal(signal, code) {
|
|
967
1407
|
if (signal !== SIGNALS.SIGTRAP) {
|
|
@@ -1026,8 +1466,12 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
1026
1466
|
this.setState("PROGRAM_STOPPED");
|
|
1027
1467
|
}
|
|
1028
1468
|
handleAssemblerExit(code) {
|
|
1469
|
+
this.collectAssemblerDiagnostics();
|
|
1029
1470
|
if (code !== 0) {
|
|
1030
|
-
this.
|
|
1471
|
+
this.setState("READY");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (this.assembleOnly) {
|
|
1031
1475
|
this.setState("READY");
|
|
1032
1476
|
return;
|
|
1033
1477
|
}
|
|
@@ -1036,9 +1480,9 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
1036
1480
|
this.setState("PROGRAM_LOADED");
|
|
1037
1481
|
return;
|
|
1038
1482
|
}
|
|
1039
|
-
this.
|
|
1483
|
+
this.beginLinking();
|
|
1040
1484
|
this.scheduler(() => {
|
|
1041
|
-
this.setEmulationArgs("/linker", this.mode.binaries.linker?.
|
|
1485
|
+
this.setEmulationArgs("/linker", this.mode.binaries.linker?.link(["/program.o"]) ?? "", "");
|
|
1042
1486
|
this.module._blinkenlib_run_fast();
|
|
1043
1487
|
});
|
|
1044
1488
|
}
|
|
@@ -1121,6 +1565,31 @@ function defaultScheduler(callback) {
|
|
|
1121
1565
|
function defer() {
|
|
1122
1566
|
return new Promise((resolve) => defaultScheduler(resolve));
|
|
1123
1567
|
}
|
|
1568
|
+
/**
|
|
1569
|
+
* A build succeeded when nothing in it was an error. Warnings ride along on both
|
|
1570
|
+
* outcomes, because a program that assembles is exactly where they matter.
|
|
1571
|
+
*/
|
|
1572
|
+
function toCompileResult(diagnostics, report) {
|
|
1573
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1574
|
+
if (errors.length === 0) return {
|
|
1575
|
+
ok: true,
|
|
1576
|
+
report,
|
|
1577
|
+
diagnostics
|
|
1578
|
+
};
|
|
1579
|
+
return {
|
|
1580
|
+
ok: false,
|
|
1581
|
+
errors,
|
|
1582
|
+
report,
|
|
1583
|
+
diagnostics
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/** Detaches a Project from the caller, who is free to mutate its own copy afterwards. */
|
|
1587
|
+
function copyX86Project(project) {
|
|
1588
|
+
return {
|
|
1589
|
+
entry: project.entry,
|
|
1590
|
+
files: Object.fromEntries(Object.entries(project.files).map(([path, contents]) => [path, contents instanceof Uint8Array ? contents.slice() : contents]))
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1124
1593
|
//#endregion
|
|
1125
1594
|
//#region src/interface.ts
|
|
1126
1595
|
let RegisterSize = /* @__PURE__ */ function(RegisterSize) {
|
|
@@ -1408,21 +1877,28 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1408
1877
|
files: { "assembly.s": code }
|
|
1409
1878
|
});
|
|
1410
1879
|
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Diagnostics only: this assembles but does not link, and for an assembler
|
|
1882
|
+
* that runs as its own wasm module it does not touch blink at all, so a
|
|
1883
|
+
* loaded or paused program survives a check.
|
|
1884
|
+
*/
|
|
1411
1885
|
async checkProject(project) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1886
|
+
return (await this.runtime.checkProject(project)).diagnostics.map((diagnostic) => {
|
|
1887
|
+
const path = diagnostic.file ?? project.entry;
|
|
1888
|
+
const lineIndex = Math.max(0, diagnostic.line - 1);
|
|
1889
|
+
const line = x86ProjectText(project, path).split(/\r?\n/)[lineIndex] ?? "";
|
|
1416
1890
|
return {
|
|
1417
1891
|
file: path,
|
|
1418
1892
|
lineIndex,
|
|
1419
|
-
column:
|
|
1893
|
+
column: locateDiagnosticColumn(diagnostic.error, line, diagnostic.warningClass),
|
|
1420
1894
|
line: {
|
|
1421
|
-
line
|
|
1895
|
+
line,
|
|
1422
1896
|
line_index: lineIndex
|
|
1423
1897
|
},
|
|
1424
|
-
message:
|
|
1425
|
-
formatted:
|
|
1898
|
+
message: diagnostic.error,
|
|
1899
|
+
formatted: diagnostic.error,
|
|
1900
|
+
severity: diagnostic.severity,
|
|
1901
|
+
...diagnostic.warningClass ? { code: diagnostic.warningClass } : {}
|
|
1426
1902
|
};
|
|
1427
1903
|
});
|
|
1428
1904
|
}
|
|
@@ -1727,6 +2203,6 @@ async function createX86Emulator(options = {}) {
|
|
|
1727
2203
|
return X86Emulator.create(options);
|
|
1728
2204
|
}
|
|
1729
2205
|
//#endregion
|
|
1730
|
-
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_PROJECT_ROOT, X86_REGISTER_NAMES, assemblers, createX86Emulator, defaultResourceUrl, fasmDiagnostics, gnuDiagnostics, isNodeRuntime, nasmDiagnostics, observeCallbackResult, readResourceBytes, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
|
|
2206
|
+
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_PROJECT_ROOT, X86_REGISTER_NAMES, assemblers, createX86Emulator, defaultResourceUrl, fasmDiagnostics, gnuDiagnostics, isNodeRuntime, ldDiagnostics, locateDiagnosticColumn, nasmDiagnostics, observeCallbackResult, readResourceBytes, selectX86Source, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
|
|
1731
2207
|
|
|
1732
2208
|
//# sourceMappingURL=index.mjs.map
|