@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/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. Both compiled as a static MUSL binary",
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
- assembler: {
38
- file: assemblerAsset("nasm.3.00.elf"),
39
- commands: "/assembler -g -F dwarf -felf64 /assembly.s -o /program.o"
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 = /^.*:(\d+): (error|warning): (.*)$/;
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) diagnostics.push({
56
- line: Number.parseInt(match[1] ?? "0", 10),
57
- error: (match[3] ?? "").trim()
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 = /\/assembly\.s:(\d+): (Error: .+)/;
193
+ const regex = /^(.*):(\d+): (Error|Warning): (.+)$/;
66
194
  for (const line of lines) {
67
195
  const match = line.match(regex);
68
- if (match) diagnostics.push({
69
- line: Number.parseInt(match[1] ?? "0", 10),
70
- error: match[2] ?? ""
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 = /\/assembly\.s \[(\d+)\]:/;
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) lineNumber = Number.parseInt(lineMatch[1] ?? "0", 10);
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?.lineIndex ?? null;
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?.name
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.module.FS.writeFile("/assembly.s", code);
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.state === "PROGRAM_LOADED") {
641
- this.sourceMap = this.tryReadSourceMap();
642
- return {
643
- ok: true,
644
- report: this.assemblerLogs
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
- return {
648
- ok: false,
649
- errors: this.assemblerErrors,
650
- report: this.assemblerLogs
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.sourceMap?.getLineIndex(address) ?? null;
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
- pauseForBreakpoint(address, lineNumber) {
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: this.getSourceLineForAddress(address) ?? void 0,
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 lineNumber = this.getSourceLineForAddress(stop.address) ?? void 0;
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.assemblerErrors = this.mode.diagnosticsParser?.(this.assemblerLogs) ?? [];
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 = code;
1206
- this.lastCompileResult = await this.runtime.compileAssembly(code);
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
- const result = await this.compile(code);
1248
- const errors = result.ok === false ? result.errors : [];
1249
- const lines = code.split(/\r?\n/);
1250
- return errors.map((error) => ({
1251
- lineIndex: Math.max(0, error.line - 1),
1252
- column: 0,
1253
- line: {
1254
- line: lines[Math.max(0, error.line - 1)] ?? "",
1255
- line_index: Math.max(0, error.line - 1)
1256
- },
1257
- message: error.error,
1258
- formatted: error.error
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: this.runtime.getSourceLineForAddress(instruction.address) ?? -1,
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 lineIndex of breakpoints) {
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
- for (const address of this.runtime.getAddressesForSourceLine(lineIndex)) addresses.set(address.toString(), address);
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.getSourceLineForAddress(pc) ?? void 0);
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.getSourceLineForAddress(pc) ?? void 0);
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: this.runtime.getSourceLineForAddress(pcBefore) ?? -1,
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: this.runtime.getSourceLineForAddress(frameAddress) ?? -1,
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