@specy/x86 2.1.0 → 2.2.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.md +39 -0
- package/dist/index.d.mts +334 -231
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +418 -125
- 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,4 +1,130 @@
|
|
|
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
|
+
/** Maps a path emitted by NASM or DWARF back to the exact Project path. */
|
|
43
|
+
function x86ProjectSourcePath(sourcePath, project) {
|
|
44
|
+
if (!sourcePath || sourcePath === "/assembly.s" || sourcePath === "assembly.s") return project.entry;
|
|
45
|
+
const withoutLeadingSlash = (sourcePath.startsWith(`/__x86_project/`) ? sourcePath.slice(15) : sourcePath).replace(/^\/+/, "");
|
|
46
|
+
const relativeToEntry = resolvePath(parentPath(project.entry), withoutLeadingSlash);
|
|
47
|
+
if (relativeToEntry && relativeToEntry in project.files) return relativeToEntry;
|
|
48
|
+
const relativeToRoot = resolvePath("", withoutLeadingSlash);
|
|
49
|
+
if (relativeToRoot && relativeToRoot in project.files) return relativeToRoot;
|
|
50
|
+
const suffix = `/${withoutLeadingSlash}`;
|
|
51
|
+
const suffixMatches = Object.keys(project.files).filter((path) => path === withoutLeadingSlash || path.endsWith(suffix));
|
|
52
|
+
return suffixMatches.length === 1 ? suffixMatches[0] : project.entry;
|
|
53
|
+
}
|
|
54
|
+
function projectDirectory(entry) {
|
|
55
|
+
const parent = parentPath(entry);
|
|
56
|
+
return parent ? `${X86_PROJECT_ROOT}/${parent}` : X86_PROJECT_ROOT;
|
|
57
|
+
}
|
|
58
|
+
function parentPath(path) {
|
|
59
|
+
const separator = path.lastIndexOf("/");
|
|
60
|
+
return separator < 0 ? "" : path.slice(0, separator);
|
|
61
|
+
}
|
|
62
|
+
function isProjectPath(path) {
|
|
63
|
+
return typeof path === "string" && path.length > 0 && !path.includes("\\") && !path.startsWith("/") && path.split("/").every((part) => part !== "" && part !== "." && part !== "..") && !Array.from(path).some((character) => {
|
|
64
|
+
const code = character.charCodeAt(0);
|
|
65
|
+
return code <= 31 || code >= 127 && code <= 159;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function resolvePath(base, written) {
|
|
69
|
+
const parts = base ? base.split("/") : [];
|
|
70
|
+
for (const part of written.split("/")) {
|
|
71
|
+
if (!part || part === ".") continue;
|
|
72
|
+
if (part === "..") {
|
|
73
|
+
if (!parts.length) return null;
|
|
74
|
+
parts.pop();
|
|
75
|
+
} else parts.push(part);
|
|
76
|
+
}
|
|
77
|
+
return parts.join("/") || null;
|
|
78
|
+
}
|
|
79
|
+
function x86ProjectText(project, path) {
|
|
80
|
+
const contents = project.files[path];
|
|
81
|
+
return typeof contents === "string" ? contents : "";
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/wasm-assembler.ts
|
|
85
|
+
const SOURCE_PATH = "/assembly.s";
|
|
86
|
+
const OBJECT_PATH = "/program.o";
|
|
87
|
+
/**
|
|
88
|
+
* `-g -F dwarf` is what makes the source map possible: the debugger reads
|
|
89
|
+
* `.debug_line` out of the linked executable to map an address back to the line
|
|
90
|
+
* it was written on. The paths match what `stageX86Project` writes, and the
|
|
91
|
+
* working directory it selects is what lets NASM resolve `%include` itself.
|
|
92
|
+
*/
|
|
93
|
+
const NASM_ARGS = [
|
|
94
|
+
"-g",
|
|
95
|
+
"-F",
|
|
96
|
+
"dwarf",
|
|
97
|
+
"-felf64",
|
|
98
|
+
SOURCE_PATH,
|
|
99
|
+
"-o",
|
|
100
|
+
OBJECT_PATH
|
|
101
|
+
];
|
|
102
|
+
const nasmWasmAssembler = { async assemble(project) {
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
const module = await createNasm({
|
|
106
|
+
noInitialRun: true,
|
|
107
|
+
print: (line) => {
|
|
108
|
+
stdout += `${line}\n`;
|
|
109
|
+
},
|
|
110
|
+
printErr: (line) => {
|
|
111
|
+
stderr += `${line}\n`;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
stageX86Project(module.FS, project);
|
|
115
|
+
const host = globalThis.process;
|
|
116
|
+
const hostExitCode = host?.exitCode;
|
|
117
|
+
const status = module.callMain([...NASM_ARGS]);
|
|
118
|
+
if (host) host.exitCode = hostExitCode;
|
|
119
|
+
const object = status === 0 && module.FS.analyzePath(OBJECT_PATH).exists ? module.FS.readFile(OBJECT_PATH) : null;
|
|
120
|
+
return {
|
|
121
|
+
status,
|
|
122
|
+
stdout,
|
|
123
|
+
stderr,
|
|
124
|
+
object
|
|
125
|
+
};
|
|
126
|
+
} };
|
|
127
|
+
//#endregion
|
|
2
128
|
//#region src/assemblers.ts
|
|
3
129
|
const assemblerAsset = (filename) => new URL(`./assets/assemblers/${filename}`, import.meta.url);
|
|
4
130
|
const assemblers = {
|
|
@@ -31,18 +157,13 @@ const assemblers = {
|
|
|
31
157
|
NASM_trunk: {
|
|
32
158
|
id: "NASM_trunk",
|
|
33
159
|
displayName: "Nasm",
|
|
34
|
-
description: "NASM 3.00 + GNU ld v2.43.50
|
|
160
|
+
description: "NASM 3.00 compiled to WebAssembly, + GNU ld v2.43.50 as a static MUSL binary",
|
|
35
161
|
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
|
-
}
|
|
162
|
+
binaries: { linker: {
|
|
163
|
+
file: assemblerAsset("gnu-ld.2.43.50.elf"),
|
|
164
|
+
commands: "/linker /program.o -o /program"
|
|
165
|
+
} },
|
|
166
|
+
wasmAssembler: nasmWasmAssembler
|
|
46
167
|
}
|
|
47
168
|
};
|
|
48
169
|
const DEFAULT_ASSEMBLER_ID = "NASM_trunk";
|
|
@@ -50,12 +171,18 @@ function nasmDiagnostics(str) {
|
|
|
50
171
|
const diagnostics = [];
|
|
51
172
|
const lines = str.split(/\r?\n/);
|
|
52
173
|
const regex = /^(.*):(\d+): (error|warning): (.*)$/;
|
|
174
|
+
const classRegex = /\s*\[-w\+([\w-]+)\]$/;
|
|
53
175
|
for (const line of lines) {
|
|
54
176
|
const match = line.match(regex);
|
|
55
|
-
if (match)
|
|
177
|
+
if (!match) continue;
|
|
178
|
+
const text = (match[4] ?? "").trim();
|
|
179
|
+
const classMatch = text.match(classRegex);
|
|
180
|
+
diagnostics.push({
|
|
56
181
|
file: match[1] || void 0,
|
|
57
182
|
line: Number.parseInt(match[2] ?? "0", 10),
|
|
58
|
-
error: (
|
|
183
|
+
error: classMatch ? text.slice(0, classMatch.index).trim() : text,
|
|
184
|
+
severity: match[3] === "warning" ? "warning" : "error",
|
|
185
|
+
...classMatch?.[1] ? { warningClass: classMatch[1] } : {}
|
|
59
186
|
});
|
|
60
187
|
}
|
|
61
188
|
return diagnostics;
|
|
@@ -63,13 +190,15 @@ function nasmDiagnostics(str) {
|
|
|
63
190
|
function gnuDiagnostics(str) {
|
|
64
191
|
const diagnostics = [];
|
|
65
192
|
const lines = str.split("\n");
|
|
66
|
-
const regex = /^(.*):(\d+): (Error: .+)$/;
|
|
193
|
+
const regex = /^(.*):(\d+): (Error|Warning): (.+)$/;
|
|
67
194
|
for (const line of lines) {
|
|
68
195
|
const match = line.match(regex);
|
|
69
|
-
if (match)
|
|
196
|
+
if (!match) continue;
|
|
197
|
+
diagnostics.push({
|
|
70
198
|
file: match[1] || void 0,
|
|
71
199
|
line: Number.parseInt(match[2] ?? "0", 10),
|
|
72
|
-
error: match[3]
|
|
200
|
+
error: `${match[3]}: ${match[4]}`,
|
|
201
|
+
severity: match[3] === "Warning" ? "warning" : "error"
|
|
73
202
|
});
|
|
74
203
|
}
|
|
75
204
|
return diagnostics;
|
|
@@ -92,7 +221,8 @@ function fasmDiagnostics(str) {
|
|
|
92
221
|
diagnostics.push({
|
|
93
222
|
file,
|
|
94
223
|
line: lineNumber,
|
|
95
|
-
error: errorMatch[0]
|
|
224
|
+
error: errorMatch[0],
|
|
225
|
+
severity: "error"
|
|
96
226
|
});
|
|
97
227
|
lineNumber = null;
|
|
98
228
|
file = void 0;
|
|
@@ -100,6 +230,41 @@ function fasmDiagnostics(str) {
|
|
|
100
230
|
}
|
|
101
231
|
return diagnostics;
|
|
102
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The one-based column a diagnostic belongs on. NASM reports a line and no
|
|
235
|
+
* column, but it names the thing it is complaining about: `msg' in "symbol
|
|
236
|
+
* `msg' not defined", `.data' in a section warning. Finding that name in the
|
|
237
|
+
* line turns a whole-line squiggle into one under the mistake itself.
|
|
238
|
+
*
|
|
239
|
+
* Falls back to the first column, which is what the caller would have used
|
|
240
|
+
* anyway, so a message that names nothing findable is no worse off.
|
|
241
|
+
*/
|
|
242
|
+
function locateDiagnosticColumn(message, line, warningClass) {
|
|
243
|
+
if (warningClass === "label-orphan") {
|
|
244
|
+
const leading = line.search(/\S/);
|
|
245
|
+
return leading < 0 ? 1 : leading + 1;
|
|
246
|
+
}
|
|
247
|
+
const quoted = message.match(/`([^'`]+)'/)?.[1];
|
|
248
|
+
if (!quoted) return 1;
|
|
249
|
+
const index = findIdentifier(line, quoted);
|
|
250
|
+
return index < 0 ? 1 : index + 1;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Where a name appears in a line as itself, rather than inside a longer one.
|
|
254
|
+
* Identifier characters on either side mean this is a different symbol that
|
|
255
|
+
* merely contains the name.
|
|
256
|
+
*/
|
|
257
|
+
function findIdentifier(line, name) {
|
|
258
|
+
const isIdentifierChar = (character) => character !== void 0 && /[\w$#@~.?]/.test(character);
|
|
259
|
+
let from = 0;
|
|
260
|
+
while (from <= line.length - name.length) {
|
|
261
|
+
const index = line.indexOf(name, from);
|
|
262
|
+
if (index < 0) return -1;
|
|
263
|
+
if (!isIdentifierChar(line[index - 1]) && !isIdentifierChar(line[index + name.length])) return index;
|
|
264
|
+
from = index + 1;
|
|
265
|
+
}
|
|
266
|
+
return -1;
|
|
267
|
+
}
|
|
103
268
|
//#endregion
|
|
104
269
|
//#region \0wasm-helpers.js
|
|
105
270
|
function loadWasmModule(sync, filepath, src, imports) {
|
|
@@ -470,85 +635,73 @@ function compareBigInt(left, right) {
|
|
|
470
635
|
return 0;
|
|
471
636
|
}
|
|
472
637
|
//#endregion
|
|
473
|
-
//#region src/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
638
|
+
//#region src/elf-symbols.ts
|
|
639
|
+
/**
|
|
640
|
+
* The entry symbol `ld` looks for when no `-e` is given. A program without it
|
|
641
|
+
* still links: `ld` warns, defaults the entry address to the start of the text
|
|
642
|
+
* segment, and the program runs from whatever happens to be there.
|
|
643
|
+
*/
|
|
644
|
+
const DEFAULT_ENTRY_SYMBOL = "_start";
|
|
645
|
+
const SYMBOL_ENTRY_SIZE = 24;
|
|
646
|
+
const SHN_UNDEF = 0;
|
|
647
|
+
const STB_LOCAL = 0;
|
|
648
|
+
/**
|
|
649
|
+
* Names every symbol an object file defines and exports, which is what decides
|
|
650
|
+
* whether the linker can find an entry point. A symbol that is merely referenced
|
|
651
|
+
* (`st_shndx` of SHN_UNDEF) is not defined here, and a local one - a label
|
|
652
|
+
* without a matching `global` directive - is invisible to the linker.
|
|
653
|
+
*/
|
|
654
|
+
function readDefinedGlobalSymbols(objectBytes) {
|
|
655
|
+
const symbolTable = findElfSection(objectBytes, ".symtab");
|
|
656
|
+
const stringTable = findElfSection(objectBytes, ".strtab");
|
|
657
|
+
if (!symbolTable || !stringTable) return [];
|
|
658
|
+
const view = new DataView(objectBytes.buffer, objectBytes.byteOffset, objectBytes.byteLength);
|
|
659
|
+
const strings = objectBytes.subarray(stringTable.offset, stringTable.offset + stringTable.size);
|
|
660
|
+
const names = [];
|
|
661
|
+
for (let offset = 0; offset + SYMBOL_ENTRY_SIZE <= symbolTable.size; offset += SYMBOL_ENTRY_SIZE) {
|
|
662
|
+
const entry = symbolTable.offset + offset;
|
|
663
|
+
const nameOffset = view.getUint32(entry, true);
|
|
664
|
+
const binding = view.getUint8(entry + 4) >> 4;
|
|
665
|
+
const sectionIndex = view.getUint16(entry + 6, true);
|
|
666
|
+
if (binding === STB_LOCAL || sectionIndex === SHN_UNDEF) continue;
|
|
667
|
+
if (nameOffset >= strings.length) continue;
|
|
668
|
+
const [name] = readNullTerminatedString(strings, nameOffset, strings.length);
|
|
669
|
+
if (name) names.push(name);
|
|
670
|
+
}
|
|
671
|
+
return names;
|
|
484
672
|
}
|
|
485
673
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
674
|
+
* The symbol a misspelling was probably meant to be, or null when nothing in the
|
|
675
|
+
* object looks close enough to say. Deliberately strict: naming the wrong symbol
|
|
676
|
+
* is worse than naming none, so this only answers for a small edit distance on a
|
|
677
|
+
* name of a similar length.
|
|
489
678
|
*/
|
|
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 {}
|
|
679
|
+
function findNearestSymbol(target, candidates) {
|
|
680
|
+
let best = null;
|
|
681
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
682
|
+
for (const candidate of candidates) {
|
|
683
|
+
if (candidate === target) return null;
|
|
684
|
+
if (Math.abs(candidate.length - target.length) > 2) continue;
|
|
685
|
+
const distance = editDistance(candidate, target);
|
|
686
|
+
if (distance < bestDistance) {
|
|
687
|
+
bestDistance = distance;
|
|
688
|
+
best = candidate;
|
|
689
|
+
}
|
|
507
690
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
stagedFiles.set(fs, current);
|
|
691
|
+
const allowed = target.length <= 4 ? 1 : 2;
|
|
692
|
+
return bestDistance <= allowed ? best : null;
|
|
511
693
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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;
|
|
527
|
-
}
|
|
528
|
-
function parentPath(path) {
|
|
529
|
-
const separator = path.lastIndexOf("/");
|
|
530
|
-
return separator < 0 ? "" : path.slice(0, separator);
|
|
531
|
-
}
|
|
532
|
-
function isProjectPath(path) {
|
|
533
|
-
return typeof path === "string" && path.length > 0 && !path.includes("\\") && !path.startsWith("/") && path.split("/").every((part) => part !== "" && part !== "." && part !== "..") && !Array.from(path).some((character) => {
|
|
534
|
-
const code = character.charCodeAt(0);
|
|
535
|
-
return code <= 31 || code >= 127 && code <= 159;
|
|
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);
|
|
694
|
+
function editDistance(left, right) {
|
|
695
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
696
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
697
|
+
const current = [leftIndex];
|
|
698
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
699
|
+
const substitution = previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
700
|
+
current.push(Math.min(substitution, previous[rightIndex] + 1, current[rightIndex - 1] + 1));
|
|
701
|
+
}
|
|
702
|
+
previous = current;
|
|
546
703
|
}
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
function x86ProjectText(project, path) {
|
|
550
|
-
const contents = project.files[path];
|
|
551
|
-
return typeof contents === "string" ? contents : "";
|
|
704
|
+
return previous[right.length];
|
|
552
705
|
}
|
|
553
706
|
//#endregion
|
|
554
707
|
//#region src/types.ts
|
|
@@ -682,13 +835,18 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
682
835
|
state = "NOT_READY";
|
|
683
836
|
stopReason = null;
|
|
684
837
|
assemblerLogs = "";
|
|
838
|
+
/** The subset of `assemblerDiagnostics` that stopped the build. */
|
|
685
839
|
assemblerErrors = [];
|
|
840
|
+
/** Everything the assembler said, warnings included. */
|
|
841
|
+
assemblerDiagnostics = [];
|
|
686
842
|
callbacks;
|
|
687
843
|
scheduler;
|
|
688
844
|
stateWaiters = [];
|
|
689
845
|
stdinBytes = [];
|
|
690
846
|
sourceMap = null;
|
|
691
847
|
sourceProject = null;
|
|
848
|
+
assembleOnly = false;
|
|
849
|
+
assembledObject = null;
|
|
692
850
|
defaultArgc = "/program";
|
|
693
851
|
defaultArgv = "";
|
|
694
852
|
constructor(module, mode, callbacks, scheduler) {
|
|
@@ -735,7 +893,7 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
735
893
|
this.assemblerLogs = "";
|
|
736
894
|
this.assemblerErrors = [];
|
|
737
895
|
this.setState("NOT_READY");
|
|
738
|
-
await this.writeExecutable("/assembler", await readResourceBytes(this.mode.binaries.assembler.file));
|
|
896
|
+
if (this.mode.binaries.assembler) await this.writeExecutable("/assembler", await readResourceBytes(this.mode.binaries.assembler.file));
|
|
739
897
|
if (this.mode.binaries.linker) await this.writeExecutable("/linker", await readResourceBytes(this.mode.binaries.linker.file));
|
|
740
898
|
this.setState("READY");
|
|
741
899
|
}
|
|
@@ -746,41 +904,140 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
746
904
|
});
|
|
747
905
|
}
|
|
748
906
|
async compileProject(project) {
|
|
907
|
+
return this.buildProject(project, { link: true });
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Assembles without linking, for diagnostics. The linked executable is only
|
|
911
|
+
* needed to run or debug a program; a caller that just wants to know what is
|
|
912
|
+
* wrong with the source pays ~450ms for a link it never looks at.
|
|
913
|
+
*
|
|
914
|
+
* A mode with a `wasmAssembler` never touches blink here, so checking also
|
|
915
|
+
* leaves a loaded program - and anything paused in the debugger - alone.
|
|
916
|
+
*/
|
|
917
|
+
async checkProject(project) {
|
|
918
|
+
if (this.mode.wasmAssembler) return this.checkProjectWithWasmAssembler(project);
|
|
919
|
+
return this.buildProject(project, { link: false });
|
|
920
|
+
}
|
|
921
|
+
async checkProjectWithWasmAssembler(project) {
|
|
922
|
+
const sourceProject = copyX86Project(project);
|
|
923
|
+
const assembled = await this.mode.wasmAssembler.assemble(sourceProject);
|
|
924
|
+
const report = assembled.stdout + assembled.stderr;
|
|
925
|
+
return toCompileResult([...this.parseAssemblerDiagnostics(report, sourceProject), ...this.entryPointDiagnostics(assembled.object, sourceProject)], report);
|
|
926
|
+
}
|
|
927
|
+
async buildProject(project, options) {
|
|
749
928
|
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);
|
|
929
|
+
const sourceProject = copyX86Project(project);
|
|
755
930
|
this.sourceProject = sourceProject;
|
|
756
931
|
this.stopReason = null;
|
|
757
932
|
this.assemblerLogs = "";
|
|
758
933
|
this.assemblerErrors = [];
|
|
934
|
+
this.assemblerDiagnostics = [];
|
|
935
|
+
this.assembledObject = null;
|
|
759
936
|
this.sourceMap = null;
|
|
760
|
-
this.
|
|
937
|
+
this.assembleOnly = !options.link;
|
|
761
938
|
try {
|
|
762
|
-
await
|
|
763
|
-
this.
|
|
764
|
-
this.module._blinkenlib_run_fast();
|
|
765
|
-
await this.waitForState((state) => state !== "ASSEMBLING" && state !== "LINKING");
|
|
939
|
+
if (this.mode.wasmAssembler) await this.assembleWithWasmAssembler(sourceProject, options);
|
|
940
|
+
else await this.assembleInBlink(sourceProject);
|
|
766
941
|
} finally {
|
|
942
|
+
this.assembleOnly = false;
|
|
767
943
|
this.module.FS.chdir("/");
|
|
768
944
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
945
|
+
const diagnostics = [...this.assemblerDiagnostics.map((diagnostic) => ({
|
|
946
|
+
...diagnostic,
|
|
947
|
+
file: x86ProjectSourcePath(diagnostic.file, sourceProject)
|
|
948
|
+
})), ...this.entryPointDiagnostics(this.takeAssembledObject(), sourceProject)];
|
|
949
|
+
if (this.state === "PROGRAM_LOADED") this.sourceMap = this.tryReadSourceMap();
|
|
950
|
+
return toCompileResult(diagnostics, this.assemblerLogs);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* The object this build's assembler wrote, for the checks that read it
|
|
954
|
+
* rather than the assembler's own words. Null when the assembly failed, and
|
|
955
|
+
* when the assembler writes an executable directly and never produces one.
|
|
956
|
+
* Read once per build, so a previous build's object can never stand in for
|
|
957
|
+
* one that was never written.
|
|
958
|
+
*/
|
|
959
|
+
takeAssembledObject() {
|
|
960
|
+
const object = this.assembledObject;
|
|
961
|
+
this.assembledObject = null;
|
|
962
|
+
return object;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Nothing in the toolchain treats a missing entry point as a failure: NASM
|
|
966
|
+
* has no opinion on it and `ld` only warns, then starts the program at the
|
|
967
|
+
* top of its text segment, where it runs whatever happens to be first. That
|
|
968
|
+
* is the least explicable thing a program can do to someone learning, so it
|
|
969
|
+
* is reported as an error here and the mistake is usually a misspelling.
|
|
970
|
+
*/
|
|
971
|
+
entryPointDiagnostics(object, sourceProject) {
|
|
972
|
+
if (!object || !this.mode.binaries.linker) return [];
|
|
973
|
+
const globals = readDefinedGlobalSymbols(object);
|
|
974
|
+
if (globals.includes("_start")) return [];
|
|
975
|
+
const nearest = findNearestSymbol(DEFAULT_ENTRY_SYMBOL, globals);
|
|
976
|
+
const nearestHint = nearest ? ` Did you mean \`${DEFAULT_ENTRY_SYMBOL}\` where you wrote \`${nearest}\`?` : "";
|
|
977
|
+
const exportHint = globals.length ? "" : ` Defining the label is not enough on its own: \`global ${DEFAULT_ENTRY_SYMBOL}\` is what exports it.`;
|
|
978
|
+
return [{
|
|
979
|
+
line: 1,
|
|
980
|
+
file: sourceProject.entry,
|
|
981
|
+
severity: "error",
|
|
982
|
+
warningClass: "entry-point",
|
|
983
|
+
error: `no \`${DEFAULT_ENTRY_SYMBOL}\` to start from.${nearestHint}${exportHint}`
|
|
984
|
+
}];
|
|
985
|
+
}
|
|
986
|
+
async assembleInBlink(sourceProject) {
|
|
987
|
+
stageX86Project(this.module.FS, sourceProject);
|
|
988
|
+
this.setState("ASSEMBLING");
|
|
989
|
+
await defer();
|
|
990
|
+
this.setEmulationArgs("/assembler", this.mode.binaries.assembler.commands, "");
|
|
991
|
+
this.module._blinkenlib_run_fast();
|
|
992
|
+
await this.waitForState((state) => state !== "ASSEMBLING" && state !== "LINKING");
|
|
993
|
+
if (this.assemblerErrors.length === 0) try {
|
|
994
|
+
this.assembledObject = this.module.FS.readFile("/program.o");
|
|
995
|
+
} catch {}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Assembles outside blink and hands the object file to the linker inside it,
|
|
999
|
+
* which is where the state machine picks up again as if blink had produced
|
|
1000
|
+
* the object itself.
|
|
1001
|
+
*/
|
|
1002
|
+
async assembleWithWasmAssembler(sourceProject, options) {
|
|
1003
|
+
this.setState("ASSEMBLING");
|
|
1004
|
+
const assembled = await this.mode.wasmAssembler.assemble(sourceProject);
|
|
1005
|
+
this.emitAssemblerOutput(assembled.stdout, this.callbacks.stdout);
|
|
1006
|
+
this.emitAssemblerOutput(assembled.stderr, this.callbacks.stderr);
|
|
1007
|
+
this.collectAssemblerDiagnostics();
|
|
1008
|
+
if (!assembled.object) {
|
|
1009
|
+
this.setState("READY");
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (!options.link) {
|
|
1013
|
+
this.assembledObject = assembled.object;
|
|
1014
|
+
this.setState("READY");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
this.assembledObject = assembled.object;
|
|
1018
|
+
this.writeExecutableSync("/program.o", assembled.object);
|
|
1019
|
+
this.setState("LINKING");
|
|
1020
|
+
await defer();
|
|
1021
|
+
this.setEmulationArgs("/linker", this.mode.binaries.linker?.commands ?? "", "");
|
|
1022
|
+
this.module._blinkenlib_run_fast();
|
|
1023
|
+
await this.waitForState((state) => state !== "LINKING");
|
|
1024
|
+
}
|
|
1025
|
+
collectAssemblerDiagnostics() {
|
|
1026
|
+
this.assemblerDiagnostics = this.mode.diagnosticsParser?.(this.assemblerLogs) ?? [];
|
|
1027
|
+
this.assemblerErrors = this.assemblerDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1028
|
+
}
|
|
1029
|
+
emitAssemblerOutput(text, callback) {
|
|
1030
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1031
|
+
const charCode = text.charCodeAt(index);
|
|
1032
|
+
this.assemblerLogs += String.fromCharCode(charCode);
|
|
1033
|
+
observeCallbackResult(callback(charCode));
|
|
775
1034
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
report: this.assemblerLogs
|
|
783
|
-
};
|
|
1035
|
+
}
|
|
1036
|
+
parseAssemblerDiagnostics(report, sourceProject) {
|
|
1037
|
+
return (this.mode.diagnosticsParser?.(report) ?? []).map((error) => ({
|
|
1038
|
+
...error,
|
|
1039
|
+
file: x86ProjectSourcePath(error.file, sourceProject)
|
|
1040
|
+
}));
|
|
784
1041
|
}
|
|
785
1042
|
loadElf(data) {
|
|
786
1043
|
if (this.state === "NOT_READY") throw new Error("Blink runtime is not ready");
|
|
@@ -1026,8 +1283,12 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
1026
1283
|
this.setState("PROGRAM_STOPPED");
|
|
1027
1284
|
}
|
|
1028
1285
|
handleAssemblerExit(code) {
|
|
1286
|
+
this.collectAssemblerDiagnostics();
|
|
1029
1287
|
if (code !== 0) {
|
|
1030
|
-
this.
|
|
1288
|
+
this.setState("READY");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (this.assembleOnly) {
|
|
1031
1292
|
this.setState("READY");
|
|
1032
1293
|
return;
|
|
1033
1294
|
}
|
|
@@ -1121,6 +1382,31 @@ function defaultScheduler(callback) {
|
|
|
1121
1382
|
function defer() {
|
|
1122
1383
|
return new Promise((resolve) => defaultScheduler(resolve));
|
|
1123
1384
|
}
|
|
1385
|
+
/**
|
|
1386
|
+
* A build succeeded when nothing in it was an error. Warnings ride along on both
|
|
1387
|
+
* outcomes, because a program that assembles is exactly where they matter.
|
|
1388
|
+
*/
|
|
1389
|
+
function toCompileResult(diagnostics, report) {
|
|
1390
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1391
|
+
if (errors.length === 0) return {
|
|
1392
|
+
ok: true,
|
|
1393
|
+
report,
|
|
1394
|
+
diagnostics
|
|
1395
|
+
};
|
|
1396
|
+
return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
errors,
|
|
1399
|
+
report,
|
|
1400
|
+
diagnostics
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
/** Detaches a Project from the caller, who is free to mutate its own copy afterwards. */
|
|
1404
|
+
function copyX86Project(project) {
|
|
1405
|
+
return {
|
|
1406
|
+
entry: project.entry,
|
|
1407
|
+
files: Object.fromEntries(Object.entries(project.files).map(([path, contents]) => [path, contents instanceof Uint8Array ? contents.slice() : contents]))
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1124
1410
|
//#endregion
|
|
1125
1411
|
//#region src/interface.ts
|
|
1126
1412
|
let RegisterSize = /* @__PURE__ */ function(RegisterSize) {
|
|
@@ -1408,21 +1694,28 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1408
1694
|
files: { "assembly.s": code }
|
|
1409
1695
|
});
|
|
1410
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Diagnostics only: this assembles but does not link, and for an assembler
|
|
1699
|
+
* that runs as its own wasm module it does not touch blink at all, so a
|
|
1700
|
+
* loaded or paused program survives a check.
|
|
1701
|
+
*/
|
|
1411
1702
|
async checkProject(project) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1703
|
+
return (await this.runtime.checkProject(project)).diagnostics.map((diagnostic) => {
|
|
1704
|
+
const path = diagnostic.file ?? project.entry;
|
|
1705
|
+
const lineIndex = Math.max(0, diagnostic.line - 1);
|
|
1706
|
+
const line = x86ProjectText(project, path).split(/\r?\n/)[lineIndex] ?? "";
|
|
1416
1707
|
return {
|
|
1417
1708
|
file: path,
|
|
1418
1709
|
lineIndex,
|
|
1419
|
-
column:
|
|
1710
|
+
column: locateDiagnosticColumn(diagnostic.error, line, diagnostic.warningClass),
|
|
1420
1711
|
line: {
|
|
1421
|
-
line
|
|
1712
|
+
line,
|
|
1422
1713
|
line_index: lineIndex
|
|
1423
1714
|
},
|
|
1424
|
-
message:
|
|
1425
|
-
formatted:
|
|
1715
|
+
message: diagnostic.error,
|
|
1716
|
+
formatted: diagnostic.error,
|
|
1717
|
+
severity: diagnostic.severity,
|
|
1718
|
+
...diagnostic.warningClass ? { code: diagnostic.warningClass } : {}
|
|
1426
1719
|
};
|
|
1427
1720
|
});
|
|
1428
1721
|
}
|
|
@@ -1727,6 +2020,6 @@ async function createX86Emulator(options = {}) {
|
|
|
1727
2020
|
return X86Emulator.create(options);
|
|
1728
2021
|
}
|
|
1729
2022
|
//#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 };
|
|
2023
|
+
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_PROJECT_ROOT, X86_REGISTER_NAMES, assemblers, createX86Emulator, defaultResourceUrl, fasmDiagnostics, gnuDiagnostics, isNodeRuntime, locateDiagnosticColumn, nasmDiagnostics, observeCallbackResult, readResourceBytes, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
|
|
1731
2024
|
|
|
1732
2025
|
//# sourceMappingURL=index.mjs.map
|