@jhlagado/azm 0.2.10 → 0.2.11
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/README.md +14 -0
- package/dist/src/core/compile.js +97 -1
- package/dist/src/expansion/op-expansion.js +47 -9
- package/dist/src/outputs/d8-files.js +1 -0
- package/dist/src/outputs/types.d.ts +1 -0
- package/dist/src/source/instruction-chain.d.ts +5 -0
- package/dist/src/source/instruction-chain.js +75 -0
- package/dist/src/syntax/names.d.ts +18 -0
- package/dist/src/syntax/names.js +44 -0
- package/dist/src/syntax/parse-data-directives.d.ts +17 -0
- package/dist/src/syntax/parse-data-directives.js +147 -0
- package/dist/src/syntax/parse-declaration-directives.d.ts +18 -0
- package/dist/src/syntax/parse-declaration-directives.js +90 -0
- package/dist/src/syntax/parse-diagnostics.d.ts +8 -0
- package/dist/src/syntax/parse-diagnostics.js +15 -0
- package/dist/src/syntax/parse-directive-statement.d.ts +1 -5
- package/dist/src/syntax/parse-directive-statement.js +19 -259
- package/dist/src/syntax/parse-instruction-chain.d.ts +22 -0
- package/dist/src/syntax/parse-instruction-chain.js +62 -0
- package/dist/src/syntax/parse-layout-declarations.js +9 -18
- package/dist/src/syntax/parse-layout-expression.js +4 -3
- package/dist/src/syntax/parse-line.js +20 -31
- package/dist/src/syntax/parse-location-directives.d.ts +7 -0
- package/dist/src/syntax/parse-location-directives.js +15 -0
- package/dist/src/syntax/statement-classification.d.ts +2 -0
- package/dist/src/syntax/statement-classification.js +24 -0
- package/dist/src/tooling/case-style.js +42 -26
- package/docs/codebase/02-source-loading-and-parsing.md +28 -8
- package/docs/codebase/04-ops-and-register-contracts.md +24 -3
- package/docs/codebase/05-interfaces-and-output-artifacts.md +10 -0
- package/docs/codebase/06-verification-and-maintenance.md +9 -3
- package/docs/codebase/appendices/a-directory-file-reference.md +17 -10
- package/docs/codebase/appendices/b-compile-flow-reference.md +3 -2
- package/docs/codebase/index.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ AZM manual and broader Debug80 documentation:
|
|
|
10
10
|
- [Debug80 documentation](https://debug80.com/)
|
|
11
11
|
- [AZM Book 0 — Assembler Manual](https://debug80.com/azm-book/book0/)
|
|
12
12
|
- [AZM Book 4](https://jhlagado.github.io/debug80-docs/azm-book/book4/)
|
|
13
|
+
- [AZM Grammar Reference](docs/reference/azm-grammar.md)
|
|
13
14
|
|
|
14
15
|
## Install
|
|
15
16
|
|
|
@@ -185,6 +186,19 @@ indent instructions and standalone directives, and align operands enough to keep
|
|
|
185
186
|
dense assembly readable. Exact tab width is less important than keeping one
|
|
186
187
|
source file internally consistent.
|
|
187
188
|
|
|
189
|
+
AZM normally uses one statement per physical line. For short, dense instruction
|
|
190
|
+
sequences, a physical line may contain multiple instructions or `op` invocations
|
|
191
|
+
separated by a spaced backslash:
|
|
192
|
+
|
|
193
|
+
```asm
|
|
194
|
+
Loop: ld a,(hl) \ inc hl \ djnz Loop
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This is only instruction compaction. Directives and declarations still belong on
|
|
198
|
+
their own lines, and labels are only allowed before the first chained
|
|
199
|
+
instruction. A semicolon still starts a comment; it is not an instruction
|
|
200
|
+
separator.
|
|
201
|
+
|
|
188
202
|
## Literals
|
|
189
203
|
|
|
190
204
|
AZM accepts the usual Z80 numeric forms:
|
package/dist/src/core/compile.js
CHANGED
|
@@ -2,10 +2,12 @@ import { assembleProgram } from '../assembly/assemble-program.js';
|
|
|
2
2
|
import { writeIntelHex } from '../outputs/hex.js';
|
|
3
3
|
import { createSourceFile } from '../source/source-file.js';
|
|
4
4
|
import { scanLogicalLines } from '../source/logical-lines.js';
|
|
5
|
-
import { stripLineComment } from '../source/strip-line-comment.js';
|
|
5
|
+
import { extractLineComment, stripLineComment } from '../source/strip-line-comment.js';
|
|
6
|
+
import { parseInstructionChain } from '../syntax/parse-instruction-chain.js';
|
|
6
7
|
import { parseLogicalLine } from '../syntax/parse-line.js';
|
|
7
8
|
import { parseLayoutDeclarationAt } from '../syntax/parse-layout-declarations.js';
|
|
8
9
|
import { collectOps, expandOpInvocation, parseOpInvocation, } from '../expansion/op-expansion.js';
|
|
10
|
+
import { parseZ80Instruction } from '../z80/parse-instruction.js';
|
|
9
11
|
import { applyConditionalAssembly } from './conditional-assembly.js';
|
|
10
12
|
export function parseNextSourceItems(lines, options = {}) {
|
|
11
13
|
const diagnostics = [];
|
|
@@ -43,6 +45,9 @@ function parsePendingLine(context, index, afterTopLevelEnd) {
|
|
|
43
45
|
if (parseExpandedOpLine(context, line)) {
|
|
44
46
|
return { consumedUntilIndex: index, afterTopLevelEnd };
|
|
45
47
|
}
|
|
48
|
+
if (parseInstructionChainLine(context, line)) {
|
|
49
|
+
return { consumedUntilIndex: index, afterTopLevelEnd };
|
|
50
|
+
}
|
|
46
51
|
return parseNormalLine(context, index, line, afterTopLevelEnd);
|
|
47
52
|
}
|
|
48
53
|
function shouldSkipPendingLine(context, index, line, afterTopLevelEnd) {
|
|
@@ -78,6 +83,97 @@ function parseNormalLine(context, index, line, afterTopLevelEnd) {
|
|
|
78
83
|
afterTopLevelEnd: afterTopLevelEnd || result.items.some((item) => item.kind === 'end'),
|
|
79
84
|
};
|
|
80
85
|
}
|
|
86
|
+
function parseInstructionChainLine(context, line) {
|
|
87
|
+
const parsed = parseInstructionChain({
|
|
88
|
+
line,
|
|
89
|
+
parseStatement: (segmentLine, statementText, statementColumn) => parseChainStatement(context, segmentLine, statementText, statementColumn),
|
|
90
|
+
makeLabelItem: (label, segmentLine) => ({
|
|
91
|
+
kind: 'label',
|
|
92
|
+
name: label.name,
|
|
93
|
+
...(label.isEntry ? { isEntry: true } : {}),
|
|
94
|
+
span: spanAt(segmentLine, label.labelColumn),
|
|
95
|
+
}),
|
|
96
|
+
makeDiagnostic: chainDiagnostic,
|
|
97
|
+
appendLineComment: appendChainComment,
|
|
98
|
+
});
|
|
99
|
+
if (parsed === undefined)
|
|
100
|
+
return false;
|
|
101
|
+
context.diagnostics.push(...parsed.diagnostics);
|
|
102
|
+
context.items.push(...parsed.items);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
function parseChainStatement(context, line, statementText, statementColumn) {
|
|
106
|
+
const segmentLine = paddedSegmentLine(line, statementText, statementColumn);
|
|
107
|
+
const opCall = parseOpInvocation(segmentLine);
|
|
108
|
+
const overloads = opCall ? context.ops.get(opCall.name) : undefined;
|
|
109
|
+
if (opCall && overloads) {
|
|
110
|
+
const diagnostics = [];
|
|
111
|
+
return {
|
|
112
|
+
items: expandOpInvocation(context.ops, overloads, opCall.operands, segmentLine, diagnostics),
|
|
113
|
+
diagnostics,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return parseChainInstruction(line, statementText, statementColumn);
|
|
117
|
+
}
|
|
118
|
+
function parseChainInstruction(line, text, column) {
|
|
119
|
+
const instruction = parseZ80Instruction(text);
|
|
120
|
+
if (instruction?.instruction) {
|
|
121
|
+
return {
|
|
122
|
+
items: [{ kind: 'instruction', instruction: instruction.instruction, span: spanAt(line, column) }],
|
|
123
|
+
diagnostics: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (instruction?.diagnostics && instruction.diagnostics.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
items: [],
|
|
129
|
+
diagnostics: instruction.diagnostics.map((message) => chainDiagnostic(line, column, message)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (instruction?.error) {
|
|
133
|
+
return { items: [], diagnostics: [chainDiagnostic(line, column, instruction.error)] };
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
items: [],
|
|
137
|
+
diagnostics: [chainDiagnostic(line, column, `unsupported source line: ${text}`)],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function appendChainComment(items, line) {
|
|
141
|
+
const comment = extractLineComment(line.text);
|
|
142
|
+
if (!comment)
|
|
143
|
+
return;
|
|
144
|
+
items.push({
|
|
145
|
+
kind: 'comment',
|
|
146
|
+
text: comment,
|
|
147
|
+
origin: 'user',
|
|
148
|
+
span: spanAt(line, firstColumn(line.text)),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function paddedSegmentLine(line, text, column) {
|
|
152
|
+
return { ...line, text: `${' '.repeat(Math.max(0, column - 1))}${text}` };
|
|
153
|
+
}
|
|
154
|
+
function spanAt(line, column) {
|
|
155
|
+
return {
|
|
156
|
+
sourceName: line.sourceName,
|
|
157
|
+
line: line.line,
|
|
158
|
+
column,
|
|
159
|
+
...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
|
|
160
|
+
...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function firstColumn(text) {
|
|
164
|
+
const match = /\S/.exec(text);
|
|
165
|
+
return match ? match.index + 1 : 1;
|
|
166
|
+
}
|
|
167
|
+
function chainDiagnostic(line, column, message) {
|
|
168
|
+
return {
|
|
169
|
+
severity: 'error',
|
|
170
|
+
code: 'AZMN_PARSE',
|
|
171
|
+
message,
|
|
172
|
+
sourceName: line.sourceName,
|
|
173
|
+
line: line.line,
|
|
174
|
+
column,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
81
177
|
export function compileSource(sourceText, options = {}) {
|
|
82
178
|
const source = createSourceFile(options.entryName ?? '<memory>', sourceText);
|
|
83
179
|
const { diagnostics, items } = parseNextSourceItems(scanLogicalLines(source));
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { stripLineComment } from '../source/strip-line-comment.js';
|
|
2
|
+
import { IDENTIFIER_PATTERN } from '../syntax/names.js';
|
|
3
|
+
import { parseInstructionChain, } from '../syntax/parse-instruction-chain.js';
|
|
2
4
|
import { parseLogicalLine } from '../syntax/parse-line.js';
|
|
3
5
|
import { expandSelectedOp } from './op-expand-selected.js';
|
|
4
6
|
import { splitOperands } from './op-operand-splitting.js';
|
|
@@ -21,7 +23,7 @@ export function collectOps(lines, diagnostics, parseOptions = {}) {
|
|
|
21
23
|
return { ops, opLineIndexes };
|
|
22
24
|
}
|
|
23
25
|
function parseOpHeader(line, diagnostics) {
|
|
24
|
-
const opHeader =
|
|
26
|
+
const opHeader = new RegExp(`^op\\s+(${IDENTIFIER_PATTERN})\\s*\\((.*)\\)\\s*$`, 'i').exec(stripLineComment(line.text).trim());
|
|
25
27
|
if (!opHeader)
|
|
26
28
|
return undefined;
|
|
27
29
|
return { name: opHeader[1] ?? '', params: parseOpParams(opHeader[2] ?? '', line, diagnostics) };
|
|
@@ -35,9 +37,8 @@ function collectOpBody(lines, startIndex, params, diagnostics, parseOptions, opL
|
|
|
35
37
|
if (isOpEnd(bodyLine.text)) {
|
|
36
38
|
return { body, terminated: true, endIndex: index };
|
|
37
39
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
body.push(template);
|
|
40
|
+
const templates = parseOpBodyTemplates(bodyLine, paramNames, diagnostics, parseOptions);
|
|
41
|
+
body.push(...templates);
|
|
41
42
|
}
|
|
42
43
|
return { body, terminated: false, endIndex: lines.length };
|
|
43
44
|
}
|
|
@@ -60,7 +61,7 @@ function recordCollectedOp(ops, header, collected, line, diagnostics) {
|
|
|
60
61
|
}
|
|
61
62
|
export function parseOpInvocation(line) {
|
|
62
63
|
const text = stripLineComment(line.text).trim();
|
|
63
|
-
const match =
|
|
64
|
+
const match = new RegExp(`^(${IDENTIFIER_PATTERN})(?:\\s+(.+))?$`).exec(text);
|
|
64
65
|
if (!match) {
|
|
65
66
|
return undefined;
|
|
66
67
|
}
|
|
@@ -87,7 +88,7 @@ function parseOpParams(text, line, diagnostics) {
|
|
|
87
88
|
}
|
|
88
89
|
const params = [];
|
|
89
90
|
for (const part of parts) {
|
|
90
|
-
const match =
|
|
91
|
+
const match = new RegExp(`^(${IDENTIFIER_PATTERN})\\s+([A-Za-z][A-Za-z0-9_]*)$`).exec(part.trim());
|
|
91
92
|
if (!match) {
|
|
92
93
|
diagnostics.push(parseDiagnostic(line, 'Invalid op parameter list: trailing or empty entries are not permitted.'));
|
|
93
94
|
continue;
|
|
@@ -115,8 +116,42 @@ function parseOpBodyTemplate(line, paramNames, diagnostics, parseOptions) {
|
|
|
115
116
|
return parsedSource;
|
|
116
117
|
return template;
|
|
117
118
|
}
|
|
119
|
+
function parseOpBodyTemplates(line, paramNames, diagnostics, parseOptions) {
|
|
120
|
+
const parsed = parseInstructionChain({
|
|
121
|
+
line,
|
|
122
|
+
parseStatement: (segmentLine, statementText, statementColumn) => parseOpBodyStatement(paddedLine(segmentLine, statementText, statementColumn), paramNames, diagnostics, parseOptions),
|
|
123
|
+
makeLabelItem: (label, segmentLine) => ({
|
|
124
|
+
kind: 'source-items',
|
|
125
|
+
items: [
|
|
126
|
+
{
|
|
127
|
+
kind: 'label',
|
|
128
|
+
name: label.name,
|
|
129
|
+
...(label.isEntry ? { isEntry: true } : {}),
|
|
130
|
+
span: { sourceName: segmentLine.sourceName, line: segmentLine.line, column: label.labelColumn },
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
}),
|
|
134
|
+
makeDiagnostic: parseDiagnosticAt,
|
|
135
|
+
});
|
|
136
|
+
if (parsed === undefined) {
|
|
137
|
+
const template = parseOpBodyTemplate(line, paramNames, diagnostics, parseOptions);
|
|
138
|
+
return template ? [template] : [];
|
|
139
|
+
}
|
|
140
|
+
diagnostics.push(...parsed.diagnostics);
|
|
141
|
+
return parsed.items;
|
|
142
|
+
}
|
|
143
|
+
function parseOpBodyStatement(line, paramNames, diagnostics, parseOptions) {
|
|
144
|
+
const statementDiagnostics = [];
|
|
145
|
+
const template = parseOpBodyTemplate(line, paramNames, statementDiagnostics, parseOptions);
|
|
146
|
+
return template
|
|
147
|
+
? { items: [template], diagnostics: statementDiagnostics }
|
|
148
|
+
: { items: [], diagnostics: statementDiagnostics };
|
|
149
|
+
}
|
|
150
|
+
function paddedLine(line, text, column) {
|
|
151
|
+
return { ...line, text: `${' '.repeat(Math.max(0, column - 1))}${text}` };
|
|
152
|
+
}
|
|
118
153
|
function parseTemplateInstructionCandidate(text, paramNames) {
|
|
119
|
-
const instruction =
|
|
154
|
+
const instruction = new RegExp(`^(${IDENTIFIER_PATTERN})(?:\\s+(.+))?$`).exec(text);
|
|
120
155
|
if (!instruction)
|
|
121
156
|
return undefined;
|
|
122
157
|
const operands = parseTemplateOperands(instruction[2] ?? '', paramNames);
|
|
@@ -155,7 +190,7 @@ function parseTemplateOperand(text, paramNames) {
|
|
|
155
190
|
if (paramNames.has(trimmed)) {
|
|
156
191
|
return { kind: 'param', name: trimmed };
|
|
157
192
|
}
|
|
158
|
-
const portParam =
|
|
193
|
+
const portParam = new RegExp(`^\\(\\s*(${IDENTIFIER_PATTERN})\\s*\\)$`).exec(trimmed);
|
|
159
194
|
if (portParam && paramNames.has(portParam[1] ?? '')) {
|
|
160
195
|
return { kind: 'port-param', name: portParam[1] ?? '' };
|
|
161
196
|
}
|
|
@@ -169,13 +204,16 @@ function isOpEnd(text) {
|
|
|
169
204
|
return /^end\s*$/i.test(stripLineComment(text).trim());
|
|
170
205
|
}
|
|
171
206
|
function parseDiagnostic(line, message) {
|
|
207
|
+
return parseDiagnosticAt(line, firstColumn(line.text), message);
|
|
208
|
+
}
|
|
209
|
+
function parseDiagnosticAt(line, column, message) {
|
|
172
210
|
return {
|
|
173
211
|
severity: 'error',
|
|
174
212
|
code: 'AZMN_PARSE',
|
|
175
213
|
message,
|
|
176
214
|
sourceName: line.sourceName,
|
|
177
215
|
line: line.line,
|
|
178
|
-
column
|
|
216
|
+
column,
|
|
179
217
|
};
|
|
180
218
|
}
|
|
181
219
|
function firstColumn(text) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { findLineCommentStart } from './line-comment-scanner.js';
|
|
2
|
+
export function splitInstructionChain(text) {
|
|
3
|
+
const commentStart = findLineCommentStart(text);
|
|
4
|
+
const codeText = commentStart === undefined ? text : text.slice(0, commentStart);
|
|
5
|
+
const separators = findChainSeparators(codeText);
|
|
6
|
+
if (separators.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
const segments = [];
|
|
9
|
+
let start = 0;
|
|
10
|
+
for (const separator of [...separators, codeText.length]) {
|
|
11
|
+
const raw = codeText.slice(start, separator);
|
|
12
|
+
segments.push(segmentFromRaw(raw, start));
|
|
13
|
+
start = separator + 1;
|
|
14
|
+
}
|
|
15
|
+
return segments;
|
|
16
|
+
}
|
|
17
|
+
function findChainSeparators(text) {
|
|
18
|
+
const separators = [];
|
|
19
|
+
let state = {};
|
|
20
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
21
|
+
const char = text[index];
|
|
22
|
+
if (state.escaped === true) {
|
|
23
|
+
state = { ...state, escaped: false };
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (char === '\\' && state.quote !== undefined) {
|
|
27
|
+
state = { ...state, escaped: true };
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (startsOrEndsQuote(text, index, state)) {
|
|
31
|
+
state =
|
|
32
|
+
state.quote === char
|
|
33
|
+
? withoutQuote(state)
|
|
34
|
+
: withQuote(state, state.quote ?? char ?? '');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (char === '\\' && isReadableSeparator(text, index)) {
|
|
38
|
+
separators.push(index);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return separators;
|
|
42
|
+
}
|
|
43
|
+
function segmentFromRaw(raw, rawStart) {
|
|
44
|
+
const leading = /^\s*/.exec(raw)?.[0].length ?? 0;
|
|
45
|
+
const trailing = /\s*$/.exec(raw)?.[0].length ?? 0;
|
|
46
|
+
const text = raw.slice(leading, raw.length - trailing);
|
|
47
|
+
return {
|
|
48
|
+
text,
|
|
49
|
+
column: rawStart + (text.length === 0 ? 0 : leading) + 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function startsOrEndsQuote(text, index, state) {
|
|
53
|
+
const char = text[index];
|
|
54
|
+
return isQuote(char) && !isApostropheSuffix(text, index, state);
|
|
55
|
+
}
|
|
56
|
+
function isQuote(char) {
|
|
57
|
+
return char === '"' || char === "'";
|
|
58
|
+
}
|
|
59
|
+
function isApostropheSuffix(text, index, state) {
|
|
60
|
+
return (state.quote === undefined &&
|
|
61
|
+
text[index] === "'" &&
|
|
62
|
+
/[A-Za-z0-9_]/.test(text[index - 1] ?? ''));
|
|
63
|
+
}
|
|
64
|
+
function withoutQuote(state) {
|
|
65
|
+
return state.escaped === true ? { escaped: true } : {};
|
|
66
|
+
}
|
|
67
|
+
function withQuote(state, quote) {
|
|
68
|
+
return {
|
|
69
|
+
quote,
|
|
70
|
+
...(state.escaped === true ? { escaped: true } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isReadableSeparator(text, index) {
|
|
74
|
+
return /\s/.test(text[index - 1] ?? '') && /\s/.test(text[index + 1] ?? '');
|
|
75
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const IDENTIFIER_PATTERN = "[A-Za-z_][A-Za-z0-9_]*";
|
|
2
|
+
export declare const LABEL_NAME_PATTERN = "[A-Za-z_.$?][A-Za-z0-9_.$?]*";
|
|
3
|
+
export interface ParsedEntryLabel {
|
|
4
|
+
readonly rawLabel: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly isEntry: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ParsedLeadingLabel extends ParsedEntryLabel {
|
|
9
|
+
readonly labelColumn: number;
|
|
10
|
+
readonly statementText: string;
|
|
11
|
+
readonly statementColumn: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function isIdentifier(text: string): boolean;
|
|
14
|
+
export declare function isLabelName(text: string): boolean;
|
|
15
|
+
export declare function parseEntryLabel(text: string): ParsedEntryLabel | undefined;
|
|
16
|
+
export declare function normalizeEntryLabelName(raw: string): string;
|
|
17
|
+
export declare function hasLeadingLabel(text: string): boolean;
|
|
18
|
+
export declare function parseLeadingLabel(text: string, column: number): ParsedLeadingLabel | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const IDENTIFIER_PATTERN = '[A-Za-z_][A-Za-z0-9_]*';
|
|
2
|
+
export const LABEL_NAME_PATTERN = '[A-Za-z_.$?][A-Za-z0-9_.$?]*';
|
|
3
|
+
const IDENTIFIER_RE = new RegExp(`^${IDENTIFIER_PATTERN}$`);
|
|
4
|
+
const LABEL_NAME_RE = new RegExp(`^${LABEL_NAME_PATTERN}$`);
|
|
5
|
+
const ENTRY_LABEL_RE = new RegExp(`^@?${LABEL_NAME_PATTERN}$`);
|
|
6
|
+
const LEADING_LABEL_RE = new RegExp(`^(@?${LABEL_NAME_PATTERN}):\\s*(.*)$`);
|
|
7
|
+
export function isIdentifier(text) {
|
|
8
|
+
return IDENTIFIER_RE.test(text);
|
|
9
|
+
}
|
|
10
|
+
export function isLabelName(text) {
|
|
11
|
+
return LABEL_NAME_RE.test(text);
|
|
12
|
+
}
|
|
13
|
+
export function parseEntryLabel(text) {
|
|
14
|
+
if (!ENTRY_LABEL_RE.test(text))
|
|
15
|
+
return undefined;
|
|
16
|
+
return {
|
|
17
|
+
rawLabel: text,
|
|
18
|
+
name: normalizeEntryLabelName(text),
|
|
19
|
+
isEntry: text.startsWith('@'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function normalizeEntryLabelName(raw) {
|
|
23
|
+
return raw.startsWith('@') ? raw.slice(1) : raw;
|
|
24
|
+
}
|
|
25
|
+
export function hasLeadingLabel(text) {
|
|
26
|
+
return new RegExp(`^@?${LABEL_NAME_PATTERN}:`).test(text);
|
|
27
|
+
}
|
|
28
|
+
export function parseLeadingLabel(text, column) {
|
|
29
|
+
const match = LEADING_LABEL_RE.exec(text);
|
|
30
|
+
if (!match)
|
|
31
|
+
return undefined;
|
|
32
|
+
const rawLabel = match[1] ?? '';
|
|
33
|
+
const parsed = parseEntryLabel(rawLabel);
|
|
34
|
+
if (!parsed)
|
|
35
|
+
return undefined;
|
|
36
|
+
const statementText = match[2] ?? '';
|
|
37
|
+
const statementOffset = text.indexOf(statementText, rawLabel.length + 1);
|
|
38
|
+
return {
|
|
39
|
+
...parsed,
|
|
40
|
+
labelColumn: column,
|
|
41
|
+
statementText,
|
|
42
|
+
statementColumn: column + (statementOffset === -1 ? text.length : statementOffset),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LogicalLine } from '../source/logical-lines.js';
|
|
2
|
+
import type { ParseLineResult } from './parse-line.js';
|
|
3
|
+
export declare function parseDataDirective(line: LogicalLine, directiveText: string, valueText: string, span: {
|
|
4
|
+
readonly sourceName: string;
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly column: number;
|
|
7
|
+
}): ParseLineResult;
|
|
8
|
+
export declare function parseDsDirective(line: LogicalLine, valueText: string, span: {
|
|
9
|
+
readonly sourceName: string;
|
|
10
|
+
readonly line: number;
|
|
11
|
+
readonly column: number;
|
|
12
|
+
}): ParseLineResult;
|
|
13
|
+
export declare function parseStringDataDirective(line: LogicalLine, directive: 'cstr' | 'istr' | 'pstr', valueText: string, span: {
|
|
14
|
+
readonly sourceName: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
readonly column: number;
|
|
17
|
+
}): ParseLineResult;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { parseWholeQuotedString } from './parse-declaration-directives.js';
|
|
2
|
+
import { parseLineError } from './parse-diagnostics.js';
|
|
3
|
+
import { parseExpression, parseTypeExpr } from './parse-expression.js';
|
|
4
|
+
export function parseDataDirective(line, directiveText, valueText, span) {
|
|
5
|
+
const directive = directiveText.slice(1).toLowerCase();
|
|
6
|
+
const parts = splitValueList(valueText);
|
|
7
|
+
const values = directive === 'db'
|
|
8
|
+
? parts.map(parseDataValue).filter((value) => value !== undefined)
|
|
9
|
+
: parts.map(parseExpression).filter((value) => value !== undefined);
|
|
10
|
+
if (values.length !== parts.length) {
|
|
11
|
+
return {
|
|
12
|
+
items: [],
|
|
13
|
+
diagnostics: [parseLineError(line, `invalid .${directive} value list`)],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
items: directive === 'db'
|
|
18
|
+
? [{ kind: 'db', values: values, span }]
|
|
19
|
+
: [{ kind: 'dw', values: values, span }],
|
|
20
|
+
diagnostics: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function parseDsDirective(line, valueText, span) {
|
|
24
|
+
const parts = splitValueList(valueText);
|
|
25
|
+
const listDiagnostic = validateDsValueList(line, parts);
|
|
26
|
+
if (listDiagnostic) {
|
|
27
|
+
return { items: [], diagnostics: [listDiagnostic] };
|
|
28
|
+
}
|
|
29
|
+
const sizeResult = parseDsSize(line, parts[0] ?? '');
|
|
30
|
+
if (sizeResult.diagnostic) {
|
|
31
|
+
return { items: [], diagnostics: [sizeResult.diagnostic] };
|
|
32
|
+
}
|
|
33
|
+
const fillResult = parseDsFill(line, parts[1]);
|
|
34
|
+
if (fillResult.diagnostic) {
|
|
35
|
+
return { items: [], diagnostics: [fillResult.diagnostic] };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
items: [
|
|
39
|
+
fillResult.fill === undefined
|
|
40
|
+
? { kind: 'ds', size: sizeResult.size, span }
|
|
41
|
+
: { kind: 'ds', size: sizeResult.size, fill: fillResult.fill, span },
|
|
42
|
+
],
|
|
43
|
+
diagnostics: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function parseStringDataDirective(line, directive, valueText, span) {
|
|
47
|
+
const value = parseQuotedString(valueText);
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
return {
|
|
50
|
+
items: [],
|
|
51
|
+
diagnostics: [parseLineError(line, `.${directive} expects one double-quoted string`)],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { items: [{ kind: 'string-data', directive, value, span }], diagnostics: [] };
|
|
55
|
+
}
|
|
56
|
+
function validateDsValueList(line, parts) {
|
|
57
|
+
return parts.length < 1 || parts.length > 2
|
|
58
|
+
? parseLineError(line, `invalid .ds value list`)
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
function parseDsSize(line, sizeText) {
|
|
62
|
+
const size = parseTypeSizeExpression(sizeText) ?? parseExpression(sizeText);
|
|
63
|
+
if (!size) {
|
|
64
|
+
return {
|
|
65
|
+
diagnostic: parseLineError(line, `invalid .ds size: ${sizeText}`),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { size };
|
|
69
|
+
}
|
|
70
|
+
function parseDsFill(line, fillText) {
|
|
71
|
+
if (fillText === undefined)
|
|
72
|
+
return { fill: undefined };
|
|
73
|
+
const fill = parseExpression(fillText);
|
|
74
|
+
if (!fill) {
|
|
75
|
+
return {
|
|
76
|
+
diagnostic: parseLineError(line, `invalid .ds fill: ${fillText}`),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { fill };
|
|
80
|
+
}
|
|
81
|
+
function parseTypeSizeExpression(text) {
|
|
82
|
+
const typeExpr = parseTypeExpr(text);
|
|
83
|
+
return typeExpr ? { kind: 'type-size', typeExpr } : undefined;
|
|
84
|
+
}
|
|
85
|
+
function splitValueList(text) {
|
|
86
|
+
const values = [];
|
|
87
|
+
let state = { quote: undefined, escaped: false, parenDepth: 0 };
|
|
88
|
+
let start = 0;
|
|
89
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
90
|
+
if (isValueSeparator(text[index] ?? '', state)) {
|
|
91
|
+
values.push(text.slice(start, index));
|
|
92
|
+
start = index + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
state = scanValueListChar(text[index] ?? '', state);
|
|
96
|
+
}
|
|
97
|
+
values.push(text.slice(start));
|
|
98
|
+
return values;
|
|
99
|
+
}
|
|
100
|
+
function isValueSeparator(char, state) {
|
|
101
|
+
return char === ',' && state.quote === undefined && state.parenDepth === 0;
|
|
102
|
+
}
|
|
103
|
+
function scanValueListChar(char, state) {
|
|
104
|
+
const escapedState = scanEscapedValueListChar(char, state);
|
|
105
|
+
if (escapedState)
|
|
106
|
+
return escapedState;
|
|
107
|
+
const quotedState = scanQuotedValueListChar(char, state);
|
|
108
|
+
if (quotedState)
|
|
109
|
+
return quotedState;
|
|
110
|
+
return scanParenthesizedValueListChar(char, state);
|
|
111
|
+
}
|
|
112
|
+
function scanEscapedValueListChar(char, state) {
|
|
113
|
+
if (state.escaped)
|
|
114
|
+
return { ...state, escaped: false };
|
|
115
|
+
if (char === '\\' && state.quote !== undefined)
|
|
116
|
+
return { ...state, escaped: true };
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function scanQuotedValueListChar(char, state) {
|
|
120
|
+
if (char !== '"' && char !== "'")
|
|
121
|
+
return undefined;
|
|
122
|
+
return { ...state, quote: state.quote === char ? undefined : (state.quote ?? char) };
|
|
123
|
+
}
|
|
124
|
+
function scanParenthesizedValueListChar(char, state) {
|
|
125
|
+
if (state.quote !== undefined)
|
|
126
|
+
return state;
|
|
127
|
+
if (char === '(')
|
|
128
|
+
return { ...state, parenDepth: state.parenDepth + 1 };
|
|
129
|
+
if (char === ')')
|
|
130
|
+
return { ...state, parenDepth: Math.max(0, state.parenDepth - 1) };
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
133
|
+
function parseQuotedString(text) {
|
|
134
|
+
const input = text.trim();
|
|
135
|
+
if (input[0] !== '"' || input[input.length - 1] !== '"') {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return parseWholeQuotedString(input);
|
|
139
|
+
}
|
|
140
|
+
function parseDataValue(text) {
|
|
141
|
+
const expression = parseExpression(text);
|
|
142
|
+
if (expression) {
|
|
143
|
+
return expression;
|
|
144
|
+
}
|
|
145
|
+
const value = parseWholeQuotedString(text);
|
|
146
|
+
return value === undefined ? undefined : { kind: 'string-fragment', value };
|
|
147
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LogicalLine } from '../source/logical-lines.js';
|
|
2
|
+
import type { ParseLineResult } from './parse-line.js';
|
|
3
|
+
export declare function parseColonDeclaration(line: LogicalLine, name: string, statementText: string, span: {
|
|
4
|
+
readonly sourceName: string;
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly column: number;
|
|
7
|
+
}): ParseLineResult | undefined;
|
|
8
|
+
export declare function parseEquItem(line: LogicalLine, name: string, expressionText: string, span: {
|
|
9
|
+
readonly sourceName: string;
|
|
10
|
+
readonly line: number;
|
|
11
|
+
readonly column: number;
|
|
12
|
+
}): ParseLineResult;
|
|
13
|
+
export declare function parseEnumItem(line: LogicalLine, name: string, membersText: string, span: {
|
|
14
|
+
readonly sourceName: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
readonly column: number;
|
|
17
|
+
}): ParseLineResult;
|
|
18
|
+
export declare function parseWholeQuotedString(text: string): string | undefined;
|