@jhlagado/azm 0.2.9 → 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 +21 -9
- 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/register-contracts/report.js +15 -3
- package/dist/src/register-contracts/smartCommentParsing.d.ts +1 -0
- package/dist/src/register-contracts/smartCommentParsing.js +42 -7
- package/dist/src/register-contracts/smartComments.d.ts +2 -2
- package/dist/src/register-contracts/smartComments.js +3 -4
- 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
|
|
|
@@ -120,9 +121,7 @@ Use `@Name:` for callable routine entries. The `@` marks a register contracts
|
|
|
120
121
|
routine boundary; call sites still write the symbol name without `@`:
|
|
121
122
|
|
|
122
123
|
```asm
|
|
123
|
-
;!
|
|
124
|
-
;! out A
|
|
125
|
-
;! clobbers BC
|
|
124
|
+
;! in A; out A; clobbers BC
|
|
126
125
|
@MxMask:
|
|
127
126
|
LD C,A
|
|
128
127
|
OR A
|
|
@@ -187,6 +186,19 @@ indent instructions and standalone directives, and align operands enough to keep
|
|
|
187
186
|
dense assembly readable. Exact tab width is less important than keeping one
|
|
188
187
|
source file internally consistent.
|
|
189
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
|
+
|
|
190
202
|
## Literals
|
|
191
203
|
|
|
192
204
|
AZM accepts the usual Z80 numeric forms:
|
|
@@ -406,9 +418,7 @@ register and stack assumptions visible before they become debugger sessions.
|
|
|
406
418
|
Routine entry labels start with `@`:
|
|
407
419
|
|
|
408
420
|
```asm
|
|
409
|
-
;! in A,HL
|
|
410
|
-
;! out carry
|
|
411
|
-
;! clobbers B
|
|
421
|
+
;! in A,HL; out carry; clobbers B
|
|
412
422
|
@CheckTile:
|
|
413
423
|
ld b,(hl)
|
|
414
424
|
cp b
|
|
@@ -423,9 +433,11 @@ name:
|
|
|
423
433
|
```
|
|
424
434
|
|
|
425
435
|
AZMDoc register contract comments use `;!` and may record inputs, outputs,
|
|
426
|
-
clobbered registers and preserved registers.
|
|
427
|
-
|
|
428
|
-
|
|
436
|
+
clobbered registers and preserved registers. Separate clauses on the same line
|
|
437
|
+
with semicolons. Older one-clause-per-line comments are still accepted, but AZM
|
|
438
|
+
generated annotations use the compact single-line form. `clobbers B` means the
|
|
439
|
+
routine may change `B`. `preserves B` means the value that enters in `B` is
|
|
440
|
+
still present when the routine returns.
|
|
429
441
|
|
|
430
442
|
Run the analysis with:
|
|
431
443
|
|
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) {
|
|
@@ -2,6 +2,7 @@ function list(units) {
|
|
|
2
2
|
return units.length === 0 ? '-' : units.join(',');
|
|
3
3
|
}
|
|
4
4
|
const FLAG_UNITS = new Set(['carry', 'zero', 'sign', 'parity', 'halfCarry']);
|
|
5
|
+
const FLAG_UNIT_LIST = ['carry', 'zero', 'sign', 'parity', 'halfCarry'];
|
|
5
6
|
const CONTRACT_CARRIER_PAIRS = [
|
|
6
7
|
{ label: 'BC', hi: 'B', lo: 'C' },
|
|
7
8
|
{ label: 'DE', hi: 'D', lo: 'E' },
|
|
@@ -32,6 +33,14 @@ export function contractCarrierList(units) {
|
|
|
32
33
|
}
|
|
33
34
|
return parts.length === 0 ? '-' : parts.join(',');
|
|
34
35
|
}
|
|
36
|
+
function sourceContractCarrierList(units) {
|
|
37
|
+
const unique = [...new Set(units)];
|
|
38
|
+
const hasAllFlags = FLAG_UNIT_LIST.every((unit) => unique.includes(unit));
|
|
39
|
+
const compacted = hasAllFlags
|
|
40
|
+
? unique.filter((unit) => !FLAG_UNITS.has(unit)).concat('F')
|
|
41
|
+
: unique;
|
|
42
|
+
return contractCarrierList(compacted);
|
|
43
|
+
}
|
|
35
44
|
function relationOutputUnits(relations) {
|
|
36
45
|
return relations.flatMap((rel) => rel.out);
|
|
37
46
|
}
|
|
@@ -59,9 +68,9 @@ function sourceContractEntries(summary) {
|
|
|
59
68
|
const outputUnits = relationOutputUnits(summary.valueRelations);
|
|
60
69
|
if (outputUnits.length > 0)
|
|
61
70
|
out.push({ keyword: 'out', carriers: contractCarrierList(outputUnits) });
|
|
62
|
-
const clobbers = summary.mayWrite.filter((unit) => !relationOut.has(unit)
|
|
71
|
+
const clobbers = summary.mayWrite.filter((unit) => !relationOut.has(unit));
|
|
63
72
|
if (clobbers.length > 0)
|
|
64
|
-
out.push({ keyword: 'clobbers', carriers:
|
|
73
|
+
out.push({ keyword: 'clobbers', carriers: sourceContractCarrierList(clobbers) });
|
|
65
74
|
return out;
|
|
66
75
|
}
|
|
67
76
|
function stackStatus(summary) {
|
|
@@ -152,5 +161,8 @@ export function renderRegisterContractsInterface(summaries) {
|
|
|
152
161
|
return `${lines.join('\n')}\n`;
|
|
153
162
|
}
|
|
154
163
|
export function renderRegisterContractsSourceBlock(summary) {
|
|
155
|
-
|
|
164
|
+
const entries = sourceContractEntries(summary);
|
|
165
|
+
if (entries.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
return [`;! ${entries.map((entry) => `${entry.keyword} ${entry.carriers}`).join('; ')}`];
|
|
156
168
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { SmartComment } from './types.js';
|
|
2
2
|
export declare function parseSmartCommentLine(line: string): SmartComment | undefined;
|
|
3
|
+
export declare function parseSmartCommentLines(line: string): SmartComment[];
|
|
3
4
|
export declare function isCompactSourceCommentLine(line: string): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expandCarrierList } from './carriers.js';
|
|
2
2
|
const COMPACT_SOURCE_TAG_RE = /^;?\s*!\s*(in|out|clobbers|preserves)(?:\s+(.+))?$/i;
|
|
3
|
+
const COMPACT_SOURCE_CLAUSE_RE = /^(in|out|clobbers|preserves)(?:\s+(.+))?$/i;
|
|
3
4
|
const COMPACT_SOURCE_LINE_RE = /^\s*;\s*!\s*(?:in|out|maybe-out|clobbers|preserves)(?:\s|$)/i;
|
|
4
5
|
const CARRIER_RE = /^\{([^}]+)\}(?:\s+(.+))?$/;
|
|
5
6
|
const CONTRACT_COMMENT_KINDS = new Set(['in', 'out', 'clobbers', 'preserves']);
|
|
@@ -35,17 +36,51 @@ function parseCarrierPayload(rest) {
|
|
|
35
36
|
return { carriers, ...(name ? { name } : {}) };
|
|
36
37
|
}
|
|
37
38
|
export function parseSmartCommentLine(line) {
|
|
39
|
+
return parseSmartCommentLines(line)[0];
|
|
40
|
+
}
|
|
41
|
+
export function parseSmartCommentLines(line) {
|
|
38
42
|
const trimmed = line.trim();
|
|
39
43
|
const expectOut = parseExpectOutComment(trimmed);
|
|
40
44
|
if (expectOut !== undefined)
|
|
41
|
-
return expectOut;
|
|
45
|
+
return [expectOut];
|
|
46
|
+
const semicolonSeparated = parseSemicolonSeparatedSourceComments(trimmed);
|
|
47
|
+
if (semicolonSeparated.length > 0)
|
|
48
|
+
return semicolonSeparated;
|
|
42
49
|
const match = COMPACT_SOURCE_TAG_RE.exec(trimmed);
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
if (match) {
|
|
51
|
+
const tag = match[1].toLowerCase();
|
|
52
|
+
if (!CONTRACT_COMMENT_KINDS.has(tag))
|
|
53
|
+
return [];
|
|
54
|
+
const comment = parseCarrierComment(tag, match[2]?.trim());
|
|
55
|
+
return comment === undefined ? [] : [comment];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
function parseSemicolonSeparatedSourceComments(trimmed) {
|
|
60
|
+
const sourcePrefix = /^;?\s*!\s*/.exec(trimmed);
|
|
61
|
+
if (sourcePrefix === null)
|
|
62
|
+
return [];
|
|
63
|
+
const content = trimmed.slice(sourcePrefix[0].length);
|
|
64
|
+
const parts = content
|
|
65
|
+
.split(';')
|
|
66
|
+
.map((part) => part.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
if (parts.length <= 1)
|
|
69
|
+
return [];
|
|
70
|
+
const comments = [];
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
const match = COMPACT_SOURCE_CLAUSE_RE.exec(part);
|
|
73
|
+
if (!match)
|
|
74
|
+
return [];
|
|
75
|
+
const tag = match[1].toLowerCase();
|
|
76
|
+
if (!CONTRACT_COMMENT_KINDS.has(tag))
|
|
77
|
+
return [];
|
|
78
|
+
const comment = parseCarrierComment(tag, match[2]?.trim());
|
|
79
|
+
if (comment === undefined)
|
|
80
|
+
return [];
|
|
81
|
+
comments.push(comment);
|
|
82
|
+
}
|
|
83
|
+
return comments;
|
|
49
84
|
}
|
|
50
85
|
function parseExpectOutComment(trimmed) {
|
|
51
86
|
const expectOut = /^;?\s*expects\s+out\s+(.+)$/i.exec(trimmed);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LocatedSmartComment, RegisterContractsRoutine, RoutineContract } from './types.js';
|
|
2
|
-
import { parseSmartCommentLine } from './smartCommentParsing.js';
|
|
3
|
-
export { parseSmartCommentLine };
|
|
2
|
+
import { parseSmartCommentLine, parseSmartCommentLines } from './smartCommentParsing.js';
|
|
3
|
+
export { parseSmartCommentLine, parseSmartCommentLines };
|
|
4
4
|
export declare function parseSmartComments(sourceLineComments: ReadonlyMap<string, ReadonlyMap<number, string>>): LocatedSmartComment[];
|
|
5
5
|
export declare function buildRoutineContracts(comments: LocatedSmartComment[], routines?: RegisterContractsRoutine[], sourceTexts?: ReadonlyMap<string, string>): Map<string, RoutineContract>;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { collectPrecedingCommentBlock } from './smartCommentBlocks.js';
|
|
2
|
-
import { parseSmartCommentLine } from './smartCommentParsing.js';
|
|
3
|
-
export { parseSmartCommentLine };
|
|
2
|
+
import { parseSmartCommentLine, parseSmartCommentLines } from './smartCommentParsing.js';
|
|
3
|
+
export { parseSmartCommentLine, parseSmartCommentLines };
|
|
4
4
|
export function parseSmartComments(sourceLineComments) {
|
|
5
5
|
const out = [];
|
|
6
6
|
for (const [file, comments] of sourceLineComments) {
|
|
7
7
|
for (const [line, text] of comments) {
|
|
8
|
-
const parsed
|
|
9
|
-
if (parsed) {
|
|
8
|
+
for (const parsed of parseSmartCommentLines(`;${text}`)) {
|
|
10
9
|
out.push({ file, line, comment: parsed });
|
|
11
10
|
}
|
|
12
11
|
}
|
|
@@ -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;
|