@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/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. 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";
@@ -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) diagnostics.push({
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: (match[4] ?? "").trim()
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) diagnostics.push({
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/project.ts
474
- const X86_PROJECT_ROOT = "/__x86_project";
475
- const stagedFiles = /* @__PURE__ */ new WeakMap();
476
- function validateX86Project(project) {
477
- if (!isProjectPath(project.entry)) throw new Error(`Invalid x86 Project entry path: ${project.entry}`);
478
- if (!Object.hasOwn(project.files, project.entry)) throw new Error(`x86 Project entry file not found: ${project.entry}`);
479
- if (typeof project.files[project.entry] !== "string") throw new Error(`x86 Project entry must be a text file: ${project.entry}`);
480
- for (const [path, contents] of Object.entries(project.files)) {
481
- if (!isProjectPath(path)) throw new Error(`Invalid x86 Project file path: ${path}`);
482
- if (typeof contents !== "string" && !(contents instanceof Uint8Array)) throw new Error(`Invalid x86 Project file contents: ${path}`);
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
- * Writes a Project into the emulator filesystem and selects the Entry directory as NASM's working
487
- * directory. NASM consequently owns `%include` and `incbin` resolution, including paths produced by
488
- * preprocessor macros.
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 stageX86Project(fs, project) {
491
- validateX86Project(project);
492
- const previous = stagedFiles.get(fs) ?? /* @__PURE__ */ new Set();
493
- const current = /* @__PURE__ */ new Set();
494
- fs.mkdirTree(X86_PROJECT_ROOT);
495
- for (const [path, contents] of Object.entries(project.files)) {
496
- const absolutePath = `${X86_PROJECT_ROOT}/${path}`;
497
- const separator = absolutePath.lastIndexOf("/");
498
- if (separator > 0) fs.mkdirTree(absolutePath.slice(0, separator));
499
- fs.writeFile(absolutePath, contents);
500
- current.add(absolutePath);
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
- fs.writeFile("/assembly.s", project.files[project.entry]);
509
- fs.chdir(projectDirectory(project.entry));
510
- stagedFiles.set(fs, current);
691
+ const allowed = target.length <= 4 ? 1 : 2;
692
+ return bestDistance <= allowed ? best : null;
511
693
  }
512
- /** Maps a path emitted by NASM or DWARF back to the exact Project path. */
513
- function x86ProjectSourcePath(sourcePath, project) {
514
- if (!sourcePath || sourcePath === "/assembly.s" || sourcePath === "assembly.s") return project.entry;
515
- const withoutLeadingSlash = (sourcePath.startsWith(`/__x86_project/`) ? sourcePath.slice(15) : sourcePath).replace(/^\/+/, "");
516
- const relativeToEntry = resolvePath(parentPath(project.entry), withoutLeadingSlash);
517
- if (relativeToEntry && relativeToEntry in project.files) return relativeToEntry;
518
- const relativeToRoot = resolvePath("", withoutLeadingSlash);
519
- if (relativeToRoot && relativeToRoot in project.files) return relativeToRoot;
520
- const suffix = `/${withoutLeadingSlash}`;
521
- const suffixMatches = Object.keys(project.files).filter((path) => path === withoutLeadingSlash || path.endsWith(suffix));
522
- return suffixMatches.length === 1 ? suffixMatches[0] : project.entry;
523
- }
524
- function projectDirectory(entry) {
525
- const parent = parentPath(entry);
526
- return parent ? `${X86_PROJECT_ROOT}/${parent}` : X86_PROJECT_ROOT;
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 parts.join("/") || null;
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.setState("ASSEMBLING");
937
+ this.assembleOnly = !options.link;
761
938
  try {
762
- await defer();
763
- this.setEmulationArgs("/assembler", this.mode.binaries.assembler.commands, "");
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
- if (this.state === "PROGRAM_LOADED") {
770
- this.sourceMap = this.tryReadSourceMap();
771
- return {
772
- ok: true,
773
- report: this.assemblerLogs
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
- return {
777
- ok: false,
778
- errors: this.assemblerErrors.map((error) => ({
779
- ...error,
780
- file: x86ProjectSourcePath(error.file, sourceProject)
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.assemblerErrors = this.mode.diagnosticsParser?.(this.assemblerLogs) ?? [];
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
- const result = await this.compileProject(project);
1413
- return (result.ok === false ? result.errors : []).map((error) => {
1414
- const path = error.file ?? project.entry;
1415
- const lineIndex = Math.max(0, error.line - 1);
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: 0,
1710
+ column: locateDiagnosticColumn(diagnostic.error, line, diagnostic.warningClass),
1420
1711
  line: {
1421
- line: x86ProjectText(project, path).split(/\r?\n/)[lineIndex] ?? "",
1712
+ line,
1422
1713
  line_index: lineIndex
1423
1714
  },
1424
- message: error.error,
1425
- formatted: error.error
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