@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,242 @@
|
|
|
1
|
+
import { type Token } from "./lexer";
|
|
2
|
+
|
|
3
|
+
export type NumericLiteralExpression = {
|
|
4
|
+
kind: "numericLiteral";
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type IdentifierExpression = {
|
|
9
|
+
kind: "identifier";
|
|
10
|
+
value: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StringExpression = {
|
|
14
|
+
kind: "string";
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ModifierExpression = {
|
|
19
|
+
kind: "modifier";
|
|
20
|
+
operator: "<" | ">" | "^";
|
|
21
|
+
expression: Expression;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type BinaryExpression = {
|
|
25
|
+
kind: "binary";
|
|
26
|
+
operator: "+" | "-" | "*" | "/";
|
|
27
|
+
left: Expression;
|
|
28
|
+
right: Expression;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type Expression =
|
|
32
|
+
| NumericLiteralExpression
|
|
33
|
+
| IdentifierExpression
|
|
34
|
+
| StringExpression
|
|
35
|
+
| ModifierExpression
|
|
36
|
+
| BinaryExpression;
|
|
37
|
+
|
|
38
|
+
export type ParsedExpression = {
|
|
39
|
+
expression: Expression;
|
|
40
|
+
nextTokenIndex: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ParsedOperand = {
|
|
44
|
+
operand: Operand;
|
|
45
|
+
nextTokenIndex: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type Operand = {
|
|
49
|
+
immediate: boolean;
|
|
50
|
+
indirect: boolean;
|
|
51
|
+
indexRegister: "x" | "y" | null;
|
|
52
|
+
expression: Expression;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const binaryPrecedence = new Map<string, number>([
|
|
56
|
+
["+", 10],
|
|
57
|
+
["-", 10],
|
|
58
|
+
["*", 20],
|
|
59
|
+
["/", 20]
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
export function parseExpression(
|
|
63
|
+
tokens: readonly Token[],
|
|
64
|
+
startIndex = 0,
|
|
65
|
+
minimumPrecedence = 0
|
|
66
|
+
): ParsedExpression {
|
|
67
|
+
let { expression: left, nextTokenIndex } = parsePrefix(tokens, startIndex);
|
|
68
|
+
|
|
69
|
+
while (nextTokenIndex < tokens.length) {
|
|
70
|
+
const operatorToken = tokens[nextTokenIndex];
|
|
71
|
+
if (operatorToken?.kind !== "expressionOperator") {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const precedence = binaryPrecedence.get(operatorToken.lexeme);
|
|
76
|
+
if (precedence === undefined || precedence < minimumPrecedence) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parsedRight = parseExpression(tokens, nextTokenIndex + 1, precedence + 1);
|
|
81
|
+
left = {
|
|
82
|
+
kind: "binary",
|
|
83
|
+
operator: operatorToken.lexeme as BinaryExpression["operator"],
|
|
84
|
+
left,
|
|
85
|
+
right: parsedRight.expression
|
|
86
|
+
};
|
|
87
|
+
nextTokenIndex = parsedRight.nextTokenIndex;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
expression: left,
|
|
92
|
+
nextTokenIndex
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseOperand(tokens: readonly Token[], startIndex = 0): ParsedOperand {
|
|
97
|
+
let index = startIndex;
|
|
98
|
+
let immediate = false;
|
|
99
|
+
let indirect = false;
|
|
100
|
+
let indexRegister: Operand["indexRegister"] = null;
|
|
101
|
+
|
|
102
|
+
if (tokens[index]?.kind === "expressionOperator" && tokens[index]?.lexeme === "#") {
|
|
103
|
+
immediate = true;
|
|
104
|
+
index += 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (tokens[index]?.kind === "numericLiteral" && tokens[index]?.lexeme.startsWith("#")) {
|
|
108
|
+
immediate = true;
|
|
109
|
+
const token = tokens[index];
|
|
110
|
+
const expression: NumericLiteralExpression = {
|
|
111
|
+
kind: "numericLiteral",
|
|
112
|
+
value: token.lexeme.slice(1)
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
operand: {
|
|
116
|
+
immediate,
|
|
117
|
+
indirect,
|
|
118
|
+
indexRegister,
|
|
119
|
+
expression
|
|
120
|
+
},
|
|
121
|
+
nextTokenIndex: index + 1
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let parsedExpression: ParsedExpression;
|
|
126
|
+
|
|
127
|
+
if (tokens[index]?.kind === "expressionOperator" && tokens[index]?.lexeme === "(") {
|
|
128
|
+
indirect = true;
|
|
129
|
+
parsedExpression = parseExpression(tokens, index + 1);
|
|
130
|
+
index = parsedExpression.nextTokenIndex;
|
|
131
|
+
|
|
132
|
+
if (tokens[index]?.kind === "expressionOperator" && tokens[index]?.lexeme === ",") {
|
|
133
|
+
indexRegister = parseIndexRegister(tokens[index + 1]);
|
|
134
|
+
index += 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
expectOperator(tokens[index], ")");
|
|
138
|
+
index += 1;
|
|
139
|
+
|
|
140
|
+
if (tokens[index]?.kind === "expressionOperator" && tokens[index]?.lexeme === ",") {
|
|
141
|
+
indexRegister = parseIndexRegister(tokens[index + 1]);
|
|
142
|
+
index += 2;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
parsedExpression = parseExpression(tokens, index);
|
|
146
|
+
index = parsedExpression.nextTokenIndex;
|
|
147
|
+
|
|
148
|
+
if (tokens[index]?.kind === "expressionOperator" && tokens[index]?.lexeme === ",") {
|
|
149
|
+
indexRegister = parseIndexRegister(tokens[index + 1]);
|
|
150
|
+
index += 2;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
operand: {
|
|
156
|
+
immediate,
|
|
157
|
+
indirect,
|
|
158
|
+
indexRegister,
|
|
159
|
+
expression: parsedExpression.expression
|
|
160
|
+
},
|
|
161
|
+
nextTokenIndex: index
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parsePrefix(tokens: readonly Token[], startIndex: number): ParsedExpression {
|
|
166
|
+
const token = tokens[startIndex];
|
|
167
|
+
if (token === undefined) {
|
|
168
|
+
throw new Error("expected expression token");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (token.kind === "numericLiteral") {
|
|
172
|
+
return {
|
|
173
|
+
expression: {
|
|
174
|
+
kind: "numericLiteral",
|
|
175
|
+
value: token.lexeme
|
|
176
|
+
},
|
|
177
|
+
nextTokenIndex: startIndex + 1
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (token.kind === "identifier" || token.kind === "label" || token.kind === "localLabel") {
|
|
182
|
+
return {
|
|
183
|
+
expression: {
|
|
184
|
+
kind: "identifier",
|
|
185
|
+
value: token.lexeme
|
|
186
|
+
},
|
|
187
|
+
nextTokenIndex: startIndex + 1
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (token.kind === "string") {
|
|
192
|
+
return {
|
|
193
|
+
expression: {
|
|
194
|
+
kind: "string",
|
|
195
|
+
value: token.lexeme.slice(1, -1)
|
|
196
|
+
},
|
|
197
|
+
nextTokenIndex: startIndex + 1
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (token.kind === "modifier") {
|
|
202
|
+
const parsedInner = parsePrefix(tokens, startIndex + 1);
|
|
203
|
+
return {
|
|
204
|
+
expression: {
|
|
205
|
+
kind: "modifier",
|
|
206
|
+
operator: token.lexeme as ModifierExpression["operator"],
|
|
207
|
+
expression: parsedInner.expression
|
|
208
|
+
},
|
|
209
|
+
nextTokenIndex: parsedInner.nextTokenIndex
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (token.kind === "expressionOperator" && token.lexeme === "(") {
|
|
214
|
+
const parsedInner = parseExpression(tokens, startIndex + 1);
|
|
215
|
+
expectOperator(tokens[parsedInner.nextTokenIndex], ")");
|
|
216
|
+
return {
|
|
217
|
+
expression: parsedInner.expression,
|
|
218
|
+
nextTokenIndex: parsedInner.nextTokenIndex + 1
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (token.kind === "expressionOperator" && token.lexeme === "#") {
|
|
223
|
+
return parsePrefix(tokens, startIndex + 1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error(`unexpected expression token: ${token.lexeme}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function expectOperator(token: Token | undefined, lexeme: string): void {
|
|
230
|
+
if (token?.kind !== "expressionOperator" || token.lexeme !== lexeme) {
|
|
231
|
+
throw new Error(`expected operator ${lexeme}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseIndexRegister(token: Token | undefined): Operand["indexRegister"] {
|
|
236
|
+
const normalized = token?.lexeme.toLowerCase();
|
|
237
|
+
if (token?.kind === "identifier" && (normalized === "x" || normalized === "y")) {
|
|
238
|
+
return normalized;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
throw new Error("expected index register");
|
|
242
|
+
}
|
package/src/asm/lexer.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { directiveTable, opcodeTable } from "./metadata";
|
|
2
|
+
|
|
3
|
+
export type TokenKind =
|
|
4
|
+
| "comment"
|
|
5
|
+
| "label"
|
|
6
|
+
| "localLabel"
|
|
7
|
+
| "directive"
|
|
8
|
+
| "mnemonic"
|
|
9
|
+
| "string"
|
|
10
|
+
| "numericLiteral"
|
|
11
|
+
| "modifier"
|
|
12
|
+
| "expressionOperator"
|
|
13
|
+
| "identifier";
|
|
14
|
+
|
|
15
|
+
export type Token = {
|
|
16
|
+
kind: TokenKind;
|
|
17
|
+
lexeme: string;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LexedLine = {
|
|
23
|
+
line: number;
|
|
24
|
+
text: string;
|
|
25
|
+
tokens: readonly Token[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LexedSource = {
|
|
29
|
+
lines: readonly LexedLine[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const operatorCharacters = new Set(["(", ")", ",", "#", "+", "-", "*", "/", "=", "<", ">", "^"]);
|
|
33
|
+
|
|
34
|
+
export function lexSource(source: string): LexedSource {
|
|
35
|
+
const lines = source.split(/\r?\n/).map((text, index) => lexLine(text, index));
|
|
36
|
+
return { lines };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function lexLine(text: string, line: number): LexedLine {
|
|
40
|
+
const tokens: Token[] = [];
|
|
41
|
+
const firstNonWhitespace = text.search(/\S/);
|
|
42
|
+
|
|
43
|
+
if (firstNonWhitespace === -1) {
|
|
44
|
+
return { line, text, tokens };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const trimmed = text.slice(firstNonWhitespace);
|
|
48
|
+
if (trimmed.startsWith(";") || (firstNonWhitespace === 0 && trimmed.startsWith("*"))) {
|
|
49
|
+
tokens.push(createToken("comment", text.slice(firstNonWhitespace), firstNonWhitespace, text.length));
|
|
50
|
+
return { line, text, tokens };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let index = firstNonWhitespace;
|
|
54
|
+
let sawOperation = false;
|
|
55
|
+
|
|
56
|
+
while (index < text.length) {
|
|
57
|
+
const char = text[index];
|
|
58
|
+
|
|
59
|
+
if (char === " " || char === "\t") {
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (char === ";") {
|
|
65
|
+
tokens.push(createToken("comment", text.slice(index), index, text.length));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (char === '"' || char === "'") {
|
|
70
|
+
const end = consumeString(text, index);
|
|
71
|
+
tokens.push(createToken("string", text.slice(index, end), index, end));
|
|
72
|
+
index = end;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (operatorCharacters.has(char)) {
|
|
77
|
+
const numericLiteral = consumeNumericLiteral(text, index);
|
|
78
|
+
if (numericLiteral !== null) {
|
|
79
|
+
tokens.push(createToken("numericLiteral", numericLiteral.lexeme, index, numericLiteral.end));
|
|
80
|
+
index = numericLiteral.end;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const operatorKind: TokenKind =
|
|
85
|
+
char === "<" || char === ">" || char === "^"
|
|
86
|
+
? "modifier"
|
|
87
|
+
: "expressionOperator";
|
|
88
|
+
tokens.push(createToken(operatorKind, char, index, index + 1));
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const end = consumeWord(text, index);
|
|
94
|
+
const lexeme = text.slice(index, end);
|
|
95
|
+
const kind = classifyWord(
|
|
96
|
+
text,
|
|
97
|
+
lexeme,
|
|
98
|
+
index,
|
|
99
|
+
end,
|
|
100
|
+
tokens.length === 0,
|
|
101
|
+
sawOperation,
|
|
102
|
+
firstNonWhitespace
|
|
103
|
+
);
|
|
104
|
+
tokens.push(createToken(kind, lexeme, index, end));
|
|
105
|
+
sawOperation ||= kind === "directive" || kind === "mnemonic";
|
|
106
|
+
index = end;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { line, text, tokens };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createToken(kind: TokenKind, lexeme: string, start: number, end: number): Token {
|
|
113
|
+
return {
|
|
114
|
+
kind,
|
|
115
|
+
lexeme,
|
|
116
|
+
start,
|
|
117
|
+
end
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function consumeString(text: string, start: number): number {
|
|
122
|
+
const quote = text[start];
|
|
123
|
+
let index = start + 1;
|
|
124
|
+
|
|
125
|
+
while (index < text.length) {
|
|
126
|
+
if (text[index] === quote) {
|
|
127
|
+
return index + 1;
|
|
128
|
+
}
|
|
129
|
+
index += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return text.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function consumeNumericLiteral(text: string, start: number): { lexeme: string; end: number } | null {
|
|
136
|
+
const prefixed = text.slice(start).match(/^#?(?:\$[0-9A-Fa-f]+|%[01]+|\d+)/);
|
|
137
|
+
if (prefixed !== null) {
|
|
138
|
+
return {
|
|
139
|
+
lexeme: prefixed[0],
|
|
140
|
+
end: start + prefixed[0].length
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function consumeWord(text: string, start: number): number {
|
|
148
|
+
let index = start;
|
|
149
|
+
|
|
150
|
+
while (index < text.length) {
|
|
151
|
+
const char = text[index];
|
|
152
|
+
if (char === " " || char === "\t" || char === ";" || operatorCharacters.has(char)) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
index += 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return index;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function classifyWord(
|
|
162
|
+
text: string,
|
|
163
|
+
lexeme: string,
|
|
164
|
+
start: number,
|
|
165
|
+
end: number,
|
|
166
|
+
isFirstToken: boolean,
|
|
167
|
+
sawOperation: boolean,
|
|
168
|
+
firstNonWhitespace: number
|
|
169
|
+
): TokenKind {
|
|
170
|
+
const normalized = lexeme.toLowerCase();
|
|
171
|
+
|
|
172
|
+
if (opcodeTable.has(normalized)) {
|
|
173
|
+
return "mnemonic";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (directiveTable.has(normalized)) {
|
|
177
|
+
return "directive";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (lexeme.startsWith("]") || lexeme.startsWith(":")) {
|
|
181
|
+
return "localLabel";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (/^(?:\$[0-9A-Fa-f]+|%[01]+|\d+)$/.test(lexeme)) {
|
|
185
|
+
return "numericLiteral";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const trailingText = text.slice(end);
|
|
189
|
+
const looksLikeLabelBoundary =
|
|
190
|
+
trailingText.length === 0 || /^[\t ]/.test(trailingText);
|
|
191
|
+
|
|
192
|
+
if (isFirstToken && !sawOperation && firstNonWhitespace === 0 && looksLikeLabelBoundary) {
|
|
193
|
+
return "label";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return "identifier";
|
|
197
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type ParsedDocument } from "./document";
|
|
2
|
+
import { type Expression, type Operand } from "./expression";
|
|
3
|
+
import { type ParsedLine } from "./parser";
|
|
4
|
+
|
|
5
|
+
export type LocalLabelDefinition = {
|
|
6
|
+
name: string;
|
|
7
|
+
line: number;
|
|
8
|
+
anchor: string;
|
|
9
|
+
qualifiedName: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LocalLabelReference = {
|
|
13
|
+
name: string;
|
|
14
|
+
line: number;
|
|
15
|
+
anchor: string;
|
|
16
|
+
qualifiedName: string;
|
|
17
|
+
targetLine: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type LocalLabelScope = {
|
|
21
|
+
definitions: Map<string, LocalLabelDefinition>;
|
|
22
|
+
references: Map<string, LocalLabelReference>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type AnchorState = {
|
|
26
|
+
name: string;
|
|
27
|
+
line: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function resolveLocalLabels(document: ParsedDocument): LocalLabelScope {
|
|
31
|
+
const definitions = new Map<string, LocalLabelDefinition>();
|
|
32
|
+
const references = new Map<string, LocalLabelReference>();
|
|
33
|
+
const definitionsByAnchor = new Map<string, Map<string, LocalLabelDefinition>>();
|
|
34
|
+
|
|
35
|
+
let currentAnchor: AnchorState | null = null;
|
|
36
|
+
|
|
37
|
+
for (const line of document.lines) {
|
|
38
|
+
currentAnchor = updateAnchor(currentAnchor, line.node, line.line);
|
|
39
|
+
|
|
40
|
+
const localDefinition = getLocalDefinition(line.node);
|
|
41
|
+
if (currentAnchor === null || localDefinition === null) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const qualifiedName = qualifyName(localDefinition, currentAnchor.line);
|
|
46
|
+
const definition: LocalLabelDefinition = {
|
|
47
|
+
name: localDefinition,
|
|
48
|
+
line: line.line,
|
|
49
|
+
anchor: currentAnchor.name,
|
|
50
|
+
qualifiedName
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
definitions.set(qualifiedName, definition);
|
|
54
|
+
|
|
55
|
+
let anchorDefinitions = definitionsByAnchor.get(currentAnchor.name);
|
|
56
|
+
if (anchorDefinitions === undefined) {
|
|
57
|
+
anchorDefinitions = new Map();
|
|
58
|
+
definitionsByAnchor.set(currentAnchor.name, anchorDefinitions);
|
|
59
|
+
}
|
|
60
|
+
anchorDefinitions.set(localDefinition, definition);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
currentAnchor = null;
|
|
64
|
+
|
|
65
|
+
for (const line of document.lines) {
|
|
66
|
+
currentAnchor = updateAnchor(currentAnchor, line.node, line.line);
|
|
67
|
+
if (currentAnchor === null) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const anchorDefinitions = definitionsByAnchor.get(currentAnchor.name);
|
|
72
|
+
if (anchorDefinitions === undefined) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const localName of findLocalReferences(line.node)) {
|
|
77
|
+
const target = anchorDefinitions.get(localName);
|
|
78
|
+
if (target === undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
references.set(qualifyName(localName, line.line), {
|
|
83
|
+
name: localName,
|
|
84
|
+
line: line.line,
|
|
85
|
+
anchor: currentAnchor.name,
|
|
86
|
+
qualifiedName: target.qualifiedName,
|
|
87
|
+
targetLine: target.line
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
definitions,
|
|
94
|
+
references
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updateAnchor(
|
|
99
|
+
currentAnchor: AnchorState | null,
|
|
100
|
+
node: ParsedLine,
|
|
101
|
+
line: number
|
|
102
|
+
): AnchorState | null {
|
|
103
|
+
const label = getGlobalLabel(node);
|
|
104
|
+
if (label === null) {
|
|
105
|
+
return currentAnchor;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: label,
|
|
110
|
+
line
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getGlobalLabel(node: ParsedLine): string | null {
|
|
115
|
+
if (node.shape === "equate" && !isLocalLabel(node.label)) {
|
|
116
|
+
return node.label;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (node.shape === "labelOnly" && !isLocalLabel(node.label)) {
|
|
120
|
+
return node.label;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (node.shape === "instruction" && node.label !== null && !isLocalLabel(node.label)) {
|
|
124
|
+
return node.label;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (node.shape === "directive" && node.label !== null && !isLocalLabel(node.label)) {
|
|
128
|
+
return node.label;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (node.shape === "data" && node.label !== null && !isLocalLabel(node.label)) {
|
|
132
|
+
return node.label;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getLocalDefinition(node: ParsedLine): string | null {
|
|
139
|
+
if (node.shape === "labelOnly" && isLocalLabel(node.label)) {
|
|
140
|
+
return node.label;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (node.shape === "instruction" && node.label !== null && isLocalLabel(node.label)) {
|
|
144
|
+
return node.label;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (node.shape === "directive" && node.label !== null && isLocalLabel(node.label)) {
|
|
148
|
+
return node.label;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.shape === "data" && node.label !== null && isLocalLabel(node.label)) {
|
|
152
|
+
return node.label;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (node.shape === "equate" && isLocalLabel(node.label)) {
|
|
156
|
+
return node.label;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findLocalReferences(node: ParsedLine): readonly string[] {
|
|
163
|
+
if (node.shape === "instruction" && node.operand !== null) {
|
|
164
|
+
return findLocalNamesInOperand(node.operand);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.shape === "directive" && node.operand !== null) {
|
|
168
|
+
return findLocalNamesInExpression(node.operand);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (node.shape === "equate") {
|
|
172
|
+
return findLocalNamesInExpression(node.expression);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function findLocalNamesInOperand(operand: Operand): readonly string[] {
|
|
179
|
+
return findLocalNamesInExpression(operand.expression);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findLocalNamesInExpression(expression: Expression): readonly string[] {
|
|
183
|
+
switch (expression.kind) {
|
|
184
|
+
case "identifier":
|
|
185
|
+
return isLocalLabel(expression.value) ? [expression.value] : [];
|
|
186
|
+
case "modifier":
|
|
187
|
+
return findLocalNamesInExpression(expression.expression);
|
|
188
|
+
case "binary":
|
|
189
|
+
return [
|
|
190
|
+
...findLocalNamesInExpression(expression.left),
|
|
191
|
+
...findLocalNamesInExpression(expression.right)
|
|
192
|
+
];
|
|
193
|
+
default:
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isLocalLabel(name: string): boolean {
|
|
199
|
+
return name.startsWith("]") || name.startsWith(":");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function qualifyName(name: string, line: number): string {
|
|
203
|
+
return `${name}@${line}`;
|
|
204
|
+
}
|