@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.
Files changed (35) hide show
  1. package/README.md +14 -0
  2. package/dist/src/core/compile.js +97 -1
  3. package/dist/src/expansion/op-expansion.js +47 -9
  4. package/dist/src/outputs/d8-files.js +1 -0
  5. package/dist/src/outputs/types.d.ts +1 -0
  6. package/dist/src/source/instruction-chain.d.ts +5 -0
  7. package/dist/src/source/instruction-chain.js +75 -0
  8. package/dist/src/syntax/names.d.ts +18 -0
  9. package/dist/src/syntax/names.js +44 -0
  10. package/dist/src/syntax/parse-data-directives.d.ts +17 -0
  11. package/dist/src/syntax/parse-data-directives.js +147 -0
  12. package/dist/src/syntax/parse-declaration-directives.d.ts +18 -0
  13. package/dist/src/syntax/parse-declaration-directives.js +90 -0
  14. package/dist/src/syntax/parse-diagnostics.d.ts +8 -0
  15. package/dist/src/syntax/parse-diagnostics.js +15 -0
  16. package/dist/src/syntax/parse-directive-statement.d.ts +1 -5
  17. package/dist/src/syntax/parse-directive-statement.js +19 -259
  18. package/dist/src/syntax/parse-instruction-chain.d.ts +22 -0
  19. package/dist/src/syntax/parse-instruction-chain.js +62 -0
  20. package/dist/src/syntax/parse-layout-declarations.js +9 -18
  21. package/dist/src/syntax/parse-layout-expression.js +4 -3
  22. package/dist/src/syntax/parse-line.js +20 -31
  23. package/dist/src/syntax/parse-location-directives.d.ts +7 -0
  24. package/dist/src/syntax/parse-location-directives.js +15 -0
  25. package/dist/src/syntax/statement-classification.d.ts +2 -0
  26. package/dist/src/syntax/statement-classification.js +24 -0
  27. package/dist/src/tooling/case-style.js +42 -26
  28. package/docs/codebase/02-source-loading-and-parsing.md +28 -8
  29. package/docs/codebase/04-ops-and-register-contracts.md +24 -3
  30. package/docs/codebase/05-interfaces-and-output-artifacts.md +10 -0
  31. package/docs/codebase/06-verification-and-maintenance.md +9 -3
  32. package/docs/codebase/appendices/a-directory-file-reference.md +17 -10
  33. package/docs/codebase/appendices/b-compile-flow-reference.md +3 -2
  34. package/docs/codebase/index.md +4 -0
  35. 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:
@@ -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 = /^op\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*$/i.exec(stripLineComment(line.text).trim());
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 template = parseOpBodyTemplate(bodyLine, paramNames, diagnostics, parseOptions);
39
- if (template)
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 = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(.+))?$/.exec(text);
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 = /^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z][A-Za-z0-9_]*)$/.exec(part.trim());
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 = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(.+))?$/.exec(text);
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 = /^\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)$/.exec(trimmed);
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: firstColumn(line.text),
216
+ column,
179
217
  };
180
218
  }
181
219
  function firstColumn(text) {
@@ -53,6 +53,7 @@ function addSourceSegments(entries, sourceSegments) {
53
53
  start: segment.start,
54
54
  end: segment.end,
55
55
  line: segment.line,
56
+ column: segment.column,
56
57
  lstLine: segment.line,
57
58
  kind: segment.kind,
58
59
  confidence: segment.confidence,
@@ -100,6 +100,7 @@ export interface D8mSegment {
100
100
  end: number;
101
101
  lstLine: number;
102
102
  line?: number;
103
+ column?: number;
103
104
  kind: D8mSegmentKind;
104
105
  confidence: D8mSegmentConfidence;
105
106
  }
@@ -0,0 +1,5 @@
1
+ export interface InstructionChainSegment {
2
+ readonly text: string;
3
+ readonly column: number;
4
+ }
5
+ export declare function splitInstructionChain(text: string): readonly InstructionChainSegment[] | undefined;
@@ -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;