@specy/x86 2.1.0 → 2.3.0

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