@razdolbai/merls 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.serena/memories/conventions.md +6 -0
  2. package/.serena/memories/core.md +8 -0
  3. package/.serena/memories/memory_maintenance.md +33 -0
  4. package/.serena/memories/suggested_commands.md +4 -0
  5. package/.serena/memories/task_completion.md +7 -0
  6. package/.serena/memories/tech_stack.md +6 -0
  7. package/.serena/project.yml +132 -0
  8. package/AGENTS.md +63 -0
  9. package/README.md +137 -0
  10. package/dist/src/asm/diagnostics.js +202 -0
  11. package/dist/src/asm/document.js +26 -0
  12. package/dist/src/asm/expression.js +163 -0
  13. package/dist/src/asm/lexer.js +122 -0
  14. package/dist/src/asm/local-labels.js +140 -0
  15. package/dist/src/asm/metadata.js +101 -0
  16. package/dist/src/asm/parser.js +118 -0
  17. package/dist/src/asm/symbols.js +40 -0
  18. package/dist/src/asm/syntax.js +44 -0
  19. package/dist/src/asm/workspace.js +73 -0
  20. package/dist/src/cli.js +21 -0
  21. package/dist/src/index.js +4 -0
  22. package/dist/src/lsp/completion.js +32 -0
  23. package/dist/src/lsp/diagnostics.js +63 -0
  24. package/dist/src/lsp/document-symbols.js +80 -0
  25. package/dist/src/lsp/hover.js +75 -0
  26. package/dist/src/lsp/symbol-navigation.js +181 -0
  27. package/dist/src/lsp/workspace-symbols.js +17 -0
  28. package/dist/src/server.js +77 -0
  29. package/dist/test/bootstrap.test.js +11 -0
  30. package/dist/test/cli-contract.test.js +74 -0
  31. package/dist/test/coc-config.test.js +21 -0
  32. package/dist/test/completion.test.js +126 -0
  33. package/dist/test/definition-references.test.js +126 -0
  34. package/dist/test/diagnostics.test.js +66 -0
  35. package/dist/test/document-model.test.js +30 -0
  36. package/dist/test/document-symbol.test.js +107 -0
  37. package/dist/test/expression.test.js +100 -0
  38. package/dist/test/fixture-corpus.test.js +33 -0
  39. package/dist/test/hover.test.js +142 -0
  40. package/dist/test/lexer.test.js +53 -0
  41. package/dist/test/line-parser.test.js +67 -0
  42. package/dist/test/local-labels.test.js +43 -0
  43. package/dist/test/metadata.test.js +27 -0
  44. package/dist/test/publish-diagnostics.test.js +137 -0
  45. package/dist/test/run-tests.js +132 -0
  46. package/dist/test/server-entrypoint.test.js +14 -0
  47. package/dist/test/server-initialize.test.js +77 -0
  48. package/dist/test/symbols.test.js +37 -0
  49. package/dist/test/syntax-shape.test.js +18 -0
  50. package/dist/test/workspace-symbol.test.js +113 -0
  51. package/dist/test/workspace.test.js +24 -0
  52. package/examples/coc-settings.json +18 -0
  53. package/package.json +26 -0
  54. package/publish.ps1 +9 -0
  55. package/src/asm/diagnostics.ts +294 -0
  56. package/src/asm/document.ts +43 -0
  57. package/src/asm/expression.ts +242 -0
  58. package/src/asm/lexer.ts +197 -0
  59. package/src/asm/local-labels.ts +204 -0
  60. package/src/asm/metadata.ts +150 -0
  61. package/src/asm/parser.ts +197 -0
  62. package/src/asm/symbols.ts +55 -0
  63. package/src/asm/syntax.ts +76 -0
  64. package/src/asm/workspace.ts +105 -0
  65. package/src/cli.ts +24 -0
  66. package/src/index.ts +1 -0
  67. package/src/lsp/completion.ts +42 -0
  68. package/src/lsp/diagnostics.ts +82 -0
  69. package/src/lsp/document-symbols.ts +111 -0
  70. package/src/lsp/hover.ts +90 -0
  71. package/src/lsp/symbol-navigation.ts +244 -0
  72. package/src/lsp/workspace-symbols.ts +24 -0
  73. package/src/server.ts +121 -0
  74. package/test/bootstrap.test.ts +7 -0
  75. package/test/cli-contract.test.ts +94 -0
  76. package/test/coc-config.test.ts +28 -0
  77. package/test/completion.test.ts +151 -0
  78. package/test/definition-references.test.ts +152 -0
  79. package/test/diagnostics.test.ts +129 -0
  80. package/test/document-model.test.ts +29 -0
  81. package/test/document-symbol.test.ts +131 -0
  82. package/test/expression.test.ts +111 -0
  83. package/test/fixture-corpus.test.ts +33 -0
  84. package/test/fixtures/invalid/65816-bank-ops.asm +17 -0
  85. package/test/fixtures/invalid/65816-long-addressing.asm +26 -0
  86. package/test/fixtures/valid/merlin32-linkscript.asm +16 -0
  87. package/test/fixtures/valid/merlin32-main-6502.asm +103 -0
  88. package/test/fixtures/valid/smoke-test.asm +7 -0
  89. package/test/hover.test.ts +175 -0
  90. package/test/lexer.test.ts +87 -0
  91. package/test/line-parser.test.ts +69 -0
  92. package/test/local-labels.test.ts +47 -0
  93. package/test/metadata.test.ts +27 -0
  94. package/test/publish-diagnostics.test.ts +206 -0
  95. package/test/run-tests.ts +139 -0
  96. package/test/server-entrypoint.test.ts +11 -0
  97. package/test/server-initialize.test.ts +101 -0
  98. package/test/smoke/run-smoke.ps1 +177 -0
  99. package/test/smoke/vimrc +17 -0
  100. package/test/symbols.test.ts +41 -0
  101. package/test/syntax-shape.test.ts +18 -0
  102. package/test/workspace-symbol.test.ts +139 -0
  103. package/test/workspace.test.ts +29 -0
  104. package/tsconfig.json +16 -0
@@ -0,0 +1,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
+ }
@@ -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
+ }