@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,33 @@
|
|
|
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.runFixtureCorpusTest = runFixtureCorpusTest;
|
|
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 validFixturePaths = [
|
|
11
|
+
"test/fixtures/valid/merlin32-linkscript.asm",
|
|
12
|
+
"test/fixtures/valid/merlin32-main-6502.asm"
|
|
13
|
+
];
|
|
14
|
+
const invalidFixturePaths = [
|
|
15
|
+
"test/fixtures/invalid/65816-bank-ops.asm",
|
|
16
|
+
"test/fixtures/invalid/65816-long-addressing.asm"
|
|
17
|
+
];
|
|
18
|
+
function runFixtureCorpusTest() {
|
|
19
|
+
for (const fixturePath of validFixturePaths) {
|
|
20
|
+
const absolutePath = node_path_1.default.resolve(process.cwd(), fixturePath);
|
|
21
|
+
strict_1.default.equal(node_fs_1.default.existsSync(absolutePath), true, `${fixturePath} should exist`);
|
|
22
|
+
const content = node_fs_1.default.readFileSync(absolutePath, "utf8");
|
|
23
|
+
strict_1.default.match(content, /Source: apple2accumulator\/merlin32/);
|
|
24
|
+
strict_1.default.ok(content.trim().length > 0, `${fixturePath} should not be empty`);
|
|
25
|
+
}
|
|
26
|
+
for (const fixturePath of invalidFixturePaths) {
|
|
27
|
+
const absolutePath = node_path_1.default.resolve(process.cwd(), fixturePath);
|
|
28
|
+
strict_1.default.equal(node_fs_1.default.existsSync(absolutePath), true, `${fixturePath} should exist`);
|
|
29
|
+
const content = node_fs_1.default.readFileSync(absolutePath, "utf8");
|
|
30
|
+
strict_1.default.match(content, /Source: apple2accumulator\/merlin32/);
|
|
31
|
+
strict_1.default.match(content, /65816-only/);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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.runHoverTest = runHoverTest;
|
|
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
|
+
function positionOf(text, needle) {
|
|
38
|
+
const index = text.indexOf(needle);
|
|
39
|
+
strict_1.default.notEqual(index, -1, `expected to find ${needle}`);
|
|
40
|
+
const prefix = text.slice(0, index);
|
|
41
|
+
const lines = prefix.split("\n");
|
|
42
|
+
return {
|
|
43
|
+
line: lines.length - 1,
|
|
44
|
+
character: lines.at(-1)?.length ?? 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function positionOfInMatch(text, needle, offset) {
|
|
48
|
+
const base = positionOf(text, needle);
|
|
49
|
+
return {
|
|
50
|
+
line: base.line,
|
|
51
|
+
character: base.character + offset
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function runHoverTest() {
|
|
55
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
56
|
+
const mainPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
57
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
58
|
+
const text = node_fs_1.default.readFileSync(mainPath, "utf8");
|
|
59
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
61
|
+
});
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let nextId = 1;
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
child.stdout.setEncoding("utf8");
|
|
66
|
+
child.stdout.on("data", (chunk) => {
|
|
67
|
+
stdout += chunk;
|
|
68
|
+
const decoded = decodeMessages(stdout);
|
|
69
|
+
stdout = decoded.rest;
|
|
70
|
+
for (const message of decoded.messages) {
|
|
71
|
+
if (message.id !== undefined) {
|
|
72
|
+
pending.get(message.id)?.(message);
|
|
73
|
+
pending.delete(message.id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
function sendRequest(method, params) {
|
|
78
|
+
const id = nextId++;
|
|
79
|
+
child.stdin.write(encodeMessage({
|
|
80
|
+
id,
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method,
|
|
83
|
+
params
|
|
84
|
+
}));
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
pending.set(id, resolve);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function sendNotification(method, params) {
|
|
90
|
+
child.stdin.write(encodeMessage({
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
method,
|
|
93
|
+
params
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await sendRequest("initialize", {
|
|
98
|
+
capabilities: {},
|
|
99
|
+
processId: process.pid,
|
|
100
|
+
rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
101
|
+
});
|
|
102
|
+
sendNotification("initialized", {});
|
|
103
|
+
sendNotification("textDocument/didOpen", {
|
|
104
|
+
textDocument: {
|
|
105
|
+
uri: mainUri,
|
|
106
|
+
languageId: "asm",
|
|
107
|
+
version: 1,
|
|
108
|
+
text
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const opcodeHover = await sendRequest("textDocument/hover", {
|
|
112
|
+
textDocument: { uri: mainUri },
|
|
113
|
+
position: positionOf(text, "adc (_tmp")
|
|
114
|
+
});
|
|
115
|
+
const opcodeResult = opcodeHover.result;
|
|
116
|
+
const opcodeText = typeof opcodeResult.contents === "string"
|
|
117
|
+
? opcodeResult.contents
|
|
118
|
+
: opcodeResult.contents.value;
|
|
119
|
+
strict_1.default.equal(opcodeText.includes("adc"), true);
|
|
120
|
+
const directiveHover = await sendRequest("textDocument/hover", {
|
|
121
|
+
textDocument: { uri: mainUri },
|
|
122
|
+
position: positionOf(text, "DUM 0")
|
|
123
|
+
});
|
|
124
|
+
const directiveResult = directiveHover.result;
|
|
125
|
+
const directiveText = typeof directiveResult.contents === "string"
|
|
126
|
+
? directiveResult.contents
|
|
127
|
+
: directiveResult.contents.value;
|
|
128
|
+
strict_1.default.equal(directiveText.includes("dum"), true);
|
|
129
|
+
const symbolHover = await sendRequest("textDocument/hover", {
|
|
130
|
+
textDocument: { uri: mainUri },
|
|
131
|
+
position: positionOfInMatch(text, "bpl GetKey", 4)
|
|
132
|
+
});
|
|
133
|
+
const symbolResult = symbolHover.result;
|
|
134
|
+
const symbolText = typeof symbolResult.contents === "string"
|
|
135
|
+
? symbolResult.contents
|
|
136
|
+
: symbolResult.contents.value;
|
|
137
|
+
strict_1.default.equal(symbolText.includes("GetKey"), true);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
child.kill();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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.runLexerTest = runLexerTest;
|
|
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 lexer_1 = require("../src/asm/lexer");
|
|
11
|
+
function summarizeTokens(tokens) {
|
|
12
|
+
return tokens.map((token) => [token.kind, token.lexeme]);
|
|
13
|
+
}
|
|
14
|
+
function findLine(lines, source) {
|
|
15
|
+
const line = lines.find((candidate) => candidate.text === source);
|
|
16
|
+
strict_1.default.ok(line, `expected to find line: ${source}`);
|
|
17
|
+
return line;
|
|
18
|
+
}
|
|
19
|
+
function runLexerTest() {
|
|
20
|
+
const mainFixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-main-6502.asm");
|
|
21
|
+
const linkFixturePath = node_path_1.default.resolve(process.cwd(), "test/fixtures/valid/merlin32-linkscript.asm");
|
|
22
|
+
const mainFixture = node_fs_1.default.readFileSync(mainFixturePath, "utf8");
|
|
23
|
+
const linkFixture = node_fs_1.default.readFileSync(linkFixturePath, "utf8");
|
|
24
|
+
const mainLines = (0, lexer_1.lexSource)(mainFixture).lines;
|
|
25
|
+
const linkLines = (0, lexer_1.lexSource)(linkFixture).lines;
|
|
26
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "; Source: apple2accumulator/merlin32").tokens), [["comment", "; Source: apple2accumulator/merlin32"]]);
|
|
27
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "TEXT = $FB39").tokens), [
|
|
28
|
+
["label", "TEXT"],
|
|
29
|
+
["expressionOperator", "="],
|
|
30
|
+
["numericLiteral", "$FB39"]
|
|
31
|
+
]);
|
|
32
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, " DUM 0").tokens), [
|
|
33
|
+
["directive", "DUM"],
|
|
34
|
+
["numericLiteral", "0"]
|
|
35
|
+
]);
|
|
36
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, " adc ($80,x)").tokens), [
|
|
37
|
+
["mnemonic", "adc"],
|
|
38
|
+
["expressionOperator", "("],
|
|
39
|
+
["numericLiteral", "$80"],
|
|
40
|
+
["expressionOperator", ","],
|
|
41
|
+
["identifier", "x"],
|
|
42
|
+
["expressionOperator", ")"]
|
|
43
|
+
]);
|
|
44
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(mainLines, "GetKey ldx $C000").tokens), [
|
|
45
|
+
["label", "GetKey"],
|
|
46
|
+
["mnemonic", "ldx"],
|
|
47
|
+
["numericLiteral", "$C000"]
|
|
48
|
+
]);
|
|
49
|
+
strict_1.default.deepEqual(summarizeTokens(findLine(linkLines, " asm \"merlin32-main-6502.asm\"").tokens), [
|
|
50
|
+
["directive", "asm"],
|
|
51
|
+
["string", "\"merlin32-main-6502.asm\""]
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.runLineParserTest = runLineParserTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const parser_1 = require("../src/asm/parser");
|
|
9
|
+
function runLineParserTest() {
|
|
10
|
+
const source = [
|
|
11
|
+
"TEXT = $FB39",
|
|
12
|
+
" adc (_tmp+dum1+1,x)",
|
|
13
|
+
"dum0 ds 1",
|
|
14
|
+
" hex 2C",
|
|
15
|
+
" adc ("
|
|
16
|
+
].join("\n");
|
|
17
|
+
const lines = (0, parser_1.parseSourceLines)(source);
|
|
18
|
+
strict_1.default.deepEqual(lines[0], {
|
|
19
|
+
shape: "equate",
|
|
20
|
+
text: "TEXT = $FB39",
|
|
21
|
+
label: "TEXT",
|
|
22
|
+
expression: {
|
|
23
|
+
kind: "numericLiteral",
|
|
24
|
+
value: "$FB39"
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
strict_1.default.deepEqual(lines[1], {
|
|
28
|
+
shape: "instruction",
|
|
29
|
+
text: " adc (_tmp+dum1+1,x)",
|
|
30
|
+
label: null,
|
|
31
|
+
mnemonic: "adc",
|
|
32
|
+
operand: {
|
|
33
|
+
immediate: false,
|
|
34
|
+
indirect: true,
|
|
35
|
+
indexRegister: "x",
|
|
36
|
+
expression: {
|
|
37
|
+
kind: "binary",
|
|
38
|
+
operator: "+",
|
|
39
|
+
left: {
|
|
40
|
+
kind: "binary",
|
|
41
|
+
operator: "+",
|
|
42
|
+
left: { kind: "identifier", value: "_tmp" },
|
|
43
|
+
right: { kind: "identifier", value: "dum1" }
|
|
44
|
+
},
|
|
45
|
+
right: { kind: "numericLiteral", value: "1" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
strict_1.default.deepEqual(lines[2], {
|
|
50
|
+
shape: "directive",
|
|
51
|
+
text: "dum0 ds 1",
|
|
52
|
+
label: "dum0",
|
|
53
|
+
directive: "ds",
|
|
54
|
+
operand: {
|
|
55
|
+
kind: "numericLiteral",
|
|
56
|
+
value: "1"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
strict_1.default.deepEqual(lines[3], {
|
|
60
|
+
shape: "data",
|
|
61
|
+
text: " hex 2C",
|
|
62
|
+
label: null,
|
|
63
|
+
directive: "hex",
|
|
64
|
+
payload: "2C"
|
|
65
|
+
});
|
|
66
|
+
strict_1.default.equal(lines[4]?.shape, "malformed");
|
|
67
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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.runLocalLabelScopeTest = runLocalLabelScopeTest;
|
|
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 local_labels_1 = require("../src/asm/local-labels");
|
|
12
|
+
function runLocalLabelScopeTest() {
|
|
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 scope = (0, local_labels_1.resolveLocalLabels)(document);
|
|
17
|
+
strict_1.default.deepEqual(scope.definitions.get("]loop@69"), {
|
|
18
|
+
name: "]loop",
|
|
19
|
+
line: 71,
|
|
20
|
+
anchor: "GetKey",
|
|
21
|
+
qualifiedName: "]loop@69"
|
|
22
|
+
});
|
|
23
|
+
strict_1.default.deepEqual(scope.references.get("]loop@73"), {
|
|
24
|
+
name: "]loop",
|
|
25
|
+
line: 73,
|
|
26
|
+
anchor: "GetKey",
|
|
27
|
+
qualifiedName: "]loop@69",
|
|
28
|
+
targetLine: 71
|
|
29
|
+
});
|
|
30
|
+
strict_1.default.deepEqual(scope.definitions.get(":err@69"), {
|
|
31
|
+
name: ":err",
|
|
32
|
+
line: 82,
|
|
33
|
+
anchor: "GetKey",
|
|
34
|
+
qualifiedName: ":err@69"
|
|
35
|
+
});
|
|
36
|
+
strict_1.default.deepEqual(scope.references.get(":good@81"), {
|
|
37
|
+
name: ":good",
|
|
38
|
+
line: 81,
|
|
39
|
+
anchor: "GetKey",
|
|
40
|
+
qualifiedName: ":good@69",
|
|
41
|
+
targetLine: 84
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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.runMetadataTableTest = runMetadataTableTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const metadata_1 = require("../src/asm/metadata");
|
|
9
|
+
function runMetadataTableTest() {
|
|
10
|
+
strict_1.default.equal(metadata_1.opcodeTable.size, 56);
|
|
11
|
+
strict_1.default.deepEqual(metadata_1.opcodeTable.get("lda")?.modes, [
|
|
12
|
+
"immediate",
|
|
13
|
+
"zeroPage",
|
|
14
|
+
"zeroPageX",
|
|
15
|
+
"absolute",
|
|
16
|
+
"absoluteX",
|
|
17
|
+
"absoluteY",
|
|
18
|
+
"indexedIndirect",
|
|
19
|
+
"indirectIndexed"
|
|
20
|
+
]);
|
|
21
|
+
strict_1.default.equal(metadata_1.opcodeTable.has("mvn"), false);
|
|
22
|
+
strict_1.default.equal(metadata_1.directiveTable.get("org")?.supported, true);
|
|
23
|
+
strict_1.default.equal(metadata_1.directiveTable.get("dum")?.supported, true);
|
|
24
|
+
strict_1.default.equal(metadata_1.directiveTable.get("xc")?.supported, false);
|
|
25
|
+
strict_1.default.equal(metadata_1.directiveTable.get("mx")?.supported, false);
|
|
26
|
+
strict_1.default.equal(metadata_1.directiveTable.get("put")?.kind, "include");
|
|
27
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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.runPublishDiagnosticsTest = runPublishDiagnosticsTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
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
|
+
function decodeMessages(streamBuffer) {
|
|
15
|
+
const messages = [];
|
|
16
|
+
let buffer = streamBuffer;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
19
|
+
if (separator === -1) {
|
|
20
|
+
return { messages, rest: buffer };
|
|
21
|
+
}
|
|
22
|
+
const header = buffer.slice(0, separator);
|
|
23
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
26
|
+
}
|
|
27
|
+
const length = Number(match[1]);
|
|
28
|
+
const body = buffer.slice(separator + 4);
|
|
29
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
30
|
+
return { messages, rest: buffer };
|
|
31
|
+
}
|
|
32
|
+
messages.push(JSON.parse(body.slice(0, length)));
|
|
33
|
+
buffer = body.slice(length);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function runPublishDiagnosticsTest() {
|
|
37
|
+
const serverPath = node_path_1.default.resolve(__dirname, "../src/server.js");
|
|
38
|
+
const documentPath = node_path_1.default.resolve(process.cwd(), "test/fixtures/invalid/publish-diagnostics.asm");
|
|
39
|
+
const documentUri = `file://${documentPath.replace(/\\/g, "/")}`;
|
|
40
|
+
const brokenText = ["dup equ 1", " lda missing", "dup equ 2", " adc ("].join("\n");
|
|
41
|
+
const fixedText = ["dup equ 1", " lda dup", " adc #1"].join("\n");
|
|
42
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [serverPath], {
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let nextId = 1;
|
|
47
|
+
const pending = new Map();
|
|
48
|
+
const diagnosticWaiters = [];
|
|
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
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (message.method === "textDocument/publishDiagnostics") {
|
|
61
|
+
const params = message.params;
|
|
62
|
+
const waiter = diagnosticWaiters.shift();
|
|
63
|
+
waiter?.(params);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
function sendRequest(method, params) {
|
|
68
|
+
const id = nextId++;
|
|
69
|
+
child.stdin.write(encodeMessage({
|
|
70
|
+
id,
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
method,
|
|
73
|
+
params
|
|
74
|
+
}));
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
pending.set(id, resolve);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function sendNotification(method, params) {
|
|
80
|
+
child.stdin.write(encodeMessage({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method,
|
|
83
|
+
params
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
function waitForDiagnostics() {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
diagnosticWaiters.push(resolve);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await sendRequest("initialize", {
|
|
93
|
+
capabilities: {},
|
|
94
|
+
processId: process.pid,
|
|
95
|
+
rootUri: `file://${node_path_1.default.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
96
|
+
});
|
|
97
|
+
sendNotification("initialized", {});
|
|
98
|
+
const openedDiagnostics = waitForDiagnostics();
|
|
99
|
+
sendNotification("textDocument/didOpen", {
|
|
100
|
+
textDocument: {
|
|
101
|
+
uri: documentUri,
|
|
102
|
+
languageId: "asm",
|
|
103
|
+
version: 1,
|
|
104
|
+
text: brokenText
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const firstPublish = await openedDiagnostics;
|
|
108
|
+
strict_1.default.equal(firstPublish.uri, documentUri);
|
|
109
|
+
strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("Unresolved reference missing") &&
|
|
110
|
+
diagnostic.severity === 1 &&
|
|
111
|
+
diagnostic.range.start.line === 1), true);
|
|
112
|
+
strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("Duplicate symbol dup") &&
|
|
113
|
+
diagnostic.severity === 1 &&
|
|
114
|
+
diagnostic.range.start.line === 2), true);
|
|
115
|
+
strict_1.default.equal(firstPublish.diagnostics.some((diagnostic) => diagnostic.message.includes("expected expression token") &&
|
|
116
|
+
diagnostic.severity === 1 &&
|
|
117
|
+
diagnostic.range.start.line === 3), true);
|
|
118
|
+
const changedDiagnostics = waitForDiagnostics();
|
|
119
|
+
sendNotification("textDocument/didChange", {
|
|
120
|
+
textDocument: {
|
|
121
|
+
uri: documentUri,
|
|
122
|
+
version: 2
|
|
123
|
+
},
|
|
124
|
+
contentChanges: [
|
|
125
|
+
{
|
|
126
|
+
text: fixedText
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
const secondPublish = await changedDiagnostics;
|
|
131
|
+
strict_1.default.equal(secondPublish.uri, documentUri);
|
|
132
|
+
strict_1.default.deepEqual(secondPublish.diagnostics, []);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
child.kill();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const bootstrap_test_1 = require("./bootstrap.test");
|
|
4
|
+
const cli_contract_test_1 = require("./cli-contract.test");
|
|
5
|
+
const coc_config_test_1 = require("./coc-config.test");
|
|
6
|
+
const completion_test_1 = require("./completion.test");
|
|
7
|
+
const definition_references_test_1 = require("./definition-references.test");
|
|
8
|
+
const diagnostics_test_1 = require("./diagnostics.test");
|
|
9
|
+
const document_model_test_1 = require("./document-model.test");
|
|
10
|
+
const document_symbol_test_1 = require("./document-symbol.test");
|
|
11
|
+
const expression_test_1 = require("./expression.test");
|
|
12
|
+
const fixture_corpus_test_1 = require("./fixture-corpus.test");
|
|
13
|
+
const hover_test_1 = require("./hover.test");
|
|
14
|
+
const lexer_test_1 = require("./lexer.test");
|
|
15
|
+
const line_parser_test_1 = require("./line-parser.test");
|
|
16
|
+
const local_labels_test_1 = require("./local-labels.test");
|
|
17
|
+
const metadata_test_1 = require("./metadata.test");
|
|
18
|
+
const publish_diagnostics_test_1 = require("./publish-diagnostics.test");
|
|
19
|
+
const server_initialize_test_1 = require("./server-initialize.test");
|
|
20
|
+
const server_entrypoint_test_1 = require("./server-entrypoint.test");
|
|
21
|
+
const symbols_test_1 = require("./symbols.test");
|
|
22
|
+
const syntax_shape_test_1 = require("./syntax-shape.test");
|
|
23
|
+
const workspace_test_1 = require("./workspace.test");
|
|
24
|
+
const workspace_symbol_test_1 = require("./workspace-symbol.test");
|
|
25
|
+
const tests = [
|
|
26
|
+
{
|
|
27
|
+
name: "workspace bootstrap exposes the project name",
|
|
28
|
+
run: bootstrap_test_1.runBootstrapTest
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "cli contract exposes a packaged stdio entrypoint",
|
|
32
|
+
run: cli_contract_test_1.runCliContractTest
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "coc.nvim example targets the packaged stdio CLI contract",
|
|
36
|
+
run: coc_config_test_1.runCocConfigTest
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "server entrypoint exposes callable startup helpers",
|
|
40
|
+
run: server_entrypoint_test_1.runServerEntrypointTest
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "server process answers initialize",
|
|
44
|
+
run: server_initialize_test_1.runInitializeHandshakeTest
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "positive fixture corpus includes upstream Merlin32 samples",
|
|
48
|
+
run: fixture_corpus_test_1.runFixtureCorpusTest
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "shared opcode and directive metadata covers 6502 and Merlin syntax",
|
|
52
|
+
run: metadata_test_1.runMetadataTableTest
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "token kinds and line shapes cover Merlin syntax categories",
|
|
56
|
+
run: syntax_shape_test_1.runSyntaxShapeTest
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "lexer tokenizes fixture comments labels mnemonics directives and literals",
|
|
60
|
+
run: lexer_test_1.runLexerTest
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "expression parser handles numeric forms modifiers arithmetic and indexed operands",
|
|
64
|
+
run: expression_test_1.runExpressionTest
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "line parser recognizes equates instructions directives data and malformed lines",
|
|
68
|
+
run: line_parser_test_1.runLineParserTest
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "document model preserves line structure and tolerates malformed lines",
|
|
72
|
+
run: document_model_test_1.runDocumentModelTest
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "symbol collection indexes labels equates and named data definitions",
|
|
76
|
+
run: symbols_test_1.runSymbolsTest
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "local label scope resolves Merlin local definitions and references",
|
|
80
|
+
run: local_labels_test_1.runLocalLabelScopeTest
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "workspace indexing follows Merlin include directives and merges symbols",
|
|
84
|
+
run: workspace_test_1.runWorkspaceGraphTest
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "diagnostics report duplicates unresolved refs malformed lines and unsupported 65816 syntax",
|
|
88
|
+
run: diagnostics_test_1.runDiagnosticsTest
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "server returns document symbols for Merlin labels equates and data definitions",
|
|
92
|
+
run: document_symbol_test_1.runDocumentSymbolTest
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "server returns workspace symbols across open Merlin documents",
|
|
96
|
+
run: workspace_symbol_test_1.runWorkspaceSymbolTest
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "server resolves definitions and references for Merlin symbols",
|
|
100
|
+
run: definition_references_test_1.runDefinitionReferencesTest
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "server returns hover information for opcodes directives and symbols",
|
|
104
|
+
run: hover_test_1.runHoverTest
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "server returns opcode directive and symbol completions",
|
|
108
|
+
run: completion_test_1.runCompletionTest
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "server publishes and clears diagnostics for open Merlin documents",
|
|
112
|
+
run: publish_diagnostics_test_1.runPublishDiagnosticsTest
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
async function main() {
|
|
116
|
+
let failures = 0;
|
|
117
|
+
for (const test of tests) {
|
|
118
|
+
try {
|
|
119
|
+
await test.run();
|
|
120
|
+
console.log(`PASS ${test.name}`);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
failures += 1;
|
|
124
|
+
console.error(`FAIL ${test.name}`);
|
|
125
|
+
console.error(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (failures > 0) {
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
void main();
|
|
@@ -0,0 +1,14 @@
|
|
|
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.runServerEntrypointTest = runServerEntrypointTest;
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const server_1 = require("../src/server");
|
|
9
|
+
function runServerEntrypointTest() {
|
|
10
|
+
strict_1.default.equal(typeof server_1.startServer, "function");
|
|
11
|
+
const connection = (0, server_1.createServerConnection)();
|
|
12
|
+
strict_1.default.equal(typeof connection.listen, "function");
|
|
13
|
+
connection.dispose();
|
|
14
|
+
}
|