@specy/x86 2.0.5 → 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/README.md +117 -117
- package/dist/assets/assemblers/fasm.1.73.32.elf +0 -0
- package/dist/assets/assemblers/gnu-as.2.43.50.elf +0 -0
- package/dist/assets/assemblers/gnu-ld.2.43.50.elf +0 -0
- package/dist/index.d.mts +371 -224
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +548 -69
- package/dist/index.mjs.map +1 -1
- package/dist/wasm/blinkenlib.d.ts +2 -2
- package/dist/wasm/blinkenlib.js +0 -0
- 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 +8 -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,30 +157,32 @@ 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";
|
|
49
170
|
function nasmDiagnostics(str) {
|
|
50
171
|
const diagnostics = [];
|
|
51
172
|
const lines = str.split(/\r?\n/);
|
|
52
|
-
const regex =
|
|
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)
|
|
56
|
-
|
|
57
|
-
|
|
177
|
+
if (!match) continue;
|
|
178
|
+
const text = (match[4] ?? "").trim();
|
|
179
|
+
const classMatch = text.match(classRegex);
|
|
180
|
+
diagnostics.push({
|
|
181
|
+
file: match[1] || void 0,
|
|
182
|
+
line: Number.parseInt(match[2] ?? "0", 10),
|
|
183
|
+
error: classMatch ? text.slice(0, classMatch.index).trim() : text,
|
|
184
|
+
severity: match[3] === "warning" ? "warning" : "error",
|
|
185
|
+
...classMatch?.[1] ? { warningClass: classMatch[1] } : {}
|
|
58
186
|
});
|
|
59
187
|
}
|
|
60
188
|
return diagnostics;
|
|
@@ -62,36 +190,81 @@ function nasmDiagnostics(str) {
|
|
|
62
190
|
function gnuDiagnostics(str) {
|
|
63
191
|
const diagnostics = [];
|
|
64
192
|
const lines = str.split("\n");
|
|
65
|
-
const regex =
|
|
193
|
+
const regex = /^(.*):(\d+): (Error|Warning): (.+)$/;
|
|
66
194
|
for (const line of lines) {
|
|
67
195
|
const match = line.match(regex);
|
|
68
|
-
if (match)
|
|
69
|
-
|
|
70
|
-
|
|
196
|
+
if (!match) continue;
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
file: match[1] || void 0,
|
|
199
|
+
line: Number.parseInt(match[2] ?? "0", 10),
|
|
200
|
+
error: `${match[3]}: ${match[4]}`,
|
|
201
|
+
severity: match[3] === "Warning" ? "warning" : "error"
|
|
71
202
|
});
|
|
72
203
|
}
|
|
73
204
|
return diagnostics;
|
|
74
205
|
}
|
|
75
206
|
function fasmDiagnostics(str) {
|
|
76
207
|
const diagnostics = [];
|
|
77
|
-
const lineRegex =
|
|
208
|
+
const lineRegex = /^(.*) \[(\d+)\]:/;
|
|
78
209
|
const errorRegex = /error: .+/;
|
|
79
210
|
const lines = str.split("\n");
|
|
80
211
|
let lineNumber = null;
|
|
212
|
+
let file;
|
|
81
213
|
for (const line of lines) {
|
|
82
214
|
const lineMatch = line.match(lineRegex);
|
|
83
215
|
const errorMatch = line.match(errorRegex);
|
|
84
|
-
if (lineMatch)
|
|
216
|
+
if (lineMatch) {
|
|
217
|
+
file = lineMatch[1] || void 0;
|
|
218
|
+
lineNumber = Number.parseInt(lineMatch[2] ?? "0", 10);
|
|
219
|
+
}
|
|
85
220
|
if (errorMatch && lineNumber !== null) {
|
|
86
221
|
diagnostics.push({
|
|
222
|
+
file,
|
|
87
223
|
line: lineNumber,
|
|
88
|
-
error: errorMatch[0]
|
|
224
|
+
error: errorMatch[0],
|
|
225
|
+
severity: "error"
|
|
89
226
|
});
|
|
90
227
|
lineNumber = null;
|
|
228
|
+
file = void 0;
|
|
91
229
|
}
|
|
92
230
|
}
|
|
93
231
|
return diagnostics;
|
|
94
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
|
+
}
|
|
95
268
|
//#endregion
|
|
96
269
|
//#region \0wasm-helpers.js
|
|
97
270
|
function loadWasmModule(sync, filepath, src, imports) {
|
|
@@ -170,6 +343,9 @@ var SourceMap = class {
|
|
|
170
343
|
return this.entries.length;
|
|
171
344
|
}
|
|
172
345
|
getLineIndex(address) {
|
|
346
|
+
return this.getLocation(address)?.lineIndex ?? null;
|
|
347
|
+
}
|
|
348
|
+
getLocation(address) {
|
|
173
349
|
let left = 0;
|
|
174
350
|
let right = this.entries.length - 1;
|
|
175
351
|
let match = null;
|
|
@@ -182,13 +358,19 @@ var SourceMap = class {
|
|
|
182
358
|
left = middle + 1;
|
|
183
359
|
} else right = middle - 1;
|
|
184
360
|
}
|
|
185
|
-
return match
|
|
361
|
+
return match ? { ...match } : null;
|
|
186
362
|
}
|
|
187
363
|
getAddressesForLine(lineIndex) {
|
|
364
|
+
return this.getAddressesForLocation(lineIndex);
|
|
365
|
+
}
|
|
366
|
+
getAddressesForLocation(lineIndex, file) {
|
|
367
|
+
return this.getAddressesMatching(lineIndex, (candidate) => file === void 0 || candidate === file);
|
|
368
|
+
}
|
|
369
|
+
getAddressesMatching(lineIndex, matchesFile) {
|
|
188
370
|
const addresses = [];
|
|
189
371
|
const seen = /* @__PURE__ */ new Set();
|
|
190
372
|
for (const entry of this.entries) {
|
|
191
|
-
if (entry.lineIndex !== lineIndex) continue;
|
|
373
|
+
if (entry.lineIndex !== lineIndex || !matchesFile(entry.file)) continue;
|
|
192
374
|
const key = entry.address.toString();
|
|
193
375
|
if (seen.has(key)) continue;
|
|
194
376
|
seen.add(key);
|
|
@@ -196,6 +378,16 @@ var SourceMap = class {
|
|
|
196
378
|
}
|
|
197
379
|
return addresses;
|
|
198
380
|
}
|
|
381
|
+
getAddresses() {
|
|
382
|
+
const addresses = [];
|
|
383
|
+
let previous;
|
|
384
|
+
for (const entry of this.entries) {
|
|
385
|
+
if (entry.address === previous) continue;
|
|
386
|
+
addresses.push(entry.address);
|
|
387
|
+
previous = entry.address;
|
|
388
|
+
}
|
|
389
|
+
return addresses;
|
|
390
|
+
}
|
|
199
391
|
};
|
|
200
392
|
function parseSourceMap(bytes) {
|
|
201
393
|
const section = findElfSection(bytes, ".debug_line");
|
|
@@ -302,7 +494,7 @@ function parseDebugLine(bytes) {
|
|
|
302
494
|
entries.push({
|
|
303
495
|
address,
|
|
304
496
|
lineIndex: Math.max(0, line - 1),
|
|
305
|
-
file: sourceFile
|
|
497
|
+
file: dwarfSourcePath(sourceFile)
|
|
306
498
|
});
|
|
307
499
|
};
|
|
308
500
|
const advanceAddress = (operationAdvance) => {
|
|
@@ -392,6 +584,11 @@ function parseDebugLine(bytes) {
|
|
|
392
584
|
}
|
|
393
585
|
return entries;
|
|
394
586
|
}
|
|
587
|
+
function dwarfSourcePath(sourceFile) {
|
|
588
|
+
if (!sourceFile?.name) return void 0;
|
|
589
|
+
if (!sourceFile.directory || sourceFile.name.startsWith("/")) return sourceFile.name;
|
|
590
|
+
return `${sourceFile.directory.replace(/\/$/, "")}/${sourceFile.name}`;
|
|
591
|
+
}
|
|
395
592
|
function readU64Number(view, offset) {
|
|
396
593
|
const value = view.getBigUint64(offset, true);
|
|
397
594
|
if (value > BigInt(Number.MAX_SAFE_INTEGER)) return Number.MAX_SAFE_INTEGER;
|
|
@@ -438,6 +635,75 @@ function compareBigInt(left, right) {
|
|
|
438
635
|
return 0;
|
|
439
636
|
}
|
|
440
637
|
//#endregion
|
|
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;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
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.
|
|
678
|
+
*/
|
|
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
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const allowed = target.length <= 4 ? 1 : 2;
|
|
692
|
+
return bestDistance <= allowed ? best : null;
|
|
693
|
+
}
|
|
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;
|
|
703
|
+
}
|
|
704
|
+
return previous[right.length];
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
441
707
|
//#region src/types.ts
|
|
442
708
|
const X86_REGISTER_NAMES = [
|
|
443
709
|
"rax",
|
|
@@ -569,12 +835,18 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
569
835
|
state = "NOT_READY";
|
|
570
836
|
stopReason = null;
|
|
571
837
|
assemblerLogs = "";
|
|
838
|
+
/** The subset of `assemblerDiagnostics` that stopped the build. */
|
|
572
839
|
assemblerErrors = [];
|
|
840
|
+
/** Everything the assembler said, warnings included. */
|
|
841
|
+
assemblerDiagnostics = [];
|
|
573
842
|
callbacks;
|
|
574
843
|
scheduler;
|
|
575
844
|
stateWaiters = [];
|
|
576
845
|
stdinBytes = [];
|
|
577
846
|
sourceMap = null;
|
|
847
|
+
sourceProject = null;
|
|
848
|
+
assembleOnly = false;
|
|
849
|
+
assembledObject = null;
|
|
578
850
|
defaultArgc = "/program";
|
|
579
851
|
defaultArgv = "";
|
|
580
852
|
constructor(module, mode, callbacks, scheduler) {
|
|
@@ -621,40 +893,158 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
621
893
|
this.assemblerLogs = "";
|
|
622
894
|
this.assemblerErrors = [];
|
|
623
895
|
this.setState("NOT_READY");
|
|
624
|
-
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));
|
|
625
897
|
if (this.mode.binaries.linker) await this.writeExecutable("/linker", await readResourceBytes(this.mode.binaries.linker.file));
|
|
626
898
|
this.setState("READY");
|
|
627
899
|
}
|
|
628
900
|
async compileAssembly(code) {
|
|
901
|
+
return this.compileProject({
|
|
902
|
+
entry: "assembly.s",
|
|
903
|
+
files: { "assembly.s": code }
|
|
904
|
+
});
|
|
905
|
+
}
|
|
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) {
|
|
629
928
|
this.assertReadyForCompile();
|
|
929
|
+
const sourceProject = copyX86Project(project);
|
|
930
|
+
this.sourceProject = sourceProject;
|
|
630
931
|
this.stopReason = null;
|
|
631
932
|
this.assemblerLogs = "";
|
|
632
933
|
this.assemblerErrors = [];
|
|
934
|
+
this.assemblerDiagnostics = [];
|
|
935
|
+
this.assembledObject = null;
|
|
633
936
|
this.sourceMap = null;
|
|
634
|
-
this.
|
|
937
|
+
this.assembleOnly = !options.link;
|
|
938
|
+
try {
|
|
939
|
+
if (this.mode.wasmAssembler) await this.assembleWithWasmAssembler(sourceProject, options);
|
|
940
|
+
else await this.assembleInBlink(sourceProject);
|
|
941
|
+
} finally {
|
|
942
|
+
this.assembleOnly = false;
|
|
943
|
+
this.module.FS.chdir("/");
|
|
944
|
+
}
|
|
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);
|
|
635
988
|
this.setState("ASSEMBLING");
|
|
636
989
|
await defer();
|
|
637
990
|
this.setEmulationArgs("/assembler", this.mode.binaries.assembler.commands, "");
|
|
638
991
|
this.module._blinkenlib_run_fast();
|
|
639
992
|
await this.waitForState((state) => state !== "ASSEMBLING" && state !== "LINKING");
|
|
640
|
-
if (this.
|
|
641
|
-
this.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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));
|
|
646
1034
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1035
|
+
}
|
|
1036
|
+
parseAssemblerDiagnostics(report, sourceProject) {
|
|
1037
|
+
return (this.mode.diagnosticsParser?.(report) ?? []).map((error) => ({
|
|
1038
|
+
...error,
|
|
1039
|
+
file: x86ProjectSourcePath(error.file, sourceProject)
|
|
1040
|
+
}));
|
|
652
1041
|
}
|
|
653
1042
|
loadElf(data) {
|
|
654
1043
|
if (this.state === "NOT_READY") throw new Error("Blink runtime is not ready");
|
|
655
1044
|
this.writeExecutableSync("/program", data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
656
1045
|
this.stopReason = null;
|
|
657
1046
|
this.sourceMap = null;
|
|
1047
|
+
this.sourceProject = null;
|
|
658
1048
|
this.setState("PROGRAM_LOADED");
|
|
659
1049
|
}
|
|
660
1050
|
starti() {
|
|
@@ -745,30 +1135,48 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
745
1135
|
return this.module.blinkenlibResolveSymbol(address);
|
|
746
1136
|
}
|
|
747
1137
|
getSourceLineForAddress(address) {
|
|
748
|
-
return this.
|
|
1138
|
+
return this.getSourceLocationForAddress(address)?.line ?? null;
|
|
1139
|
+
}
|
|
1140
|
+
getSourceLocationForAddress(address) {
|
|
1141
|
+
const location = this.sourceMap?.getLocation(address);
|
|
1142
|
+
if (!location) return null;
|
|
1143
|
+
return {
|
|
1144
|
+
path: this.sourceProject ? x86ProjectSourcePath(location.file, this.sourceProject) : location.file ?? "assembly.s",
|
|
1145
|
+
line: location.lineIndex
|
|
1146
|
+
};
|
|
749
1147
|
}
|
|
750
1148
|
getAddressesForSourceLine(lineIndex) {
|
|
751
1149
|
return this.sourceMap?.getAddressesForLine(lineIndex) ?? [];
|
|
752
1150
|
}
|
|
753
|
-
|
|
1151
|
+
getAddressesForSourceLocation(location) {
|
|
1152
|
+
if (!this.sourceMap || !this.sourceProject) return [];
|
|
1153
|
+
return this.sourceMap.getAddressesMatching(location.line, (file) => x86ProjectSourcePath(file, this.sourceProject) === location.path);
|
|
1154
|
+
}
|
|
1155
|
+
getSourceMappedAddresses() {
|
|
1156
|
+
return this.sourceMap?.getAddresses() ?? [];
|
|
1157
|
+
}
|
|
1158
|
+
pauseForBreakpoint(address, location) {
|
|
754
1159
|
this.stopReason = {
|
|
755
1160
|
loadFail: false,
|
|
756
1161
|
exitCode: 0,
|
|
757
1162
|
kind: "breakpoint",
|
|
758
1163
|
details: `execution paused at breakpoint 0x${address.toString(16)}`,
|
|
759
1164
|
address,
|
|
760
|
-
lineNumber
|
|
1165
|
+
lineNumber: location?.line,
|
|
1166
|
+
file: location?.path
|
|
761
1167
|
};
|
|
762
1168
|
this.setState("PROGRAM_PAUSED");
|
|
763
1169
|
}
|
|
764
1170
|
pauseForLimit(address, executedInstructions) {
|
|
1171
|
+
const location = this.getSourceLocationForAddress(address);
|
|
765
1172
|
this.stopReason = {
|
|
766
1173
|
loadFail: false,
|
|
767
1174
|
exitCode: 0,
|
|
768
1175
|
kind: "limit",
|
|
769
1176
|
details: `execution paused after ${executedInstructions.toString()} instructions`,
|
|
770
1177
|
address,
|
|
771
|
-
lineNumber:
|
|
1178
|
+
lineNumber: location?.line,
|
|
1179
|
+
file: location?.path,
|
|
772
1180
|
executedInstructions
|
|
773
1181
|
};
|
|
774
1182
|
this.setState("PROGRAM_PAUSED");
|
|
@@ -842,14 +1250,15 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
842
1250
|
}
|
|
843
1251
|
if (code === SIGTRAP_CODES.BLINK_BREAKPOINT || code === SIGTRAP_CODES.BLINK_RUN_LIMIT) {
|
|
844
1252
|
const stop = this.module.blinkenlibGetRunStop();
|
|
845
|
-
const
|
|
1253
|
+
const location = this.getSourceLocationForAddress(stop.address);
|
|
846
1254
|
this.stopReason = {
|
|
847
1255
|
loadFail: false,
|
|
848
1256
|
exitCode: 0,
|
|
849
1257
|
kind: stop.kind === "limit" ? "limit" : "breakpoint",
|
|
850
1258
|
details: stop.kind === "limit" ? `execution paused after ${stop.executedInstructions.toString()} instructions` : `execution paused at breakpoint 0x${stop.address.toString(16)}`,
|
|
851
1259
|
address: stop.address,
|
|
852
|
-
lineNumber,
|
|
1260
|
+
lineNumber: location?.line,
|
|
1261
|
+
file: location?.path,
|
|
853
1262
|
executedInstructions: stop.executedInstructions
|
|
854
1263
|
};
|
|
855
1264
|
this.setState("PROGRAM_PAUSED");
|
|
@@ -874,8 +1283,12 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
874
1283
|
this.setState("PROGRAM_STOPPED");
|
|
875
1284
|
}
|
|
876
1285
|
handleAssemblerExit(code) {
|
|
1286
|
+
this.collectAssemblerDiagnostics();
|
|
877
1287
|
if (code !== 0) {
|
|
878
|
-
this.
|
|
1288
|
+
this.setState("READY");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (this.assembleOnly) {
|
|
879
1292
|
this.setState("READY");
|
|
880
1293
|
return;
|
|
881
1294
|
}
|
|
@@ -969,6 +1382,31 @@ function defaultScheduler(callback) {
|
|
|
969
1382
|
function defer() {
|
|
970
1383
|
return new Promise((resolve) => defaultScheduler(resolve));
|
|
971
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
|
+
}
|
|
972
1410
|
//#endregion
|
|
973
1411
|
//#region src/interface.ts
|
|
974
1412
|
let RegisterSize = /* @__PURE__ */ function(RegisterSize) {
|
|
@@ -1101,7 +1539,8 @@ function stripPrivateHistory(entry) {
|
|
|
1101
1539
|
pc: entry.pc,
|
|
1102
1540
|
old_ccr: entry.old_ccr,
|
|
1103
1541
|
new_ccr: entry.new_ccr,
|
|
1104
|
-
line: entry.line
|
|
1542
|
+
line: entry.line,
|
|
1543
|
+
file: entry.file
|
|
1105
1544
|
};
|
|
1106
1545
|
}
|
|
1107
1546
|
function toHistoryPc(address) {
|
|
@@ -1201,9 +1640,15 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1201
1640
|
await this.runtime.setMode(mode);
|
|
1202
1641
|
}
|
|
1203
1642
|
async compile(code) {
|
|
1643
|
+
return this.compileProject({
|
|
1644
|
+
entry: "assembly.s",
|
|
1645
|
+
files: { "assembly.s": code }
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
async compileProject(project) {
|
|
1204
1649
|
this.clearExecutionTrace();
|
|
1205
|
-
this.lastSourceCode =
|
|
1206
|
-
this.lastCompileResult = await this.runtime.
|
|
1650
|
+
this.lastSourceCode = x86ProjectText(project, project.entry);
|
|
1651
|
+
this.lastCompileResult = await this.runtime.compileProject(project);
|
|
1207
1652
|
return this.lastCompileResult;
|
|
1208
1653
|
}
|
|
1209
1654
|
loadElf(data) {
|
|
@@ -1244,19 +1689,35 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1244
1689
|
return String(error);
|
|
1245
1690
|
}
|
|
1246
1691
|
async checkCode(code) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1692
|
+
return this.checkProject({
|
|
1693
|
+
entry: "assembly.s",
|
|
1694
|
+
files: { "assembly.s": code }
|
|
1695
|
+
});
|
|
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
|
+
*/
|
|
1702
|
+
async checkProject(project) {
|
|
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] ?? "";
|
|
1707
|
+
return {
|
|
1708
|
+
file: path,
|
|
1709
|
+
lineIndex,
|
|
1710
|
+
column: locateDiagnosticColumn(diagnostic.error, line, diagnostic.warningClass),
|
|
1711
|
+
line: {
|
|
1712
|
+
line,
|
|
1713
|
+
line_index: lineIndex
|
|
1714
|
+
},
|
|
1715
|
+
message: diagnostic.error,
|
|
1716
|
+
formatted: diagnostic.error,
|
|
1717
|
+
severity: diagnostic.severity,
|
|
1718
|
+
...diagnostic.warningClass ? { code: diagnostic.warningClass } : {}
|
|
1719
|
+
};
|
|
1720
|
+
});
|
|
1260
1721
|
}
|
|
1261
1722
|
undo() {
|
|
1262
1723
|
const entry = this.history.pop();
|
|
@@ -1329,12 +1790,24 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1329
1790
|
getInstructionAt(address) {
|
|
1330
1791
|
const instruction = this.runtime.getInstructionAt(address);
|
|
1331
1792
|
if (!instruction) return null;
|
|
1793
|
+
const location = this.runtime.getSourceLocationForAddress(instruction.address);
|
|
1332
1794
|
return {
|
|
1333
1795
|
address: instruction.address,
|
|
1334
|
-
lineNumber:
|
|
1796
|
+
lineNumber: location?.line ?? -1,
|
|
1797
|
+
file: location?.path,
|
|
1798
|
+
size: instruction.size,
|
|
1799
|
+
bytes: this.runtime.readMemoryBytes(instruction.address, BigInt(instruction.size)),
|
|
1335
1800
|
code: instruction.code
|
|
1336
1801
|
};
|
|
1337
1802
|
}
|
|
1803
|
+
/** Every instruction address represented in the linked program's DWARF source map. */
|
|
1804
|
+
getCompiledInstructions() {
|
|
1805
|
+
if (this.runtime.state === "PROGRAM_LOADED") this.runtime.pauseAtEntry();
|
|
1806
|
+
return this.runtime.getSourceMappedAddresses().flatMap((address) => {
|
|
1807
|
+
const instruction = this.getInstructionAt(address);
|
|
1808
|
+
return instruction ? [instruction] : [];
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1338
1811
|
getRegisterValues() {
|
|
1339
1812
|
return X86_REGISTER_NAMES.map((register) => this.getRegisterValue(register));
|
|
1340
1813
|
}
|
|
@@ -1370,9 +1843,11 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1370
1843
|
}
|
|
1371
1844
|
resolveBreakpointAddresses(breakpoints) {
|
|
1372
1845
|
const addresses = /* @__PURE__ */ new Map();
|
|
1373
|
-
for (const
|
|
1846
|
+
for (const breakpoint of breakpoints) {
|
|
1847
|
+
const lineIndex = typeof breakpoint === "number" ? breakpoint : breakpoint.line;
|
|
1374
1848
|
if (!Number.isSafeInteger(lineIndex) || lineIndex < 0) throw new Error(`Invalid breakpoint line index: ${lineIndex}`);
|
|
1375
|
-
|
|
1849
|
+
const resolved = typeof breakpoint === "number" ? this.runtime.getAddressesForSourceLine(lineIndex) : this.runtime.getAddressesForSourceLocation(breakpoint);
|
|
1850
|
+
for (const address of resolved) addresses.set(address.toString(), address);
|
|
1376
1851
|
}
|
|
1377
1852
|
return [...addresses.values()];
|
|
1378
1853
|
}
|
|
@@ -1384,7 +1859,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1384
1859
|
while (this.runtime.state === "PROGRAM_RUNNING") {
|
|
1385
1860
|
const pc = this.getPc();
|
|
1386
1861
|
if (breakpointSet.has(pc.toString())) {
|
|
1387
|
-
this.runtime.pauseForBreakpoint(pc, this.runtime.
|
|
1862
|
+
this.runtime.pauseForBreakpoint(pc, this.runtime.getSourceLocationForAddress(pc) ?? void 0);
|
|
1388
1863
|
return this.getStatus();
|
|
1389
1864
|
}
|
|
1390
1865
|
if (hasLimit && executedInstructions >= limit) {
|
|
@@ -1405,7 +1880,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1405
1880
|
while (this.runtime.state === "PROGRAM_RUNNING") {
|
|
1406
1881
|
const pc = this.getPc();
|
|
1407
1882
|
if (breakpointSet.has(pc.toString())) {
|
|
1408
|
-
this.runtime.pauseForBreakpoint(pc, this.runtime.
|
|
1883
|
+
this.runtime.pauseForBreakpoint(pc, this.runtime.getSourceLocationForAddress(pc) ?? void 0);
|
|
1409
1884
|
return this.getStatus();
|
|
1410
1885
|
}
|
|
1411
1886
|
if (hasLimit && executedInstructions >= limit) {
|
|
@@ -1463,12 +1938,14 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1463
1938
|
}
|
|
1464
1939
|
const memoryWrites = nativeStep.valid ? this.recordMemoryMutations(nativeStep.memoryWrites, mutations) : [];
|
|
1465
1940
|
if (nativeStep.valid) this.recordControlFlowMutation(nativeStep, instruction, mutations);
|
|
1941
|
+
const location = this.runtime.getSourceLocationForAddress(pcBefore);
|
|
1466
1942
|
const entry = {
|
|
1467
1943
|
mutations,
|
|
1468
1944
|
pc: toHistoryPc(pcBefore),
|
|
1469
1945
|
old_ccr: { bits: flagsBefore },
|
|
1470
1946
|
new_ccr: { bits: flagsAfter },
|
|
1471
|
-
line:
|
|
1947
|
+
line: location?.line ?? -1,
|
|
1948
|
+
file: location?.path,
|
|
1472
1949
|
registersBefore,
|
|
1473
1950
|
flagsBefore,
|
|
1474
1951
|
callStackBefore,
|
|
@@ -1506,12 +1983,14 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
1506
1983
|
const returnAddress = instruction ? instruction.address + BigInt(instruction.size) : step.pcBefore;
|
|
1507
1984
|
const symbol = this.runtime.resolveSymbol(step.pcAfter);
|
|
1508
1985
|
const frameAddress = symbol?.address ?? step.pcAfter;
|
|
1986
|
+
const location = this.runtime.getSourceLocationForAddress(frameAddress);
|
|
1509
1987
|
this.callStack.push({
|
|
1510
1988
|
name: symbol?.name ?? "",
|
|
1511
1989
|
address: frameAddress,
|
|
1512
1990
|
destination: returnAddress,
|
|
1513
1991
|
sp: step.spAfter,
|
|
1514
|
-
line:
|
|
1992
|
+
line: location?.line ?? -1,
|
|
1993
|
+
file: location?.path,
|
|
1515
1994
|
color: makeFrameColor(this.callStack.length, frameAddress)
|
|
1516
1995
|
});
|
|
1517
1996
|
mutations.push({
|
|
@@ -1541,6 +2020,6 @@ async function createX86Emulator(options = {}) {
|
|
|
1541
2020
|
return X86Emulator.create(options);
|
|
1542
2021
|
}
|
|
1543
2022
|
//#endregion
|
|
1544
|
-
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_REGISTER_NAMES, assemblers, createX86Emulator, defaultResourceUrl, fasmDiagnostics, gnuDiagnostics, isNodeRuntime, nasmDiagnostics, observeCallbackResult, readResourceBytes, toResourceUrl };
|
|
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 };
|
|
1545
2024
|
|
|
1546
2025
|
//# sourceMappingURL=index.mjs.map
|