@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.
- package/.serena/memories/conventions.md +6 -0
- package/.serena/memories/core.md +8 -0
- package/.serena/memories/memory_maintenance.md +33 -0
- package/.serena/memories/suggested_commands.md +4 -0
- package/.serena/memories/task_completion.md +7 -0
- package/.serena/memories/tech_stack.md +6 -0
- package/.serena/project.yml +132 -0
- package/AGENTS.md +63 -0
- package/README.md +137 -0
- package/dist/src/asm/diagnostics.js +202 -0
- package/dist/src/asm/document.js +26 -0
- package/dist/src/asm/expression.js +163 -0
- package/dist/src/asm/lexer.js +122 -0
- package/dist/src/asm/local-labels.js +140 -0
- package/dist/src/asm/metadata.js +101 -0
- package/dist/src/asm/parser.js +118 -0
- package/dist/src/asm/symbols.js +40 -0
- package/dist/src/asm/syntax.js +44 -0
- package/dist/src/asm/workspace.js +73 -0
- package/dist/src/cli.js +21 -0
- package/dist/src/index.js +4 -0
- package/dist/src/lsp/completion.js +32 -0
- package/dist/src/lsp/diagnostics.js +63 -0
- package/dist/src/lsp/document-symbols.js +80 -0
- package/dist/src/lsp/hover.js +75 -0
- package/dist/src/lsp/symbol-navigation.js +181 -0
- package/dist/src/lsp/workspace-symbols.js +17 -0
- package/dist/src/server.js +77 -0
- package/dist/test/bootstrap.test.js +11 -0
- package/dist/test/cli-contract.test.js +74 -0
- package/dist/test/coc-config.test.js +21 -0
- package/dist/test/completion.test.js +126 -0
- package/dist/test/definition-references.test.js +126 -0
- package/dist/test/diagnostics.test.js +66 -0
- package/dist/test/document-model.test.js +30 -0
- package/dist/test/document-symbol.test.js +107 -0
- package/dist/test/expression.test.js +100 -0
- package/dist/test/fixture-corpus.test.js +33 -0
- package/dist/test/hover.test.js +142 -0
- package/dist/test/lexer.test.js +53 -0
- package/dist/test/line-parser.test.js +67 -0
- package/dist/test/local-labels.test.js +43 -0
- package/dist/test/metadata.test.js +27 -0
- package/dist/test/publish-diagnostics.test.js +137 -0
- package/dist/test/run-tests.js +132 -0
- package/dist/test/server-entrypoint.test.js +14 -0
- package/dist/test/server-initialize.test.js +77 -0
- package/dist/test/symbols.test.js +37 -0
- package/dist/test/syntax-shape.test.js +18 -0
- package/dist/test/workspace-symbol.test.js +113 -0
- package/dist/test/workspace.test.js +24 -0
- package/examples/coc-settings.json +18 -0
- package/package.json +26 -0
- package/publish.ps1 +9 -0
- package/src/asm/diagnostics.ts +294 -0
- package/src/asm/document.ts +43 -0
- package/src/asm/expression.ts +242 -0
- package/src/asm/lexer.ts +197 -0
- package/src/asm/local-labels.ts +204 -0
- package/src/asm/metadata.ts +150 -0
- package/src/asm/parser.ts +197 -0
- package/src/asm/symbols.ts +55 -0
- package/src/asm/syntax.ts +76 -0
- package/src/asm/workspace.ts +105 -0
- package/src/cli.ts +24 -0
- package/src/index.ts +1 -0
- package/src/lsp/completion.ts +42 -0
- package/src/lsp/diagnostics.ts +82 -0
- package/src/lsp/document-symbols.ts +111 -0
- package/src/lsp/hover.ts +90 -0
- package/src/lsp/symbol-navigation.ts +244 -0
- package/src/lsp/workspace-symbols.ts +24 -0
- package/src/server.ts +121 -0
- package/test/bootstrap.test.ts +7 -0
- package/test/cli-contract.test.ts +94 -0
- package/test/coc-config.test.ts +28 -0
- package/test/completion.test.ts +151 -0
- package/test/definition-references.test.ts +152 -0
- package/test/diagnostics.test.ts +129 -0
- package/test/document-model.test.ts +29 -0
- package/test/document-symbol.test.ts +131 -0
- package/test/expression.test.ts +111 -0
- package/test/fixture-corpus.test.ts +33 -0
- package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
- package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
- package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
- package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
- package/test/fixtures/valid/smoke-test.asm +7 -0
- package/test/hover.test.ts +175 -0
- package/test/lexer.test.ts +87 -0
- package/test/line-parser.test.ts +69 -0
- package/test/local-labels.test.ts +47 -0
- package/test/metadata.test.ts +27 -0
- package/test/publish-diagnostics.test.ts +206 -0
- package/test/run-tests.ts +139 -0
- package/test/server-entrypoint.test.ts +11 -0
- package/test/server-initialize.test.ts +101 -0
- package/test/smoke/run-smoke.ps1 +177 -0
- package/test/smoke/vimrc +17 -0
- package/test/symbols.test.ts +41 -0
- package/test/syntax-shape.test.ts +18 -0
- package/test/workspace-symbol.test.ts +139 -0
- package/test/workspace.test.ts +29 -0
- 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
|
+
}
|
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,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
|
+
}
|