@razdolbai/merls 0.1.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.
Files changed (104) hide show
  1. package/.serena/memories/conventions.md +6 -0
  2. package/.serena/memories/core.md +8 -0
  3. package/.serena/memories/memory_maintenance.md +33 -0
  4. package/.serena/memories/suggested_commands.md +4 -0
  5. package/.serena/memories/task_completion.md +7 -0
  6. package/.serena/memories/tech_stack.md +6 -0
  7. package/.serena/project.yml +132 -0
  8. package/AGENTS.md +63 -0
  9. package/README.md +137 -0
  10. package/dist/src/asm/diagnostics.js +202 -0
  11. package/dist/src/asm/document.js +26 -0
  12. package/dist/src/asm/expression.js +163 -0
  13. package/dist/src/asm/lexer.js +122 -0
  14. package/dist/src/asm/local-labels.js +140 -0
  15. package/dist/src/asm/metadata.js +101 -0
  16. package/dist/src/asm/parser.js +118 -0
  17. package/dist/src/asm/symbols.js +40 -0
  18. package/dist/src/asm/syntax.js +44 -0
  19. package/dist/src/asm/workspace.js +73 -0
  20. package/dist/src/cli.js +21 -0
  21. package/dist/src/index.js +4 -0
  22. package/dist/src/lsp/completion.js +32 -0
  23. package/dist/src/lsp/diagnostics.js +63 -0
  24. package/dist/src/lsp/document-symbols.js +80 -0
  25. package/dist/src/lsp/hover.js +75 -0
  26. package/dist/src/lsp/symbol-navigation.js +181 -0
  27. package/dist/src/lsp/workspace-symbols.js +17 -0
  28. package/dist/src/server.js +77 -0
  29. package/dist/test/bootstrap.test.js +11 -0
  30. package/dist/test/cli-contract.test.js +74 -0
  31. package/dist/test/coc-config.test.js +21 -0
  32. package/dist/test/completion.test.js +126 -0
  33. package/dist/test/definition-references.test.js +126 -0
  34. package/dist/test/diagnostics.test.js +66 -0
  35. package/dist/test/document-model.test.js +30 -0
  36. package/dist/test/document-symbol.test.js +107 -0
  37. package/dist/test/expression.test.js +100 -0
  38. package/dist/test/fixture-corpus.test.js +33 -0
  39. package/dist/test/hover.test.js +142 -0
  40. package/dist/test/lexer.test.js +53 -0
  41. package/dist/test/line-parser.test.js +67 -0
  42. package/dist/test/local-labels.test.js +43 -0
  43. package/dist/test/metadata.test.js +27 -0
  44. package/dist/test/publish-diagnostics.test.js +137 -0
  45. package/dist/test/run-tests.js +132 -0
  46. package/dist/test/server-entrypoint.test.js +14 -0
  47. package/dist/test/server-initialize.test.js +77 -0
  48. package/dist/test/symbols.test.js +37 -0
  49. package/dist/test/syntax-shape.test.js +18 -0
  50. package/dist/test/workspace-symbol.test.js +113 -0
  51. package/dist/test/workspace.test.js +24 -0
  52. package/examples/coc-settings.json +18 -0
  53. package/package.json +26 -0
  54. package/publish.ps1 +9 -0
  55. package/src/asm/diagnostics.ts +294 -0
  56. package/src/asm/document.ts +43 -0
  57. package/src/asm/expression.ts +242 -0
  58. package/src/asm/lexer.ts +197 -0
  59. package/src/asm/local-labels.ts +204 -0
  60. package/src/asm/metadata.ts +150 -0
  61. package/src/asm/parser.ts +197 -0
  62. package/src/asm/symbols.ts +55 -0
  63. package/src/asm/syntax.ts +76 -0
  64. package/src/asm/workspace.ts +105 -0
  65. package/src/cli.ts +24 -0
  66. package/src/index.ts +1 -0
  67. package/src/lsp/completion.ts +42 -0
  68. package/src/lsp/diagnostics.ts +82 -0
  69. package/src/lsp/document-symbols.ts +111 -0
  70. package/src/lsp/hover.ts +90 -0
  71. package/src/lsp/symbol-navigation.ts +244 -0
  72. package/src/lsp/workspace-symbols.ts +24 -0
  73. package/src/server.ts +121 -0
  74. package/test/bootstrap.test.ts +7 -0
  75. package/test/cli-contract.test.ts +94 -0
  76. package/test/coc-config.test.ts +28 -0
  77. package/test/completion.test.ts +151 -0
  78. package/test/definition-references.test.ts +152 -0
  79. package/test/diagnostics.test.ts +129 -0
  80. package/test/document-model.test.ts +29 -0
  81. package/test/document-symbol.test.ts +131 -0
  82. package/test/expression.test.ts +111 -0
  83. package/test/fixture-corpus.test.ts +33 -0
  84. package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
  85. package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
  86. package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
  87. package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
  88. package/test/fixtures/valid/smoke-test.asm +7 -0
  89. package/test/hover.test.ts +175 -0
  90. package/test/lexer.test.ts +87 -0
  91. package/test/line-parser.test.ts +69 -0
  92. package/test/local-labels.test.ts +47 -0
  93. package/test/metadata.test.ts +27 -0
  94. package/test/publish-diagnostics.test.ts +206 -0
  95. package/test/run-tests.ts +139 -0
  96. package/test/server-entrypoint.test.ts +11 -0
  97. package/test/server-initialize.test.ts +101 -0
  98. package/test/smoke/run-smoke.ps1 +177 -0
  99. package/test/smoke/vimrc +17 -0
  100. package/test/symbols.test.ts +41 -0
  101. package/test/syntax-shape.test.ts +18 -0
  102. package/test/workspace-symbol.test.ts +139 -0
  103. package/test/workspace.test.ts +29 -0
  104. package/tsconfig.json +16 -0
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runInitializeHandshakeTest = runInitializeHandshakeTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ function encodeMessage(message) {
11
+ const body = JSON.stringify(message);
12
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
13
+ }
14
+ async function runInitializeHandshakeTest() {
15
+ const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
16
+ const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
17
+ stdio: ["pipe", "pipe", "pipe"]
18
+ });
19
+ const response = new Promise((resolve, reject) => {
20
+ let stdout = "";
21
+ let stderr = "";
22
+ child.stdout.setEncoding("utf8");
23
+ child.stderr.setEncoding("utf8");
24
+ child.stdout.on("data", (chunk) => {
25
+ stdout += chunk;
26
+ const separator = stdout.indexOf("\r\n\r\n");
27
+ if (separator === -1) {
28
+ return;
29
+ }
30
+ const header = stdout.slice(0, separator);
31
+ const match = /Content-Length: (\d+)/i.exec(header);
32
+ if (!match) {
33
+ reject(new Error(`Missing Content-Length header in response: ${stdout}`));
34
+ return;
35
+ }
36
+ const length = Number(match[1]);
37
+ const body = stdout.slice(separator + 4);
38
+ if (Buffer.byteLength(body, "utf8") < length) {
39
+ return;
40
+ }
41
+ resolve(JSON.parse(body.slice(0, length)));
42
+ });
43
+ child.stderr.on("data", (chunk) => {
44
+ stderr += chunk;
45
+ });
46
+ child.once("error", reject);
47
+ child.once("exit", (code) => {
48
+ reject(new Error(`Server exited before initialize response. code=${code}, stderr=${stderr}`));
49
+ });
50
+ });
51
+ child.stdin.write(encodeMessage({
52
+ id: 1,
53
+ jsonrpc: "2.0",
54
+ method: "initialize",
55
+ params: {
56
+ capabilities: {},
57
+ clientInfo: {
58
+ name: "merls-test"
59
+ },
60
+ processId: process.pid,
61
+ rootUri: null
62
+ }
63
+ }));
64
+ try {
65
+ const message = await response;
66
+ strict_1.default.equal(message.id, 1);
67
+ strict_1.default.equal(message.jsonrpc, "2.0");
68
+ strict_1.default.equal(typeof message.result?.capabilities, "object");
69
+ strict_1.default.equal(message.result?.capabilities?.textDocumentSync?.openClose, true);
70
+ strict_1.default.equal(message.result?.capabilities?.textDocumentSync?.change, 1);
71
+ strict_1.default.equal(message.result?.capabilities?.definitionProvider, true);
72
+ strict_1.default.equal(message.result?.capabilities?.referencesProvider, true);
73
+ }
74
+ finally {
75
+ child.kill();
76
+ }
77
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSymbolsTest = runSymbolsTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const document_1 = require("../src/asm/document");
11
+ const symbols_1 = require("../src/asm/symbols");
12
+ function runSymbolsTest() {
13
+ const fixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
14
+ const source = node_fs_1.default.readFileSync(fixturePath, "utf8");
15
+ const document = (0, document_1.parseDocument)(source);
16
+ const symbols = (0, symbols_1.collectSymbols)(document);
17
+ strict_1.default.deepEqual(symbols.get("TEXT"), {
18
+ name: "TEXT",
19
+ kind: "equate",
20
+ line: 11
21
+ });
22
+ strict_1.default.deepEqual(symbols.get("TEST_START"), {
23
+ name: "TEST_START",
24
+ kind: "label",
25
+ line: 31
26
+ });
27
+ strict_1.default.deepEqual(symbols.get("dum0"), {
28
+ name: "dum0",
29
+ kind: "data",
30
+ line: 17
31
+ });
32
+ strict_1.default.deepEqual(symbols.get("_num1"), {
33
+ name: "_num1",
34
+ kind: "data",
35
+ line: 25
36
+ });
37
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSyntaxShapeTest = runSyntaxShapeTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const syntax_1 = require("../src/asm/syntax");
9
+ function runSyntaxShapeTest() {
10
+ strict_1.default.equal(syntax_1.tokenKindTable.get("comment")?.captureExamples[0], "; trailing note");
11
+ strict_1.default.equal(syntax_1.tokenKindTable.get("localLabel")?.captureExamples[0], "]loop");
12
+ strict_1.default.equal(syntax_1.tokenKindTable.get("modifier")?.captureExamples.includes("<value"), true);
13
+ strict_1.default.equal(syntax_1.tokenKindTable.has("expressionOperator"), true);
14
+ strict_1.default.equal(syntax_1.lineShapeTable.get("instruction")?.allowsLabel, true);
15
+ strict_1.default.equal(syntax_1.lineShapeTable.get("directive")?.requiresOperand, false);
16
+ strict_1.default.equal(syntax_1.lineShapeTable.get("equate")?.allowsExpression, true);
17
+ strict_1.default.equal(syntax_1.lineShapeTable.get("malformed")?.terminal, true);
18
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runWorkspaceSymbolTest = runWorkspaceSymbolTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ function encodeMessage(message) {
12
+ const body = JSON.stringify(message);
13
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
14
+ }
15
+ function decodeMessages(streamBuffer) {
16
+ const messages = [];
17
+ let buffer = streamBuffer;
18
+ for (;;) {
19
+ const separator = buffer.indexOf("\r\n\r\n");
20
+ if (separator === -1) {
21
+ return { messages, rest: buffer };
22
+ }
23
+ const header = buffer.slice(0, separator);
24
+ const match = /Content-Length: (\d+)/i.exec(header);
25
+ if (!match) {
26
+ throw new Error(`Missing Content-Length header: ${header}`);
27
+ }
28
+ const length = Number(match[1]);
29
+ const body = buffer.slice(separator + 4);
30
+ if (Buffer.byteLength(body, "utf8") < length) {
31
+ return { messages, rest: buffer };
32
+ }
33
+ messages.push(JSON.parse(body.slice(0, length)));
34
+ buffer = body.slice(length);
35
+ }
36
+ }
37
+ async function runWorkspaceSymbolTest() {
38
+ const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
39
+ const linkPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-linkscript.asm");
40
+ const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
41
+ const linkUri = `file://${linkPath.replace(/\\/g, "/")}`;
42
+ const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
43
+ const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
44
+ stdio: ["pipe", "pipe", "pipe"]
45
+ });
46
+ let stdout = "";
47
+ let nextId = 1;
48
+ const pending = new Map();
49
+ child.stdout.setEncoding("utf8");
50
+ child.stdout.on("data", (chunk) => {
51
+ stdout += chunk;
52
+ const decoded = decodeMessages(stdout);
53
+ stdout = decoded.rest;
54
+ for (const message of decoded.messages) {
55
+ if (message.id !== undefined) {
56
+ pending.get(message.id)?.(message);
57
+ pending.delete(message.id);
58
+ }
59
+ }
60
+ });
61
+ function sendRequest(method, params) {
62
+ const id = nextId++;
63
+ child.stdin.write(encodeMessage({
64
+ id,
65
+ jsonrpc: "2.0",
66
+ method,
67
+ params
68
+ }));
69
+ return new Promise((resolve) => {
70
+ pending.set(id, resolve);
71
+ });
72
+ }
73
+ function sendNotification(method, params) {
74
+ child.stdin.write(encodeMessage({
75
+ jsonrpc: "2.0",
76
+ method,
77
+ params
78
+ }));
79
+ }
80
+ try {
81
+ await sendRequest("initialize", {
82
+ capabilities: {},
83
+ processId: process.pid,
84
+ rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
85
+ });
86
+ sendNotification("initialized", {});
87
+ sendNotification("textDocument/didOpen", {
88
+ textDocument: {
89
+ uri: linkUri,
90
+ languageId: "asm",
91
+ version: 1,
92
+ text: node_fs_1.default.readFileSync(linkPath, "utf8")
93
+ }
94
+ });
95
+ sendNotification("textDocument/didOpen", {
96
+ textDocument: {
97
+ uri: mainUri,
98
+ languageId: "asm",
99
+ version: 1,
100
+ text: node_fs_1.default.readFileSync(mainPath, "utf8")
101
+ }
102
+ });
103
+ const response = await sendRequest("workspace/symbol", {
104
+ query: "Get"
105
+ });
106
+ const symbols = response.result;
107
+ strict_1.default.equal(Array.isArray(symbols), true);
108
+ strict_1.default.equal(symbols.some((symbol) => symbol.name === "GetKey" && symbol.location.uri === mainUri), true);
109
+ }
110
+ finally {
111
+ child.kill();
112
+ }
113
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runWorkspaceGraphTest = runWorkspaceGraphTest;
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const workspace_1 = require("../src/asm/workspace");
10
+ function runWorkspaceGraphTest() {
11
+ const entryPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-linkscript.asm");
12
+ const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
13
+ const workspace = (0, workspace_1.indexWorkspace)(entryPath);
14
+ strict_1.default.deepEqual(workspace.loadOrder, [entryPath, mainPath]);
15
+ strict_1.default.deepEqual(workspace.dependencies.get(entryPath), [mainPath]);
16
+ strict_1.default.equal(workspace.documents.has(entryPath), true);
17
+ strict_1.default.equal(workspace.documents.has(mainPath), true);
18
+ strict_1.default.deepEqual(workspace.symbols.get("TEXT"), {
19
+ name: "TEXT",
20
+ kind: "equate",
21
+ line: 11,
22
+ filePath: mainPath
23
+ });
24
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "languageserver": {
3
+ "merls": {
4
+ "command": "node",
5
+ "args": [
6
+ "C:/Users/alexe/Projects/merls/dist/src/cli.js",
7
+ "--stdio"
8
+ ],
9
+ "rootPatterns": [
10
+ ".git",
11
+ "package.json"
12
+ ],
13
+ "filetypes": [
14
+ "asm"
15
+ ]
16
+ }
17
+ }
18
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@razdolbai/merls",
3
+ "version": "0.1.0",
4
+ "description": "Language server for Merlin-style 6502 assembly.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "merls": "dist/src/cli.js"
8
+ },
9
+ "main": "dist/src/server.js",
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
13
+ "dev": "tsc -p tsconfig.json --watch",
14
+ "test": "npm run build && node dist/test/run-tests.js"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.0.0"
18
+ },
19
+ "dependencies": {
20
+ "vscode-languageserver": "^9.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.0.10",
24
+ "typescript": "^5.8.3"
25
+ }
26
+ }
package/publish.ps1 ADDED
@@ -0,0 +1,9 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ Write-Host "Building package..." -ForegroundColor Cyan
4
+ npm run build
5
+
6
+ Write-Host "Publishing to npm..." -ForegroundColor Cyan
7
+ npm publish --access=public
8
+
9
+ Write-Host "Publish successful!" -ForegroundColor Green
@@ -0,0 +1,294 @@
1
+ import { type ParsedDocument } from "./document";
2
+ import { type Expression, type Operand } from "./expression";
3
+ import { resolveLocalLabels } from "./local-labels";
4
+ import { directiveTable } from "./metadata";
5
+ import { type ParsedLine } from "./parser";
6
+
7
+ export type DiagnosticCode =
8
+ | "duplicate-symbol"
9
+ | "unresolved-reference"
10
+ | "malformed-line"
11
+ | "unsupported-65816";
12
+
13
+ export type Diagnostic = {
14
+ filePath: string;
15
+ line: number;
16
+ code: DiagnosticCode;
17
+ message: string;
18
+ };
19
+
20
+ export type DocumentEntry = {
21
+ filePath: string;
22
+ document: ParsedDocument;
23
+ };
24
+
25
+ type SymbolRecord = {
26
+ name: string;
27
+ line: number;
28
+ filePath: string;
29
+ };
30
+
31
+ export function collectWorkspaceDiagnostics(
32
+ documents: readonly DocumentEntry[]
33
+ ): readonly Diagnostic[] {
34
+ const diagnostics: Diagnostic[] = [];
35
+ const globalSymbols = new Set<string>();
36
+ const symbolRecords: SymbolRecord[] = [];
37
+
38
+ for (const entry of documents) {
39
+ for (const symbol of collectGlobalDefinitions(entry.document, entry.filePath)) {
40
+ symbolRecords.push(symbol);
41
+ globalSymbols.add(symbol.name);
42
+ }
43
+ }
44
+
45
+ diagnostics.push(...collectDuplicateSymbolDiagnostics(symbolRecords));
46
+
47
+ for (const entry of documents) {
48
+ diagnostics.push(
49
+ ...collectMalformedDiagnostics(entry.filePath, entry.document),
50
+ ...collectUnsupportedDiagnostics(entry.filePath, entry.document),
51
+ ...collectUnresolvedDiagnostics(entry.filePath, entry.document, globalSymbols)
52
+ );
53
+ }
54
+
55
+ return diagnostics;
56
+ }
57
+
58
+ function collectDuplicateSymbolDiagnostics(
59
+ symbolRecords: readonly SymbolRecord[]
60
+ ): readonly Diagnostic[] {
61
+ const firstDefinitionByName = new Map<string, SymbolRecord>();
62
+ const diagnostics: Diagnostic[] = [];
63
+
64
+ for (const symbol of symbolRecords) {
65
+ const firstDefinition = firstDefinitionByName.get(symbol.name);
66
+ if (firstDefinition === undefined) {
67
+ firstDefinitionByName.set(symbol.name, symbol);
68
+ continue;
69
+ }
70
+
71
+ diagnostics.push({
72
+ filePath: symbol.filePath,
73
+ line: symbol.line,
74
+ code: "duplicate-symbol",
75
+ message: `Duplicate symbol ${symbol.name}; first defined at line ${firstDefinition.line}`
76
+ });
77
+ }
78
+
79
+ return diagnostics;
80
+ }
81
+
82
+ function collectMalformedDiagnostics(
83
+ filePath: string,
84
+ document: ParsedDocument
85
+ ): readonly Diagnostic[] {
86
+ return document.errors.map((error) => ({
87
+ filePath,
88
+ line: error.line,
89
+ code: "malformed-line" as const,
90
+ message: error.message
91
+ }));
92
+ }
93
+
94
+ function collectUnsupportedDiagnostics(
95
+ filePath: string,
96
+ document: ParsedDocument
97
+ ): readonly Diagnostic[] {
98
+ const diagnostics: Diagnostic[] = [];
99
+
100
+ for (const line of document.lines) {
101
+ const unsupportedDirective = getUnsupportedDirective(line.node);
102
+ if (unsupportedDirective !== null) {
103
+ diagnostics.push({
104
+ filePath,
105
+ line: line.line,
106
+ code: "unsupported-65816",
107
+ message: `Unsupported 65816 directive: ${unsupportedDirective}`
108
+ });
109
+ }
110
+
111
+ const unsupportedText = getUnsupportedTextPattern(line.node.text);
112
+ if (unsupportedText !== null) {
113
+ diagnostics.push({
114
+ filePath,
115
+ line: line.line,
116
+ code: "unsupported-65816",
117
+ message: `Unsupported 65816 syntax: ${unsupportedText}`
118
+ });
119
+ }
120
+ }
121
+
122
+ return diagnostics;
123
+ }
124
+
125
+ function collectUnresolvedDiagnostics(
126
+ filePath: string,
127
+ document: ParsedDocument,
128
+ globalSymbols: ReadonlySet<string>
129
+ ): readonly Diagnostic[] {
130
+ const diagnostics: Diagnostic[] = [];
131
+ const localScope = resolveLocalLabels(document);
132
+
133
+ for (const line of document.lines) {
134
+ for (const reference of findExpressionReferences(line.node)) {
135
+ if (reference.startsWith("]") || reference.startsWith(":")) {
136
+ const localKey = `${reference}@${line.line}`;
137
+ const localDefinitionKey = [...localScope.definitions.keys()].find((key) =>
138
+ key.startsWith(`${reference}@`)
139
+ );
140
+ if (!localScope.references.has(localKey) && localDefinitionKey === undefined) {
141
+ diagnostics.push({
142
+ filePath,
143
+ line: line.line,
144
+ code: "unresolved-reference",
145
+ message: `Unresolved local reference ${reference}`
146
+ });
147
+ }
148
+ continue;
149
+ }
150
+
151
+ if (!globalSymbols.has(reference)) {
152
+ diagnostics.push({
153
+ filePath,
154
+ line: line.line,
155
+ code: "unresolved-reference",
156
+ message: `Unresolved reference ${reference}`
157
+ });
158
+ }
159
+ }
160
+ }
161
+
162
+ return diagnostics;
163
+ }
164
+
165
+ function collectGlobalDefinitions(
166
+ document: ParsedDocument,
167
+ filePath: string
168
+ ): readonly SymbolRecord[] {
169
+ const symbols: SymbolRecord[] = [];
170
+
171
+ for (const line of document.lines) {
172
+ const name = getGlobalDefinitionName(line.node);
173
+ if (name === null) {
174
+ continue;
175
+ }
176
+
177
+ symbols.push({
178
+ name,
179
+ line: line.line,
180
+ filePath
181
+ });
182
+ }
183
+
184
+ return symbols;
185
+ }
186
+
187
+ function getGlobalDefinitionName(node: ParsedLine): string | null {
188
+ if (node.shape === "equate" && !isLocalLabel(node.label)) {
189
+ return node.label;
190
+ }
191
+
192
+ if (node.shape === "labelOnly" && !isLocalLabel(node.label)) {
193
+ return node.label;
194
+ }
195
+
196
+ if (node.shape === "instruction" && node.label !== null && !isLocalLabel(node.label)) {
197
+ return node.label;
198
+ }
199
+
200
+ if (node.shape === "directive" && node.label !== null && !isLocalLabel(node.label)) {
201
+ return node.label;
202
+ }
203
+
204
+ if (node.shape === "data" && node.label !== null && !isLocalLabel(node.label)) {
205
+ return node.label;
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ function findExpressionReferences(node: ParsedLine): readonly string[] {
212
+ if (node.shape === "instruction" && node.operand !== null) {
213
+ return findReferencesInOperand(node.operand);
214
+ }
215
+
216
+ if (node.shape === "directive" && node.operand !== null) {
217
+ const directive = directiveTable.get(node.directive);
218
+ if (directive?.kind === "include" || directive?.kind === "build") {
219
+ if (node.directive === "typ") {
220
+ const knownAliases = new Set([
221
+ "txt", "bin", "sys", "bas", "var", "rel",
222
+ "lib", "s16", "rtl", "exe", "pif", "tif",
223
+ "nda", "cda", "tol", "dvr", "ldf", "fst"
224
+ ]);
225
+ return findReferencesInExpression(node.operand).filter(
226
+ (ref) => !knownAliases.has(ref.toLowerCase())
227
+ );
228
+ }
229
+ return [];
230
+ }
231
+ return findReferencesInExpression(node.operand);
232
+ }
233
+
234
+ if (node.shape === "equate") {
235
+ return findReferencesInExpression(node.expression);
236
+ }
237
+
238
+ return [];
239
+ }
240
+
241
+ function findReferencesInOperand(operand: Operand): readonly string[] {
242
+ return findReferencesInExpression(operand.expression);
243
+ }
244
+
245
+ function findReferencesInExpression(expression: Expression): readonly string[] {
246
+ switch (expression.kind) {
247
+ case "identifier":
248
+ return [expression.value];
249
+ case "modifier":
250
+ return findReferencesInExpression(expression.expression);
251
+ case "binary":
252
+ return [
253
+ ...findReferencesInExpression(expression.left),
254
+ ...findReferencesInExpression(expression.right)
255
+ ];
256
+ default:
257
+ return [];
258
+ }
259
+ }
260
+
261
+ function getUnsupportedDirective(node: ParsedLine): string | null {
262
+ if (node.shape !== "directive") {
263
+ return null;
264
+ }
265
+
266
+ const directive = directiveTable.get(node.directive);
267
+ if (directive?.supported === false) {
268
+ return node.directive;
269
+ }
270
+
271
+ return null;
272
+ }
273
+
274
+ function getUnsupportedTextPattern(text: string): string | null {
275
+ const trimmed = text.trim().toLowerCase();
276
+
277
+ if (/^(pea|mvn|mvp)\b/.test(trimmed)) {
278
+ return trimmed.split(/\s+/, 1)[0] ?? trimmed;
279
+ }
280
+
281
+ if (trimmed.includes("^")) {
282
+ return "^";
283
+ }
284
+
285
+ if (trimmed.includes("|")) {
286
+ return "|";
287
+ }
288
+
289
+ return null;
290
+ }
291
+
292
+ function isLocalLabel(name: string): boolean {
293
+ return name.startsWith("]") || name.startsWith(":");
294
+ }
@@ -0,0 +1,43 @@
1
+ import { parseSourceLines, type ParsedLine } from "./parser";
2
+
3
+ export type DocumentLine = {
4
+ line: number;
5
+ node: ParsedLine;
6
+ };
7
+
8
+ export type DocumentError = {
9
+ line: number;
10
+ text: string;
11
+ message: string;
12
+ };
13
+
14
+ export type ParsedDocument = {
15
+ lines: readonly DocumentLine[];
16
+ errors: readonly DocumentError[];
17
+ };
18
+
19
+ export function parseDocument(source: string): ParsedDocument {
20
+ const parsedLines = parseSourceLines(source);
21
+ const lines: DocumentLine[] = [];
22
+ const errors: DocumentError[] = [];
23
+
24
+ parsedLines.forEach((node, line) => {
25
+ lines.push({
26
+ line,
27
+ node
28
+ });
29
+
30
+ if (node.shape === "malformed") {
31
+ errors.push({
32
+ line,
33
+ text: node.text,
34
+ message: node.message
35
+ });
36
+ }
37
+ });
38
+
39
+ return {
40
+ lines,
41
+ errors
42
+ };
43
+ }