@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,150 @@
1
+ export type AddressingMode =
2
+ | "implied"
3
+ | "accumulator"
4
+ | "immediate"
5
+ | "zeroPage"
6
+ | "zeroPageX"
7
+ | "zeroPageY"
8
+ | "relative"
9
+ | "absolute"
10
+ | "absoluteX"
11
+ | "absoluteY"
12
+ | "indirect"
13
+ | "indexedIndirect"
14
+ | "indirectIndexed";
15
+
16
+ export type DirectiveKind =
17
+ | "assembler"
18
+ | "build"
19
+ | "data"
20
+ | "include"
21
+ | "mode"
22
+ | "storage";
23
+
24
+ export type OpcodeDefinition = {
25
+ mnemonic: string;
26
+ modes: readonly AddressingMode[];
27
+ };
28
+
29
+ export type DirectiveDefinition = {
30
+ name: string;
31
+ kind: DirectiveKind;
32
+ supported: boolean;
33
+ summary: string;
34
+ };
35
+
36
+ function defineOpcode(
37
+ mnemonic: string,
38
+ modes: readonly AddressingMode[]
39
+ ): OpcodeDefinition {
40
+ return {
41
+ mnemonic,
42
+ modes
43
+ };
44
+ }
45
+
46
+ function defineDirective(
47
+ name: string,
48
+ kind: DirectiveKind,
49
+ supported: boolean,
50
+ summary: string
51
+ ): DirectiveDefinition {
52
+ return {
53
+ name,
54
+ kind,
55
+ supported,
56
+ summary
57
+ };
58
+ }
59
+
60
+ export const opcodeDefinitions: readonly OpcodeDefinition[] = [
61
+ defineOpcode("adc", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
62
+ defineOpcode("and", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
63
+ defineOpcode("asl", ["accumulator", "zeroPage", "zeroPageX", "absolute", "absoluteX"]),
64
+ defineOpcode("bcc", ["relative"]),
65
+ defineOpcode("bcs", ["relative"]),
66
+ defineOpcode("beq", ["relative"]),
67
+ defineOpcode("bit", ["zeroPage", "absolute"]),
68
+ defineOpcode("bmi", ["relative"]),
69
+ defineOpcode("bne", ["relative"]),
70
+ defineOpcode("bpl", ["relative"]),
71
+ defineOpcode("brk", ["implied"]),
72
+ defineOpcode("bvc", ["relative"]),
73
+ defineOpcode("bvs", ["relative"]),
74
+ defineOpcode("clc", ["implied"]),
75
+ defineOpcode("cld", ["implied"]),
76
+ defineOpcode("cli", ["implied"]),
77
+ defineOpcode("clv", ["implied"]),
78
+ defineOpcode("cmp", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
79
+ defineOpcode("cpx", ["immediate", "zeroPage", "absolute"]),
80
+ defineOpcode("cpy", ["immediate", "zeroPage", "absolute"]),
81
+ defineOpcode("dec", ["zeroPage", "zeroPageX", "absolute", "absoluteX"]),
82
+ defineOpcode("dex", ["implied"]),
83
+ defineOpcode("dey", ["implied"]),
84
+ defineOpcode("eor", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
85
+ defineOpcode("inc", ["zeroPage", "zeroPageX", "absolute", "absoluteX"]),
86
+ defineOpcode("inx", ["implied"]),
87
+ defineOpcode("iny", ["implied"]),
88
+ defineOpcode("jmp", ["absolute", "indirect"]),
89
+ defineOpcode("jsr", ["absolute"]),
90
+ defineOpcode("lda", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
91
+ defineOpcode("ldx", ["immediate", "zeroPage", "zeroPageY", "absolute", "absoluteY"]),
92
+ defineOpcode("ldy", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX"]),
93
+ defineOpcode("lsr", ["accumulator", "zeroPage", "zeroPageX", "absolute", "absoluteX"]),
94
+ defineOpcode("nop", ["implied"]),
95
+ defineOpcode("ora", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
96
+ defineOpcode("pha", ["implied"]),
97
+ defineOpcode("php", ["implied"]),
98
+ defineOpcode("pla", ["implied"]),
99
+ defineOpcode("plp", ["implied"]),
100
+ defineOpcode("rol", ["accumulator", "zeroPage", "zeroPageX", "absolute", "absoluteX"]),
101
+ defineOpcode("ror", ["accumulator", "zeroPage", "zeroPageX", "absolute", "absoluteX"]),
102
+ defineOpcode("rti", ["implied"]),
103
+ defineOpcode("rts", ["implied"]),
104
+ defineOpcode("sbc", ["immediate", "zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
105
+ defineOpcode("sec", ["implied"]),
106
+ defineOpcode("sed", ["implied"]),
107
+ defineOpcode("sei", ["implied"]),
108
+ defineOpcode("sta", ["zeroPage", "zeroPageX", "absolute", "absoluteX", "absoluteY", "indexedIndirect", "indirectIndexed"]),
109
+ defineOpcode("stx", ["zeroPage", "zeroPageY", "absolute"]),
110
+ defineOpcode("sty", ["zeroPage", "zeroPageX", "absolute"]),
111
+ defineOpcode("tax", ["implied"]),
112
+ defineOpcode("tay", ["implied"]),
113
+ defineOpcode("tsx", ["implied"]),
114
+ defineOpcode("txa", ["implied"]),
115
+ defineOpcode("txs", ["implied"]),
116
+ defineOpcode("tya", ["implied"])
117
+ ];
118
+
119
+ export const opcodeTable = new Map(
120
+ opcodeDefinitions.map((definition) => [definition.mnemonic, definition] as const)
121
+ );
122
+
123
+ export const directiveDefinitions: readonly DirectiveDefinition[] = [
124
+ defineDirective("asm", "include", true, "Assemble another source file immediately."),
125
+ defineDirective("asc", "data", true, "Emit an ASCII string."),
126
+ defineDirective("da", "data", true, "Emit an address-sized value."),
127
+ defineDirective("db", "data", true, "Emit byte data."),
128
+ defineDirective("dend", "storage", true, "End a DUM storage section."),
129
+ defineDirective("dsk", "build", true, "Set the output disk or image name."),
130
+ defineDirective("ds", "storage", true, "Reserve storage bytes."),
131
+ defineDirective("dum", "storage", true, "Begin a DUM storage section."),
132
+ defineDirective("end", "assembler", true, "End assembly."),
133
+ defineDirective("equ", "assembler", true, "Define a symbolic constant."),
134
+ defineDirective("err", "assembler", true, "Force an assembly error."),
135
+ defineDirective("hex", "data", true, "Emit raw hexadecimal bytes."),
136
+ defineDirective("mac", "assembler", true, "Begin a macro definition."),
137
+ defineDirective("mx", "mode", false, "65816-only accumulator and index width control."),
138
+ defineDirective("org", "assembler", true, "Set or restore the assembly origin."),
139
+ defineDirective("put", "include", true, "Include another source file."),
140
+ defineDirective("sav", "build", true, "Save an output file."),
141
+ defineDirective("sna", "build", true, "Set the output file name."),
142
+ defineDirective("str", "data", true, "Emit a Merlin string."),
143
+ defineDirective("typ", "build", true, "Set the output file type."),
144
+ defineDirective("use", "include", true, "Include a library-style source file."),
145
+ defineDirective("xc", "mode", false, "65816-only extended instruction mode control.")
146
+ ];
147
+
148
+ export const directiveTable = new Map(
149
+ directiveDefinitions.map((definition) => [definition.name, definition] as const)
150
+ );
@@ -0,0 +1,197 @@
1
+ import { directiveTable } from "./metadata";
2
+ import { type Expression, type Operand, parseExpression, parseOperand } from "./expression";
3
+ import { type LexedLine, lexSource, type Token } from "./lexer";
4
+
5
+ export type ParsedLine =
6
+ | EmptyLine
7
+ | CommentLine
8
+ | LabelOnlyLine
9
+ | EquateLine
10
+ | InstructionLine
11
+ | DirectiveLine
12
+ | DataLine
13
+ | MalformedLine;
14
+
15
+ export type EmptyLine = {
16
+ shape: "empty";
17
+ text: string;
18
+ };
19
+
20
+ export type CommentLine = {
21
+ shape: "commentOnly";
22
+ text: string;
23
+ comment: string;
24
+ };
25
+
26
+ export type LabelOnlyLine = {
27
+ shape: "labelOnly";
28
+ text: string;
29
+ label: string;
30
+ };
31
+
32
+ export type EquateLine = {
33
+ shape: "equate";
34
+ text: string;
35
+ label: string;
36
+ expression: Expression;
37
+ };
38
+
39
+ export type InstructionLine = {
40
+ shape: "instruction";
41
+ text: string;
42
+ label: string | null;
43
+ mnemonic: string;
44
+ operand: Operand | null;
45
+ };
46
+
47
+ export type DirectiveLine = {
48
+ shape: "directive";
49
+ text: string;
50
+ label: string | null;
51
+ directive: string;
52
+ operand: Expression | null;
53
+ };
54
+
55
+ export type DataLine = {
56
+ shape: "data";
57
+ text: string;
58
+ label: string | null;
59
+ directive: string;
60
+ payload: string;
61
+ };
62
+
63
+ export type MalformedLine = {
64
+ shape: "malformed";
65
+ text: string;
66
+ message: string;
67
+ };
68
+
69
+ const dataDirectiveKinds = new Set(["data"]);
70
+
71
+ export function parseSourceLines(source: string): readonly ParsedLine[] {
72
+ return lexSource(source).lines.map(parseLexedLine);
73
+ }
74
+
75
+ export function parseLexedLine(line: LexedLine): ParsedLine {
76
+ const tokens = stripTrailingComment(line.tokens);
77
+
78
+ if (tokens.length === 0) {
79
+ const commentToken = line.tokens[0];
80
+ if (commentToken?.kind === "comment") {
81
+ return {
82
+ shape: "commentOnly",
83
+ text: line.text,
84
+ comment: commentToken.lexeme
85
+ };
86
+ }
87
+
88
+ return {
89
+ shape: "empty",
90
+ text: line.text
91
+ };
92
+ }
93
+
94
+ try {
95
+ return parseStructuredLine(line.text, tokens);
96
+ } catch (error) {
97
+ return {
98
+ shape: "malformed",
99
+ text: line.text,
100
+ message: error instanceof Error ? error.message : "unknown parse failure"
101
+ };
102
+ }
103
+ }
104
+
105
+ function parseStructuredLine(text: string, tokens: readonly Token[]): ParsedLine {
106
+ let index = 0;
107
+ let label: string | null = null;
108
+
109
+ if (tokens[index]?.kind === "label" || tokens[index]?.kind === "localLabel") {
110
+ label = tokens[index]?.lexeme ?? null;
111
+ index += 1;
112
+ }
113
+
114
+ const token = tokens[index];
115
+ if (token === undefined) {
116
+ if (label !== null) {
117
+ return {
118
+ shape: "labelOnly",
119
+ text,
120
+ label
121
+ };
122
+ }
123
+
124
+ return {
125
+ shape: "empty",
126
+ text
127
+ };
128
+ }
129
+
130
+ if (token.kind === "expressionOperator" && token.lexeme === "=") {
131
+ if (label === null) {
132
+ throw new Error("equate requires a label");
133
+ }
134
+
135
+ const parsed = parseExpression(tokens, index + 1);
136
+ return {
137
+ shape: "equate",
138
+ text,
139
+ label,
140
+ expression: parsed.expression
141
+ };
142
+ }
143
+
144
+ if (token.kind === "mnemonic") {
145
+ const operandTokens = tokens.slice(index + 1);
146
+ const operand = operandTokens.length > 0 ? parseOperand(operandTokens).operand : null;
147
+ return {
148
+ shape: "instruction",
149
+ text,
150
+ label,
151
+ mnemonic: token.lexeme.toLowerCase(),
152
+ operand
153
+ };
154
+ }
155
+
156
+ if (token.kind === "directive") {
157
+ const directive = directiveTable.get(token.lexeme.toLowerCase());
158
+ if (directive !== undefined && dataDirectiveKinds.has(directive.kind)) {
159
+ return {
160
+ shape: "data",
161
+ text,
162
+ label,
163
+ directive: token.lexeme.toLowerCase(),
164
+ payload: tokens.slice(index + 1).map((current) => current.lexeme).join("")
165
+ };
166
+ }
167
+
168
+ const operandTokens = tokens.slice(index + 1);
169
+ const directiveName = token.lexeme.toLowerCase();
170
+
171
+ if (operandTokens.length > 0 && (directiveName === "end" || directiveName === "dend" || directiveName === "xc")) {
172
+ throw new Error(`unexpected operand for ${directiveName}`);
173
+ }
174
+
175
+ const operand = operandTokens.length > 0
176
+ ? parseExpression(operandTokens).expression
177
+ : null;
178
+ return {
179
+ shape: "directive",
180
+ text,
181
+ label,
182
+ directive: directiveName,
183
+ operand
184
+ };
185
+ }
186
+
187
+ throw new Error(`unsupported line start: ${token.lexeme}`);
188
+ }
189
+
190
+ function stripTrailingComment(tokens: readonly Token[]): readonly Token[] {
191
+ const commentIndex = tokens.findIndex((token) => token.kind === "comment");
192
+ if (commentIndex === -1) {
193
+ return tokens;
194
+ }
195
+
196
+ return tokens.slice(0, commentIndex);
197
+ }
@@ -0,0 +1,55 @@
1
+ import { directiveTable } from "./metadata";
2
+ import { type ParsedDocument } from "./document";
3
+
4
+ export type SymbolKind = "label" | "equate" | "data";
5
+
6
+ export type SymbolDefinition = {
7
+ name: string;
8
+ kind: SymbolKind;
9
+ line: number;
10
+ };
11
+
12
+ export function collectSymbols(document: ParsedDocument): Map<string, SymbolDefinition> {
13
+ const symbols = new Map<string, SymbolDefinition>();
14
+
15
+ for (const line of document.lines) {
16
+ const node = line.node;
17
+
18
+ if (node.shape === "equate") {
19
+ symbols.set(node.label, defineSymbol(node.label, "equate", line.line));
20
+ continue;
21
+ }
22
+
23
+ if (node.shape === "labelOnly") {
24
+ symbols.set(node.label, defineSymbol(node.label, "label", line.line));
25
+ continue;
26
+ }
27
+
28
+ if (node.shape === "instruction" && node.label !== null) {
29
+ symbols.set(node.label, defineSymbol(node.label, "label", line.line));
30
+ continue;
31
+ }
32
+
33
+ if (node.shape === "directive" && node.label !== null) {
34
+ const directive = directiveTable.get(node.directive);
35
+ if (directive?.kind === "data" || directive?.kind === "storage") {
36
+ symbols.set(node.label, defineSymbol(node.label, "data", line.line));
37
+ }
38
+ continue;
39
+ }
40
+
41
+ if (node.shape === "data" && node.label !== null) {
42
+ symbols.set(node.label, defineSymbol(node.label, "data", line.line));
43
+ }
44
+ }
45
+
46
+ return symbols;
47
+ }
48
+
49
+ function defineSymbol(name: string, kind: SymbolKind, line: number): SymbolDefinition {
50
+ return {
51
+ name,
52
+ kind,
53
+ line
54
+ };
55
+ }
@@ -0,0 +1,76 @@
1
+ export type TokenKindDefinition = {
2
+ name: string;
3
+ description: string;
4
+ captureExamples: readonly string[];
5
+ };
6
+
7
+ export type LineShapeDefinition = {
8
+ name: string;
9
+ description: string;
10
+ allowsLabel: boolean;
11
+ allowsExpression: boolean;
12
+ requiresOperand: boolean;
13
+ terminal: boolean;
14
+ };
15
+
16
+ function defineTokenKind(
17
+ name: string,
18
+ description: string,
19
+ captureExamples: readonly string[]
20
+ ): TokenKindDefinition {
21
+ return {
22
+ name,
23
+ description,
24
+ captureExamples
25
+ };
26
+ }
27
+
28
+ function defineLineShape(
29
+ name: string,
30
+ description: string,
31
+ allowsLabel: boolean,
32
+ allowsExpression: boolean,
33
+ requiresOperand: boolean,
34
+ terminal: boolean
35
+ ): LineShapeDefinition {
36
+ return {
37
+ name,
38
+ description,
39
+ allowsLabel,
40
+ allowsExpression,
41
+ requiresOperand,
42
+ terminal
43
+ };
44
+ }
45
+
46
+ export const tokenKindDefinitions: readonly TokenKindDefinition[] = [
47
+ defineTokenKind("comment", "Line or trailing comment text.", ["; trailing note", "* monitor addresses"]),
48
+ defineTokenKind("label", "Global symbol at the start of a line.", ["TEST_START", "GetKey"]),
49
+ defineTokenKind("localLabel", "Merlin local label form.", ["]loop", ":good"]),
50
+ defineTokenKind("directive", "Assembler directive or pseudo-op.", ["org", "dum", "hex"]),
51
+ defineTokenKind("mnemonic", "6502 instruction mnemonic.", ["lda", "adc", "jmp"]),
52
+ defineTokenKind("string", "Quoted string literal.", ["\"THE END\"", "'A'"]),
53
+ defineTokenKind("numericLiteral", "Numeric literal in Merlin syntax.", ["$800", "#0", "%00"]),
54
+ defineTokenKind("modifier", "Unary byte or bank selector modifier.", ["<value", ">value", "^value"]),
55
+ defineTokenKind("expressionOperator", "Operator inside an expression.", ["+", "-", "*"]),
56
+ defineTokenKind("identifier", "Non-label symbol reference.", ["_tmp", "dumSize", "DOSWARM"])
57
+ ];
58
+
59
+ export const tokenKindTable = new Map(
60
+ tokenKindDefinitions.map((definition) => [definition.name, definition] as const)
61
+ );
62
+
63
+ export const lineShapeDefinitions: readonly LineShapeDefinition[] = [
64
+ defineLineShape("empty", "Whitespace-only line.", false, false, false, true),
65
+ defineLineShape("commentOnly", "Full-line comment.", false, false, false, true),
66
+ defineLineShape("labelOnly", "Standalone label with no operation.", true, false, false, true),
67
+ defineLineShape("instruction", "Instruction mnemonic with optional operand.", true, true, false, true),
68
+ defineLineShape("directive", "Directive with optional operand depending on directive kind.", true, true, false, true),
69
+ defineLineShape("equate", "Symbol definition using an expression.", true, true, true, true),
70
+ defineLineShape("data", "Data-emitting directive such as ASC, DB, or HEX.", true, true, true, true),
71
+ defineLineShape("malformed", "Token sequence that does not match a known line shape.", true, true, false, true)
72
+ ];
73
+
74
+ export const lineShapeTable = new Map(
75
+ lineShapeDefinitions.map((definition) => [definition.name, definition] as const)
76
+ );
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { parseDocument, type ParsedDocument } from "./document";
5
+ import { type Expression } from "./expression";
6
+ import { collectSymbols } from "./symbols";
7
+
8
+ export type WorkspaceSymbol = {
9
+ name: string;
10
+ kind: "label" | "equate" | "data";
11
+ line: number;
12
+ filePath: string;
13
+ };
14
+
15
+ export type IndexedWorkspace = {
16
+ documents: Map<string, ParsedDocument>;
17
+ dependencies: Map<string, readonly string[]>;
18
+ loadOrder: readonly string[];
19
+ symbols: Map<string, WorkspaceSymbol>;
20
+ };
21
+
22
+ const includeDirectives = new Set(["asm", "put", "use"]);
23
+
24
+ export function indexWorkspace(entryPath: string): IndexedWorkspace {
25
+ const documents = new Map<string, ParsedDocument>();
26
+ const dependencies = new Map<string, readonly string[]>();
27
+ const loadOrder: string[] = [];
28
+ const symbols = new Map<string, WorkspaceSymbol>();
29
+
30
+ visitFile(path.resolve(entryPath), documents, dependencies, loadOrder);
31
+
32
+ for (const filePath of loadOrder) {
33
+ const document = documents.get(filePath);
34
+ if (document === undefined) {
35
+ continue;
36
+ }
37
+
38
+ for (const symbol of collectSymbols(document).values()) {
39
+ symbols.set(symbol.name, {
40
+ ...symbol,
41
+ filePath
42
+ });
43
+ }
44
+ }
45
+
46
+ return {
47
+ documents,
48
+ dependencies,
49
+ loadOrder,
50
+ symbols
51
+ };
52
+ }
53
+
54
+ function visitFile(
55
+ filePath: string,
56
+ documents: Map<string, ParsedDocument>,
57
+ dependencies: Map<string, readonly string[]>,
58
+ loadOrder: string[]
59
+ ): void {
60
+ if (documents.has(filePath)) {
61
+ return;
62
+ }
63
+
64
+ const source = fs.readFileSync(filePath, "utf8");
65
+ const document = parseDocument(source);
66
+ documents.set(filePath, document);
67
+ loadOrder.push(filePath);
68
+
69
+ const resolvedDependencies = document.lines
70
+ .flatMap((line) => {
71
+ const node = line.node;
72
+ if (node.shape !== "directive" || !includeDirectives.has(node.directive)) {
73
+ return [];
74
+ }
75
+
76
+ const includePath = readIncludePath(node.operand);
77
+ if (includePath === null) {
78
+ return [];
79
+ }
80
+
81
+ return [path.resolve(path.dirname(filePath), includePath)];
82
+ });
83
+
84
+ dependencies.set(filePath, resolvedDependencies);
85
+
86
+ for (const dependencyPath of resolvedDependencies) {
87
+ visitFile(dependencyPath, documents, dependencies, loadOrder);
88
+ }
89
+ }
90
+
91
+ function readIncludePath(expression: Expression | null): string | null {
92
+ if (expression === null) {
93
+ return null;
94
+ }
95
+
96
+ if (expression.kind === "identifier") {
97
+ return expression.value;
98
+ }
99
+
100
+ if (expression.kind === "string") {
101
+ return expression.value;
102
+ }
103
+
104
+ return null;
105
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startServer } from "./server";
4
+
5
+ const usage = "Usage: merls --stdio";
6
+
7
+ export function runCli(argv: readonly string[]): number {
8
+ if (argv.length === 1 && argv[0] === "--stdio") {
9
+ startServer();
10
+ return 0;
11
+ }
12
+
13
+ if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
14
+ process.stdout.write(`${usage}\n`);
15
+ return 0;
16
+ }
17
+
18
+ process.stderr.write(`${usage}\n`);
19
+ return 1;
20
+ }
21
+
22
+ if (require.main === module) {
23
+ process.exitCode = runCli(process.argv.slice(2));
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export const projectName = "merls";
@@ -0,0 +1,42 @@
1
+ import {
2
+ CompletionItemKind,
3
+ type CompletionItem
4
+ } from "vscode-languageserver/node";
5
+
6
+ import { directiveDefinitions, opcodeDefinitions } from "../asm/metadata";
7
+ import { parseDocument } from "../asm/document";
8
+ import { collectSymbols } from "../asm/symbols";
9
+
10
+ export function buildCompletionItems(
11
+ openDocuments: ReadonlyMap<string, string>,
12
+ uri: string,
13
+ line: number
14
+ ): CompletionItem[] {
15
+ const source = openDocuments.get(uri);
16
+ if (source === undefined) {
17
+ return [];
18
+ }
19
+
20
+ const document = parseDocument(source);
21
+ const text = document.lines[line]?.node.text.trimStart().toLowerCase() ?? "";
22
+
23
+ if (text === "ld" || text === "ld\n" || text.startsWith("ld")) {
24
+ return opcodeDefinitions.map((opcode) => ({
25
+ label: opcode.mnemonic,
26
+ kind: CompletionItemKind.Keyword
27
+ }));
28
+ }
29
+
30
+ if (text === "du" || text.startsWith("du")) {
31
+ return directiveDefinitions.map((directive) => ({
32
+ label: directive.name,
33
+ kind: CompletionItemKind.Function
34
+ }));
35
+ }
36
+
37
+ const symbols = collectSymbols(document);
38
+ return [...symbols.values()].map((symbol) => ({
39
+ label: symbol.name,
40
+ kind: CompletionItemKind.Variable
41
+ }));
42
+ }