@jhlagado/azm 0.2.10 → 0.2.12
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/assembly/import-visibility.js +108 -33
- package/dist/src/core/compile.js +98 -1
- package/dist/src/expansion/op-expand-selected.js +8 -1
- package/dist/src/expansion/op-expansion.d.ts +3 -0
- package/dist/src/expansion/op-expansion.js +47 -9
- package/dist/src/node/source-host.js +5 -2
- 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/source/logical-lines.d.ts +1 -0
- package/dist/src/source/source-span.d.ts +1 -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 +10 -18
- package/dist/src/syntax/parse-layout-expression.js +4 -3
- package/dist/src/syntax/parse-line.js +21 -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:
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { diagnostic } from '../semantics/diagnostics.js';
|
|
2
2
|
export function validateImportVisibility(items, diagnostics) {
|
|
3
|
-
const
|
|
3
|
+
const symbols = collectSymbolVisibility(items);
|
|
4
4
|
for (const item of items) {
|
|
5
|
-
validateItemReferences(item,
|
|
5
|
+
validateItemReferences(item, symbols, diagnostics);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
-
function
|
|
8
|
+
function collectSymbolVisibility(items) {
|
|
9
9
|
const labels = new Map();
|
|
10
|
+
const exactSymbols = new Set();
|
|
10
11
|
const importedSourceUnits = importedUnitNames(items);
|
|
12
|
+
const symbolConflicts = buildSymbolConflictIndex(items);
|
|
11
13
|
for (const item of items) {
|
|
14
|
+
for (const name of exactSymbolNames(item)) {
|
|
15
|
+
exactSymbols.add(name);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
19
|
+
const item = items[index];
|
|
12
20
|
if (item.kind !== 'label')
|
|
13
21
|
continue;
|
|
14
22
|
labels.set(item.name, {
|
|
@@ -16,14 +24,75 @@ function collectLabelVisibility(items) {
|
|
|
16
24
|
definingSourceUnit: item.span.sourceUnit,
|
|
17
25
|
definingSourceName: item.span.sourceName,
|
|
18
26
|
public: isPublicLabel(item, importedSourceUnits),
|
|
27
|
+
duplicateName: hasAddressPlanningNameConflict(item.name, symbolConflicts, items, index),
|
|
19
28
|
});
|
|
20
29
|
}
|
|
21
|
-
return labels;
|
|
30
|
+
return { labels, exactSymbols };
|
|
31
|
+
}
|
|
32
|
+
function buildSymbolConflictIndex(items) {
|
|
33
|
+
const exact = new Map();
|
|
34
|
+
const declarationLower = new Map();
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
for (const name of exactSymbolNames(item)) {
|
|
37
|
+
exact.set(name, (exact.get(name) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
const declarationName = caseInsensitiveDeclarationName(item);
|
|
40
|
+
if (declarationName !== undefined) {
|
|
41
|
+
const key = declarationName.toLowerCase();
|
|
42
|
+
declarationLower.set(key, (declarationLower.get(key) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { exact, declarationLower };
|
|
46
|
+
}
|
|
47
|
+
function hasAddressPlanningNameConflict(labelName, conflicts, items, labelIndex) {
|
|
48
|
+
return ((conflicts.exact.get(labelName) ?? 0) > 1 ||
|
|
49
|
+
(conflicts.declarationLower.get(labelName.toLowerCase()) ?? 0) > 0 ||
|
|
50
|
+
hasReportedEnumMemberConflict(labelName, items, labelIndex));
|
|
51
|
+
}
|
|
52
|
+
function hasReportedEnumMemberConflict(labelName, items, labelIndex) {
|
|
53
|
+
const lowerName = labelName.toLowerCase();
|
|
54
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
55
|
+
const item = items[index];
|
|
56
|
+
if (item.kind !== 'enum')
|
|
57
|
+
continue;
|
|
58
|
+
for (const memberName of qualifiedEnumMemberNames(item)) {
|
|
59
|
+
if (memberName === labelName)
|
|
60
|
+
return true;
|
|
61
|
+
if (index > labelIndex && memberName.toLowerCase() === lowerName)
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
function exactSymbolNames(item) {
|
|
68
|
+
switch (item.kind) {
|
|
69
|
+
case 'label':
|
|
70
|
+
case 'equ':
|
|
71
|
+
return [item.name];
|
|
72
|
+
case 'enum':
|
|
73
|
+
return qualifiedEnumMemberNames(item);
|
|
74
|
+
default:
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function qualifiedEnumMemberNames(item) {
|
|
79
|
+
return item.kind === 'enum' ? item.members.map((member) => `${item.name}.${member}`) : [];
|
|
80
|
+
}
|
|
81
|
+
function caseInsensitiveDeclarationName(item) {
|
|
82
|
+
switch (item.kind) {
|
|
83
|
+
case 'enum':
|
|
84
|
+
case 'type':
|
|
85
|
+
case 'type-alias':
|
|
86
|
+
return item.name;
|
|
87
|
+
default:
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
22
90
|
}
|
|
23
91
|
function importedUnitNames(items) {
|
|
24
92
|
const units = new Set();
|
|
25
93
|
for (const item of items) {
|
|
26
|
-
if (item.span.
|
|
94
|
+
if (item.span.sourceUnitRelation === 'import' &&
|
|
95
|
+
item.span.sourceUnit !== undefined) {
|
|
27
96
|
units.add(item.span.sourceUnit);
|
|
28
97
|
}
|
|
29
98
|
}
|
|
@@ -34,39 +103,39 @@ function isPublicLabel(item, importedSourceUnits) {
|
|
|
34
103
|
item.span.sourceUnit === undefined ||
|
|
35
104
|
!importedSourceUnits.has(item.span.sourceUnit));
|
|
36
105
|
}
|
|
37
|
-
function validateItemReferences(item,
|
|
106
|
+
function validateItemReferences(item, symbols, diagnostics) {
|
|
38
107
|
switch (item.kind) {
|
|
39
108
|
case 'org':
|
|
40
|
-
validateExpression(item.expression, item.span,
|
|
109
|
+
validateExpression(item.expression, item.span, symbols, diagnostics);
|
|
41
110
|
return;
|
|
42
111
|
case 'equ':
|
|
43
|
-
validateExpression(item.expression, item.span,
|
|
112
|
+
validateExpression(item.expression, item.span, symbols, diagnostics);
|
|
44
113
|
return;
|
|
45
114
|
case 'db':
|
|
46
115
|
for (const value of item.values) {
|
|
47
|
-
validateDataValue(value, item.span,
|
|
116
|
+
validateDataValue(value, item.span, symbols, diagnostics);
|
|
48
117
|
}
|
|
49
118
|
return;
|
|
50
119
|
case 'dw':
|
|
51
120
|
for (const value of item.values) {
|
|
52
|
-
validateExpression(value, item.span,
|
|
121
|
+
validateExpression(value, item.span, symbols, diagnostics);
|
|
53
122
|
}
|
|
54
123
|
return;
|
|
55
124
|
case 'ds':
|
|
56
|
-
validateExpression(item.size, item.span,
|
|
125
|
+
validateExpression(item.size, item.span, symbols, diagnostics);
|
|
57
126
|
if (item.fill !== undefined) {
|
|
58
|
-
validateExpression(item.fill, item.span,
|
|
127
|
+
validateExpression(item.fill, item.span, symbols, diagnostics);
|
|
59
128
|
}
|
|
60
129
|
return;
|
|
61
130
|
case 'align':
|
|
62
|
-
validateExpression(item.alignment, item.span,
|
|
131
|
+
validateExpression(item.alignment, item.span, symbols, diagnostics);
|
|
63
132
|
return;
|
|
64
133
|
case 'binfrom':
|
|
65
134
|
case 'binto':
|
|
66
|
-
validateExpression(item.expression, item.span,
|
|
135
|
+
validateExpression(item.expression, item.span, symbols, diagnostics);
|
|
67
136
|
return;
|
|
68
137
|
case 'instruction':
|
|
69
|
-
validateInstruction(item.instruction, item.span,
|
|
138
|
+
validateInstruction(item.instruction, item.span, symbols, diagnostics);
|
|
70
139
|
return;
|
|
71
140
|
case 'label':
|
|
72
141
|
case 'comment':
|
|
@@ -78,14 +147,14 @@ function validateItemReferences(item, labels, diagnostics) {
|
|
|
78
147
|
return;
|
|
79
148
|
}
|
|
80
149
|
}
|
|
81
|
-
function validateDataValue(value, span,
|
|
150
|
+
function validateDataValue(value, span, symbols, diagnostics) {
|
|
82
151
|
if ('kind' in value && value.kind === 'string-fragment')
|
|
83
152
|
return;
|
|
84
|
-
validateExpression(value, span,
|
|
153
|
+
validateExpression(value, span, symbols, diagnostics);
|
|
85
154
|
}
|
|
86
|
-
function validateInstruction(instruction, span,
|
|
155
|
+
function validateInstruction(instruction, span, symbols, diagnostics) {
|
|
87
156
|
for (const expression of instructionExpressions(instruction)) {
|
|
88
|
-
validateExpression(expression, span,
|
|
157
|
+
validateExpression(expression, span, symbols, diagnostics);
|
|
89
158
|
}
|
|
90
159
|
}
|
|
91
160
|
function instructionExpressions(instruction) {
|
|
@@ -154,49 +223,55 @@ function operandExpressions(operand) {
|
|
|
154
223
|
return [];
|
|
155
224
|
}
|
|
156
225
|
}
|
|
157
|
-
function validateExpression(expression, span,
|
|
226
|
+
function validateExpression(expression, span, symbols, diagnostics) {
|
|
158
227
|
switch (expression.kind) {
|
|
159
228
|
case 'symbol':
|
|
160
|
-
validateSymbolReference(expression.name, span,
|
|
229
|
+
validateSymbolReference(expression.name, span, symbols, diagnostics);
|
|
161
230
|
return;
|
|
162
231
|
case 'byte-function':
|
|
163
232
|
case 'unary':
|
|
164
|
-
validateExpression(expression.expression, span,
|
|
233
|
+
validateExpression(expression.expression, span, symbols, diagnostics);
|
|
165
234
|
return;
|
|
166
235
|
case 'binary':
|
|
167
|
-
validateExpression(expression.left, span,
|
|
168
|
-
validateExpression(expression.right, span,
|
|
236
|
+
validateExpression(expression.left, span, symbols, diagnostics);
|
|
237
|
+
validateExpression(expression.right, span, symbols, diagnostics);
|
|
169
238
|
return;
|
|
170
239
|
case 'layout-cast':
|
|
171
|
-
validateExpression(expression.base, span,
|
|
240
|
+
validateExpression(expression.base, span, symbols, diagnostics);
|
|
172
241
|
for (const part of expression.path) {
|
|
173
242
|
if (part.kind === 'index') {
|
|
174
|
-
validateExpression(part.expression, span,
|
|
243
|
+
validateExpression(part.expression, span, symbols, diagnostics);
|
|
175
244
|
}
|
|
176
245
|
}
|
|
177
246
|
return;
|
|
178
247
|
case 'number':
|
|
179
248
|
case 'current-location':
|
|
180
|
-
case 'type-size':
|
|
181
249
|
case 'sizeof':
|
|
182
250
|
case 'offset':
|
|
183
251
|
return;
|
|
252
|
+
case 'type-size':
|
|
253
|
+
if (expression.typeExpr.length === undefined) {
|
|
254
|
+
validateSymbolReference(expression.typeExpr.name, span, symbols, diagnostics);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
184
257
|
}
|
|
185
258
|
}
|
|
186
|
-
function validateSymbolReference(name, referenceSpan,
|
|
187
|
-
const label = lookupLabel(
|
|
188
|
-
if (!label || label.public)
|
|
259
|
+
function validateSymbolReference(name, referenceSpan, symbols, diagnostics) {
|
|
260
|
+
const label = lookupLabel(symbols, name);
|
|
261
|
+
if (!label || label.duplicateName || label.public)
|
|
189
262
|
return;
|
|
190
263
|
if (referenceSpan.sourceUnit === label.definingSourceUnit)
|
|
191
264
|
return;
|
|
192
265
|
diagnostics.push(diagnostic(referenceSpan, `symbol "${name}" is private to ${label.definingSourceName}; export it with @${label.name} or keep the reference inside that file`));
|
|
193
266
|
}
|
|
194
|
-
function lookupLabel(
|
|
195
|
-
const direct = labels.get(name);
|
|
267
|
+
function lookupLabel(symbols, name) {
|
|
268
|
+
const direct = symbols.labels.get(name);
|
|
196
269
|
if (direct)
|
|
197
270
|
return direct;
|
|
271
|
+
if (symbols.exactSymbols.has(name))
|
|
272
|
+
return undefined;
|
|
198
273
|
const lowerName = name.toLowerCase();
|
|
199
|
-
for (const [key, label] of labels) {
|
|
274
|
+
for (const [key, label] of symbols.labels) {
|
|
200
275
|
if (key.toLowerCase() === lowerName)
|
|
201
276
|
return label;
|
|
202
277
|
}
|
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,98 @@ 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
|
+
...(line.sourceUnitRelation !== undefined ? { sourceUnitRelation: line.sourceUnitRelation } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function firstColumn(text) {
|
|
165
|
+
const match = /\S/.exec(text);
|
|
166
|
+
return match ? match.index + 1 : 1;
|
|
167
|
+
}
|
|
168
|
+
function chainDiagnostic(line, column, message) {
|
|
169
|
+
return {
|
|
170
|
+
severity: 'error',
|
|
171
|
+
code: 'AZMN_PARSE',
|
|
172
|
+
message,
|
|
173
|
+
sourceName: line.sourceName,
|
|
174
|
+
line: line.line,
|
|
175
|
+
column,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
81
178
|
export function compileSource(sourceText, options = {}) {
|
|
82
179
|
const source = createSourceFile(options.entryName ?? '<memory>', sourceText);
|
|
83
180
|
const { diagnostics, items } = parseNextSourceItems(scanLogicalLines(source));
|
|
@@ -60,7 +60,14 @@ function expandTemplateItem(ops, item, bindings, line, diagnostics, overload, ex
|
|
|
60
60
|
}
|
|
61
61
|
function opEmittedSource(line) {
|
|
62
62
|
return {
|
|
63
|
-
span: {
|
|
63
|
+
span: {
|
|
64
|
+
sourceName: line.sourceName,
|
|
65
|
+
line: line.line,
|
|
66
|
+
column: firstColumn(line.text),
|
|
67
|
+
...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
|
|
68
|
+
...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
|
|
69
|
+
...(line.sourceUnitRelation !== undefined ? { sourceUnitRelation: line.sourceUnitRelation } : {}),
|
|
70
|
+
},
|
|
64
71
|
kind: 'macro',
|
|
65
72
|
};
|
|
66
73
|
}
|
|
@@ -6,6 +6,9 @@ export type LogicalLineLike = {
|
|
|
6
6
|
readonly sourceName: string;
|
|
7
7
|
readonly line: number;
|
|
8
8
|
readonly text: string;
|
|
9
|
+
readonly sourceUnit?: string;
|
|
10
|
+
readonly sourceRelation?: 'entry' | 'include' | 'import';
|
|
11
|
+
readonly sourceUnitRelation?: 'entry' | 'include' | 'import';
|
|
9
12
|
};
|
|
10
13
|
interface OpParam {
|
|
11
14
|
readonly name: string;
|
|
@@ -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) {
|
|
@@ -30,6 +30,7 @@ export async function expandSourceForTooling(options) {
|
|
|
30
30
|
sourceStack: [],
|
|
31
31
|
sourceUnit: entryFile,
|
|
32
32
|
sourceRelation: 'entry',
|
|
33
|
+
sourceUnitRelation: 'entry',
|
|
33
34
|
...(options.preloadedText !== undefined ? { preloadedText: options.preloadedText } : {}),
|
|
34
35
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
35
36
|
});
|
|
@@ -60,7 +61,7 @@ async function expandFile(options) {
|
|
|
60
61
|
recordLineComment(options.sourceLineComments, line);
|
|
61
62
|
const directive = parseSourceLoadDirective(line.text);
|
|
62
63
|
if (!directive) {
|
|
63
|
-
output.push(withSourceOwnership(line, options.sourceUnit, options.sourceRelation));
|
|
64
|
+
output.push(withSourceOwnership(line, options.sourceUnit, options.sourceRelation, options.sourceUnitRelation));
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
const result = await resolveSourcePath(sourcePath, directive.path, options.includeDirs);
|
|
@@ -88,6 +89,7 @@ async function expandFile(options) {
|
|
|
88
89
|
sourceStack: [...options.sourceStack, { sourcePath }],
|
|
89
90
|
sourceUnit: directive.kind === 'import' ? result.resolved : options.sourceUnit,
|
|
90
91
|
sourceRelation: directive.kind,
|
|
92
|
+
sourceUnitRelation: directive.kind === 'import' ? 'import' : options.sourceUnitRelation,
|
|
91
93
|
});
|
|
92
94
|
if (included !== undefined) {
|
|
93
95
|
output.push(...included);
|
|
@@ -95,11 +97,12 @@ async function expandFile(options) {
|
|
|
95
97
|
}
|
|
96
98
|
return output;
|
|
97
99
|
}
|
|
98
|
-
function withSourceOwnership(line, sourceUnit, sourceRelation) {
|
|
100
|
+
function withSourceOwnership(line, sourceUnit, sourceRelation, sourceUnitRelation) {
|
|
99
101
|
return {
|
|
100
102
|
...line,
|
|
101
103
|
sourceUnit,
|
|
102
104
|
sourceRelation,
|
|
105
|
+
sourceUnitRelation,
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
function recordLineComment(comments, line) {
|
|
@@ -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
|
+
}
|