@razdolbai/merls 1.2.2 → 1.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/AGENTS.md +64 -64
- package/dist/src/lsp/completion.js +14 -11
- package/dist/test/completion.test.js +1 -1
- package/dist/test/definition-references.test.js +1 -1
- package/dist/test/diagnostics.test.js +2 -2
- package/dist/test/document-symbol.test.js +1 -1
- package/dist/test/fixture-corpus.test.js +4 -4
- package/dist/test/hover.test.js +1 -1
- package/dist/test/lexer.test.js +4 -4
- package/dist/test/local-labels.test.js +1 -1
- package/dist/test/publish-diagnostics.test.js +1 -1
- package/dist/test/semantic-tokens.test.js +1 -1
- package/dist/test/symbols.test.js +1 -1
- package/dist/test/workspace-symbol.test.js +2 -2
- package/dist/test/workspace.test.js +2 -2
- package/package.json +1 -1
- package/src/lsp/completion.ts +15 -11
- package/test/completion.test.ts +151 -151
- package/test/definition-references.test.ts +152 -152
- package/test/diagnostics.test.ts +129 -129
- package/test/document-symbol.test.ts +131 -131
- package/test/fixture-corpus.test.ts +33 -33
- package/test/fixtures/valid/{merlin32-linkscript.asm → merlin32-linkscript.S} +16 -16
- package/test/hover.test.ts +175 -175
- package/test/lexer.test.ts +87 -87
- package/test/local-labels.test.ts +47 -47
- package/test/publish-diagnostics.test.ts +206 -206
- package/test/semantic-tokens.test.ts +128 -128
- package/test/smoke/run-smoke.ps1 +177 -177
- package/test/symbols.test.ts +41 -41
- package/test/workspace-symbol.test.ts +139 -139
- package/test/workspace.test.ts +29 -29
- /package/test/fixtures/invalid/{65816-bank-ops.asm → 65816-bank-ops.S} +0 -0
- /package/test/fixtures/invalid/{65816-long-addressing.asm → 65816-long-addressing.S} +0 -0
- /package/test/fixtures/valid/{merlin32-main-6502.asm → merlin32-main-6502.S} +0 -0
- /package/test/fixtures/valid/{smoke-test.asm → smoke-test.S} +0 -0
package/test/hover.test.ts
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
type JsonRpcMessage = {
|
|
7
|
-
id?: number;
|
|
8
|
-
jsonrpc: "2.0";
|
|
9
|
-
result?: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function encodeMessage(message: object): string {
|
|
13
|
-
const body = JSON.stringify(message);
|
|
14
|
-
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
18
|
-
const messages: JsonRpcMessage[] = [];
|
|
19
|
-
let buffer = streamBuffer;
|
|
20
|
-
|
|
21
|
-
for (;;) {
|
|
22
|
-
const separator = buffer.indexOf("\r\n\r\n");
|
|
23
|
-
if (separator === -1) {
|
|
24
|
-
return { messages, rest: buffer };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const header = buffer.slice(0, separator);
|
|
28
|
-
const match = /Content-Length: (\d+)/i.exec(header);
|
|
29
|
-
if (!match) {
|
|
30
|
-
throw new Error(`Missing Content-Length header: ${header}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const length = Number(match[1]);
|
|
34
|
-
const body = buffer.slice(separator + 4);
|
|
35
|
-
if (Buffer.byteLength(body, "utf8") < length) {
|
|
36
|
-
return { messages, rest: buffer };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
40
|
-
buffer = body.slice(length);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function positionOf(text: string, needle: string): { line: number; character: number } {
|
|
45
|
-
const index = text.indexOf(needle);
|
|
46
|
-
assert.notEqual(index, -1, `expected to find ${needle}`);
|
|
47
|
-
const prefix = text.slice(0, index);
|
|
48
|
-
const lines = prefix.split("\n");
|
|
49
|
-
return {
|
|
50
|
-
line: lines.length - 1,
|
|
51
|
-
character: lines.at(-1)?.length ?? 0
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function positionOfInMatch(
|
|
56
|
-
text: string,
|
|
57
|
-
needle: string,
|
|
58
|
-
offset: number
|
|
59
|
-
): { line: number; character: number } {
|
|
60
|
-
const base = positionOf(text, needle);
|
|
61
|
-
return {
|
|
62
|
-
line: base.line,
|
|
63
|
-
character: base.character + offset
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function runHoverTest(): Promise<void> {
|
|
68
|
-
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
69
|
-
const mainPath = path.resolve(
|
|
70
|
-
process.cwd(),
|
|
71
|
-
"test/fixtures/valid/merlin32-main-6502.
|
|
72
|
-
);
|
|
73
|
-
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
74
|
-
const text = fs.readFileSync(mainPath, "utf8");
|
|
75
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
76
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
let stdout = "";
|
|
80
|
-
let nextId = 1;
|
|
81
|
-
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
82
|
-
|
|
83
|
-
child.stdout.setEncoding("utf8");
|
|
84
|
-
child.stdout.on("data", (chunk: string) => {
|
|
85
|
-
stdout += chunk;
|
|
86
|
-
const decoded = decodeMessages(stdout);
|
|
87
|
-
stdout = decoded.rest;
|
|
88
|
-
|
|
89
|
-
for (const message of decoded.messages) {
|
|
90
|
-
if (message.id !== undefined) {
|
|
91
|
-
pending.get(message.id)?.(message);
|
|
92
|
-
pending.delete(message.id);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
98
|
-
const id = nextId++;
|
|
99
|
-
child.stdin.write(
|
|
100
|
-
encodeMessage({
|
|
101
|
-
id,
|
|
102
|
-
jsonrpc: "2.0",
|
|
103
|
-
method,
|
|
104
|
-
params
|
|
105
|
-
})
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
return new Promise((resolve) => {
|
|
109
|
-
pending.set(id, resolve);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function sendNotification(method: string, params: object): void {
|
|
114
|
-
child.stdin.write(
|
|
115
|
-
encodeMessage({
|
|
116
|
-
jsonrpc: "2.0",
|
|
117
|
-
method,
|
|
118
|
-
params
|
|
119
|
-
})
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
await sendRequest("initialize", {
|
|
125
|
-
capabilities: {},
|
|
126
|
-
processId: process.pid,
|
|
127
|
-
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
sendNotification("initialized", {});
|
|
131
|
-
sendNotification("textDocument/didOpen", {
|
|
132
|
-
textDocument: {
|
|
133
|
-
uri: mainUri,
|
|
134
|
-
languageId: "asm",
|
|
135
|
-
version: 1,
|
|
136
|
-
text
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const opcodeHover = await sendRequest("textDocument/hover", {
|
|
141
|
-
textDocument: { uri: mainUri },
|
|
142
|
-
position: positionOf(text, "adc (_tmp")
|
|
143
|
-
});
|
|
144
|
-
const opcodeResult = opcodeHover.result as { contents: string | { value: string } };
|
|
145
|
-
const opcodeText =
|
|
146
|
-
typeof opcodeResult.contents === "string"
|
|
147
|
-
? opcodeResult.contents
|
|
148
|
-
: opcodeResult.contents.value;
|
|
149
|
-
assert.equal(opcodeText.includes("adc"), true);
|
|
150
|
-
|
|
151
|
-
const directiveHover = await sendRequest("textDocument/hover", {
|
|
152
|
-
textDocument: { uri: mainUri },
|
|
153
|
-
position: positionOf(text, "DUM 0")
|
|
154
|
-
});
|
|
155
|
-
const directiveResult = directiveHover.result as { contents: string | { value: string } };
|
|
156
|
-
const directiveText =
|
|
157
|
-
typeof directiveResult.contents === "string"
|
|
158
|
-
? directiveResult.contents
|
|
159
|
-
: directiveResult.contents.value;
|
|
160
|
-
assert.equal(directiveText.includes("dum"), true);
|
|
161
|
-
|
|
162
|
-
const symbolHover = await sendRequest("textDocument/hover", {
|
|
163
|
-
textDocument: { uri: mainUri },
|
|
164
|
-
position: positionOfInMatch(text, "bpl GetKey", 4)
|
|
165
|
-
});
|
|
166
|
-
const symbolResult = symbolHover.result as { contents: string | { value: string } };
|
|
167
|
-
const symbolText =
|
|
168
|
-
typeof symbolResult.contents === "string"
|
|
169
|
-
? symbolResult.contents
|
|
170
|
-
: symbolResult.contents.value;
|
|
171
|
-
assert.equal(symbolText.includes("GetKey"), true);
|
|
172
|
-
} finally {
|
|
173
|
-
child.kill();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
type JsonRpcMessage = {
|
|
7
|
+
id?: number;
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
result?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function encodeMessage(message: object): string {
|
|
13
|
+
const body = JSON.stringify(message);
|
|
14
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeMessages(streamBuffer: string): { messages: JsonRpcMessage[]; rest: string } {
|
|
18
|
+
const messages: JsonRpcMessage[] = [];
|
|
19
|
+
let buffer = streamBuffer;
|
|
20
|
+
|
|
21
|
+
for (;;) {
|
|
22
|
+
const separator = buffer.indexOf("\r\n\r\n");
|
|
23
|
+
if (separator === -1) {
|
|
24
|
+
return { messages, rest: buffer };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const header = buffer.slice(0, separator);
|
|
28
|
+
const match = /Content-Length: (\d+)/i.exec(header);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`Missing Content-Length header: ${header}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const length = Number(match[1]);
|
|
34
|
+
const body = buffer.slice(separator + 4);
|
|
35
|
+
if (Buffer.byteLength(body, "utf8") < length) {
|
|
36
|
+
return { messages, rest: buffer };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
messages.push(JSON.parse(body.slice(0, length)) as JsonRpcMessage);
|
|
40
|
+
buffer = body.slice(length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positionOf(text: string, needle: string): { line: number; character: number } {
|
|
45
|
+
const index = text.indexOf(needle);
|
|
46
|
+
assert.notEqual(index, -1, `expected to find ${needle}`);
|
|
47
|
+
const prefix = text.slice(0, index);
|
|
48
|
+
const lines = prefix.split("\n");
|
|
49
|
+
return {
|
|
50
|
+
line: lines.length - 1,
|
|
51
|
+
character: lines.at(-1)?.length ?? 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function positionOfInMatch(
|
|
56
|
+
text: string,
|
|
57
|
+
needle: string,
|
|
58
|
+
offset: number
|
|
59
|
+
): { line: number; character: number } {
|
|
60
|
+
const base = positionOf(text, needle);
|
|
61
|
+
return {
|
|
62
|
+
line: base.line,
|
|
63
|
+
character: base.character + offset
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runHoverTest(): Promise<void> {
|
|
68
|
+
const serverPath = path.resolve(__dirname, "../src/server.js");
|
|
69
|
+
const mainPath = path.resolve(
|
|
70
|
+
process.cwd(),
|
|
71
|
+
"test/fixtures/valid/merlin32-main-6502.S"
|
|
72
|
+
);
|
|
73
|
+
const mainUri = `file://${mainPath.replace(/\\/g, "/")}`;
|
|
74
|
+
const text = fs.readFileSync(mainPath, "utf8");
|
|
75
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
76
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let stdout = "";
|
|
80
|
+
let nextId = 1;
|
|
81
|
+
const pending = new Map<number, (message: JsonRpcMessage) => void>();
|
|
82
|
+
|
|
83
|
+
child.stdout.setEncoding("utf8");
|
|
84
|
+
child.stdout.on("data", (chunk: string) => {
|
|
85
|
+
stdout += chunk;
|
|
86
|
+
const decoded = decodeMessages(stdout);
|
|
87
|
+
stdout = decoded.rest;
|
|
88
|
+
|
|
89
|
+
for (const message of decoded.messages) {
|
|
90
|
+
if (message.id !== undefined) {
|
|
91
|
+
pending.get(message.id)?.(message);
|
|
92
|
+
pending.delete(message.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function sendRequest(method: string, params: object): Promise<JsonRpcMessage> {
|
|
98
|
+
const id = nextId++;
|
|
99
|
+
child.stdin.write(
|
|
100
|
+
encodeMessage({
|
|
101
|
+
id,
|
|
102
|
+
jsonrpc: "2.0",
|
|
103
|
+
method,
|
|
104
|
+
params
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
pending.set(id, resolve);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sendNotification(method: string, params: object): void {
|
|
114
|
+
child.stdin.write(
|
|
115
|
+
encodeMessage({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
method,
|
|
118
|
+
params
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await sendRequest("initialize", {
|
|
125
|
+
capabilities: {},
|
|
126
|
+
processId: process.pid,
|
|
127
|
+
rootUri: `file://${path.resolve(process.cwd()).replace(/\\/g, "/")}`
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
sendNotification("initialized", {});
|
|
131
|
+
sendNotification("textDocument/didOpen", {
|
|
132
|
+
textDocument: {
|
|
133
|
+
uri: mainUri,
|
|
134
|
+
languageId: "asm",
|
|
135
|
+
version: 1,
|
|
136
|
+
text
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const opcodeHover = await sendRequest("textDocument/hover", {
|
|
141
|
+
textDocument: { uri: mainUri },
|
|
142
|
+
position: positionOf(text, "adc (_tmp")
|
|
143
|
+
});
|
|
144
|
+
const opcodeResult = opcodeHover.result as { contents: string | { value: string } };
|
|
145
|
+
const opcodeText =
|
|
146
|
+
typeof opcodeResult.contents === "string"
|
|
147
|
+
? opcodeResult.contents
|
|
148
|
+
: opcodeResult.contents.value;
|
|
149
|
+
assert.equal(opcodeText.includes("adc"), true);
|
|
150
|
+
|
|
151
|
+
const directiveHover = await sendRequest("textDocument/hover", {
|
|
152
|
+
textDocument: { uri: mainUri },
|
|
153
|
+
position: positionOf(text, "DUM 0")
|
|
154
|
+
});
|
|
155
|
+
const directiveResult = directiveHover.result as { contents: string | { value: string } };
|
|
156
|
+
const directiveText =
|
|
157
|
+
typeof directiveResult.contents === "string"
|
|
158
|
+
? directiveResult.contents
|
|
159
|
+
: directiveResult.contents.value;
|
|
160
|
+
assert.equal(directiveText.includes("dum"), true);
|
|
161
|
+
|
|
162
|
+
const symbolHover = await sendRequest("textDocument/hover", {
|
|
163
|
+
textDocument: { uri: mainUri },
|
|
164
|
+
position: positionOfInMatch(text, "bpl GetKey", 4)
|
|
165
|
+
});
|
|
166
|
+
const symbolResult = symbolHover.result as { contents: string | { value: string } };
|
|
167
|
+
const symbolText =
|
|
168
|
+
typeof symbolResult.contents === "string"
|
|
169
|
+
? symbolResult.contents
|
|
170
|
+
: symbolResult.contents.value;
|
|
171
|
+
assert.equal(symbolText.includes("GetKey"), true);
|
|
172
|
+
} finally {
|
|
173
|
+
child.kill();
|
|
174
|
+
}
|
|
175
|
+
}
|
package/test/lexer.test.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
type LexedLine,
|
|
7
|
-
type Token,
|
|
8
|
-
lexSource
|
|
9
|
-
} from "../src/asm/lexer";
|
|
10
|
-
|
|
11
|
-
function summarizeTokens(tokens: readonly Token[]): readonly (readonly [string, string])[] {
|
|
12
|
-
return tokens.map((token) => [token.kind, token.lexeme] as const);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function findLine(lines: readonly LexedLine[], source: string): LexedLine {
|
|
16
|
-
const line = lines.find((candidate) => candidate.text === source);
|
|
17
|
-
assert.ok(line, `expected to find line: ${source}`);
|
|
18
|
-
return line;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function runLexerTest(): void {
|
|
22
|
-
const mainFixturePath = path.resolve(
|
|
23
|
-
process.cwd(),
|
|
24
|
-
"test/fixtures/valid/merlin32-main-6502.
|
|
25
|
-
);
|
|
26
|
-
const linkFixturePath = path.resolve(
|
|
27
|
-
process.cwd(),
|
|
28
|
-
"test/fixtures/valid/merlin32-linkscript.
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const mainFixture = fs.readFileSync(mainFixturePath, "utf8");
|
|
32
|
-
const linkFixture = fs.readFileSync(linkFixturePath, "utf8");
|
|
33
|
-
|
|
34
|
-
const mainLines = lexSource(mainFixture).lines;
|
|
35
|
-
const linkLines = lexSource(linkFixture).lines;
|
|
36
|
-
|
|
37
|
-
assert.deepEqual(
|
|
38
|
-
summarizeTokens(findLine(mainLines, "; Source: apple2accumulator/merlin32").tokens),
|
|
39
|
-
[["comment", "; Source: apple2accumulator/merlin32"]]
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
assert.deepEqual(
|
|
43
|
-
summarizeTokens(findLine(mainLines, "TEXT = $FB39").tokens),
|
|
44
|
-
[
|
|
45
|
-
["label", "TEXT"],
|
|
46
|
-
["expressionOperator", "="],
|
|
47
|
-
["numericLiteral", "$FB39"]
|
|
48
|
-
]
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
assert.deepEqual(
|
|
52
|
-
summarizeTokens(findLine(mainLines, " DUM 0").tokens),
|
|
53
|
-
[
|
|
54
|
-
["directive", "DUM"],
|
|
55
|
-
["numericLiteral", "0"]
|
|
56
|
-
]
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
assert.deepEqual(
|
|
60
|
-
summarizeTokens(findLine(mainLines, " adc ($80,x)").tokens),
|
|
61
|
-
[
|
|
62
|
-
["mnemonic", "adc"],
|
|
63
|
-
["expressionOperator", "("],
|
|
64
|
-
["numericLiteral", "$80"],
|
|
65
|
-
["expressionOperator", ","],
|
|
66
|
-
["identifier", "x"],
|
|
67
|
-
["expressionOperator", ")"]
|
|
68
|
-
]
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
assert.deepEqual(
|
|
72
|
-
summarizeTokens(findLine(mainLines, "GetKey ldx $C000").tokens),
|
|
73
|
-
[
|
|
74
|
-
["label", "GetKey"],
|
|
75
|
-
["mnemonic", "ldx"],
|
|
76
|
-
["numericLiteral", "$C000"]
|
|
77
|
-
]
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
assert.deepEqual(
|
|
81
|
-
summarizeTokens(findLine(linkLines, " asm \"merlin32-main-6502.
|
|
82
|
-
[
|
|
83
|
-
["directive", "asm"],
|
|
84
|
-
["string", "\"merlin32-main-6502.
|
|
85
|
-
]
|
|
86
|
-
);
|
|
87
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type LexedLine,
|
|
7
|
+
type Token,
|
|
8
|
+
lexSource
|
|
9
|
+
} from "../src/asm/lexer";
|
|
10
|
+
|
|
11
|
+
function summarizeTokens(tokens: readonly Token[]): readonly (readonly [string, string])[] {
|
|
12
|
+
return tokens.map((token) => [token.kind, token.lexeme] as const);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findLine(lines: readonly LexedLine[], source: string): LexedLine {
|
|
16
|
+
const line = lines.find((candidate) => candidate.text === source);
|
|
17
|
+
assert.ok(line, `expected to find line: ${source}`);
|
|
18
|
+
return line;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runLexerTest(): void {
|
|
22
|
+
const mainFixturePath = path.resolve(
|
|
23
|
+
process.cwd(),
|
|
24
|
+
"test/fixtures/valid/merlin32-main-6502.S"
|
|
25
|
+
);
|
|
26
|
+
const linkFixturePath = path.resolve(
|
|
27
|
+
process.cwd(),
|
|
28
|
+
"test/fixtures/valid/merlin32-linkscript.S"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const mainFixture = fs.readFileSync(mainFixturePath, "utf8");
|
|
32
|
+
const linkFixture = fs.readFileSync(linkFixturePath, "utf8");
|
|
33
|
+
|
|
34
|
+
const mainLines = lexSource(mainFixture).lines;
|
|
35
|
+
const linkLines = lexSource(linkFixture).lines;
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
summarizeTokens(findLine(mainLines, "; Source: apple2accumulator/merlin32").tokens),
|
|
39
|
+
[["comment", "; Source: apple2accumulator/merlin32"]]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
summarizeTokens(findLine(mainLines, "TEXT = $FB39").tokens),
|
|
44
|
+
[
|
|
45
|
+
["label", "TEXT"],
|
|
46
|
+
["expressionOperator", "="],
|
|
47
|
+
["numericLiteral", "$FB39"]
|
|
48
|
+
]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
summarizeTokens(findLine(mainLines, " DUM 0").tokens),
|
|
53
|
+
[
|
|
54
|
+
["directive", "DUM"],
|
|
55
|
+
["numericLiteral", "0"]
|
|
56
|
+
]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
summarizeTokens(findLine(mainLines, " adc ($80,x)").tokens),
|
|
61
|
+
[
|
|
62
|
+
["mnemonic", "adc"],
|
|
63
|
+
["expressionOperator", "("],
|
|
64
|
+
["numericLiteral", "$80"],
|
|
65
|
+
["expressionOperator", ","],
|
|
66
|
+
["identifier", "x"],
|
|
67
|
+
["expressionOperator", ")"]
|
|
68
|
+
]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
summarizeTokens(findLine(mainLines, "GetKey ldx $C000").tokens),
|
|
73
|
+
[
|
|
74
|
+
["label", "GetKey"],
|
|
75
|
+
["mnemonic", "ldx"],
|
|
76
|
+
["numericLiteral", "$C000"]
|
|
77
|
+
]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
summarizeTokens(findLine(linkLines, " asm \"merlin32-main-6502.S\"").tokens),
|
|
82
|
+
[
|
|
83
|
+
["directive", "asm"],
|
|
84
|
+
["string", "\"merlin32-main-6502.S\""]
|
|
85
|
+
]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { parseDocument } from "../src/asm/document";
|
|
6
|
-
import { resolveLocalLabels } from "../src/asm/local-labels";
|
|
7
|
-
|
|
8
|
-
export function runLocalLabelScopeTest(): void {
|
|
9
|
-
const fixturePath = path.resolve(
|
|
10
|
-
process.cwd(),
|
|
11
|
-
"test/fixtures/valid/merlin32-main-6502.
|
|
12
|
-
);
|
|
13
|
-
const source = fs.readFileSync(fixturePath, "utf8");
|
|
14
|
-
|
|
15
|
-
const document = parseDocument(source);
|
|
16
|
-
const scope = resolveLocalLabels(document);
|
|
17
|
-
|
|
18
|
-
assert.deepEqual(scope.definitions.get("]loop@69"), {
|
|
19
|
-
name: "]loop",
|
|
20
|
-
line: 71,
|
|
21
|
-
anchor: "GetKey",
|
|
22
|
-
qualifiedName: "]loop@69"
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
assert.deepEqual(scope.references.get("]loop@73"), {
|
|
26
|
-
name: "]loop",
|
|
27
|
-
line: 73,
|
|
28
|
-
anchor: "GetKey",
|
|
29
|
-
qualifiedName: "]loop@69",
|
|
30
|
-
targetLine: 71
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
assert.deepEqual(scope.definitions.get(":err@69"), {
|
|
34
|
-
name: ":err",
|
|
35
|
-
line: 82,
|
|
36
|
-
anchor: "GetKey",
|
|
37
|
-
qualifiedName: ":err@69"
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
assert.deepEqual(scope.references.get(":good@81"), {
|
|
41
|
-
name: ":good",
|
|
42
|
-
line: 81,
|
|
43
|
-
anchor: "GetKey",
|
|
44
|
-
qualifiedName: ":good@69",
|
|
45
|
-
targetLine: 84
|
|
46
|
-
});
|
|
47
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { parseDocument } from "../src/asm/document";
|
|
6
|
+
import { resolveLocalLabels } from "../src/asm/local-labels";
|
|
7
|
+
|
|
8
|
+
export function runLocalLabelScopeTest(): void {
|
|
9
|
+
const fixturePath = path.resolve(
|
|
10
|
+
process.cwd(),
|
|
11
|
+
"test/fixtures/valid/merlin32-main-6502.S"
|
|
12
|
+
);
|
|
13
|
+
const source = fs.readFileSync(fixturePath, "utf8");
|
|
14
|
+
|
|
15
|
+
const document = parseDocument(source);
|
|
16
|
+
const scope = resolveLocalLabels(document);
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(scope.definitions.get("]loop@69"), {
|
|
19
|
+
name: "]loop",
|
|
20
|
+
line: 71,
|
|
21
|
+
anchor: "GetKey",
|
|
22
|
+
qualifiedName: "]loop@69"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(scope.references.get("]loop@73"), {
|
|
26
|
+
name: "]loop",
|
|
27
|
+
line: 73,
|
|
28
|
+
anchor: "GetKey",
|
|
29
|
+
qualifiedName: "]loop@69",
|
|
30
|
+
targetLine: 71
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(scope.definitions.get(":err@69"), {
|
|
34
|
+
name: ":err",
|
|
35
|
+
line: 82,
|
|
36
|
+
anchor: "GetKey",
|
|
37
|
+
qualifiedName: ":err@69"
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(scope.references.get(":good@81"), {
|
|
41
|
+
name: ":good",
|
|
42
|
+
line: 81,
|
|
43
|
+
anchor: "GetKey",
|
|
44
|
+
qualifiedName: ":good@69",
|
|
45
|
+
targetLine: 84
|
|
46
|
+
});
|
|
47
|
+
}
|