@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.
Files changed (41) hide show
  1. package/README.md +14 -0
  2. package/dist/src/assembly/import-visibility.js +108 -33
  3. package/dist/src/core/compile.js +98 -1
  4. package/dist/src/expansion/op-expand-selected.js +8 -1
  5. package/dist/src/expansion/op-expansion.d.ts +3 -0
  6. package/dist/src/expansion/op-expansion.js +47 -9
  7. package/dist/src/node/source-host.js +5 -2
  8. package/dist/src/outputs/d8-files.js +1 -0
  9. package/dist/src/outputs/types.d.ts +1 -0
  10. package/dist/src/source/instruction-chain.d.ts +5 -0
  11. package/dist/src/source/instruction-chain.js +75 -0
  12. package/dist/src/source/logical-lines.d.ts +1 -0
  13. package/dist/src/source/source-span.d.ts +1 -0
  14. package/dist/src/syntax/names.d.ts +18 -0
  15. package/dist/src/syntax/names.js +44 -0
  16. package/dist/src/syntax/parse-data-directives.d.ts +17 -0
  17. package/dist/src/syntax/parse-data-directives.js +147 -0
  18. package/dist/src/syntax/parse-declaration-directives.d.ts +18 -0
  19. package/dist/src/syntax/parse-declaration-directives.js +90 -0
  20. package/dist/src/syntax/parse-diagnostics.d.ts +8 -0
  21. package/dist/src/syntax/parse-diagnostics.js +15 -0
  22. package/dist/src/syntax/parse-directive-statement.d.ts +1 -5
  23. package/dist/src/syntax/parse-directive-statement.js +19 -259
  24. package/dist/src/syntax/parse-instruction-chain.d.ts +22 -0
  25. package/dist/src/syntax/parse-instruction-chain.js +62 -0
  26. package/dist/src/syntax/parse-layout-declarations.js +10 -18
  27. package/dist/src/syntax/parse-layout-expression.js +4 -3
  28. package/dist/src/syntax/parse-line.js +21 -31
  29. package/dist/src/syntax/parse-location-directives.d.ts +7 -0
  30. package/dist/src/syntax/parse-location-directives.js +15 -0
  31. package/dist/src/syntax/statement-classification.d.ts +2 -0
  32. package/dist/src/syntax/statement-classification.js +24 -0
  33. package/dist/src/tooling/case-style.js +42 -26
  34. package/docs/codebase/02-source-loading-and-parsing.md +28 -8
  35. package/docs/codebase/04-ops-and-register-contracts.md +24 -3
  36. package/docs/codebase/05-interfaces-and-output-artifacts.md +10 -0
  37. package/docs/codebase/06-verification-and-maintenance.md +9 -3
  38. package/docs/codebase/appendices/a-directory-file-reference.md +17 -10
  39. package/docs/codebase/appendices/b-compile-flow-reference.md +3 -2
  40. package/docs/codebase/index.md +4 -0
  41. 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 labels = collectLabelVisibility(items);
3
+ const symbols = collectSymbolVisibility(items);
4
4
  for (const item of items) {
5
- validateItemReferences(item, labels, diagnostics);
5
+ validateItemReferences(item, symbols, diagnostics);
6
6
  }
7
7
  }
8
- function collectLabelVisibility(items) {
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.sourceRelation === 'import' && item.span.sourceUnit !== undefined) {
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, labels, diagnostics) {
106
+ function validateItemReferences(item, symbols, diagnostics) {
38
107
  switch (item.kind) {
39
108
  case 'org':
40
- validateExpression(item.expression, item.span, labels, diagnostics);
109
+ validateExpression(item.expression, item.span, symbols, diagnostics);
41
110
  return;
42
111
  case 'equ':
43
- validateExpression(item.expression, item.span, labels, diagnostics);
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, labels, diagnostics);
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, labels, diagnostics);
121
+ validateExpression(value, item.span, symbols, diagnostics);
53
122
  }
54
123
  return;
55
124
  case 'ds':
56
- validateExpression(item.size, item.span, labels, diagnostics);
125
+ validateExpression(item.size, item.span, symbols, diagnostics);
57
126
  if (item.fill !== undefined) {
58
- validateExpression(item.fill, item.span, labels, diagnostics);
127
+ validateExpression(item.fill, item.span, symbols, diagnostics);
59
128
  }
60
129
  return;
61
130
  case 'align':
62
- validateExpression(item.alignment, item.span, labels, diagnostics);
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, labels, diagnostics);
135
+ validateExpression(item.expression, item.span, symbols, diagnostics);
67
136
  return;
68
137
  case 'instruction':
69
- validateInstruction(item.instruction, item.span, labels, diagnostics);
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, labels, diagnostics) {
150
+ function validateDataValue(value, span, symbols, diagnostics) {
82
151
  if ('kind' in value && value.kind === 'string-fragment')
83
152
  return;
84
- validateExpression(value, span, labels, diagnostics);
153
+ validateExpression(value, span, symbols, diagnostics);
85
154
  }
86
- function validateInstruction(instruction, span, labels, diagnostics) {
155
+ function validateInstruction(instruction, span, symbols, diagnostics) {
87
156
  for (const expression of instructionExpressions(instruction)) {
88
- validateExpression(expression, span, labels, diagnostics);
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, labels, diagnostics) {
226
+ function validateExpression(expression, span, symbols, diagnostics) {
158
227
  switch (expression.kind) {
159
228
  case 'symbol':
160
- validateSymbolReference(expression.name, span, labels, diagnostics);
229
+ validateSymbolReference(expression.name, span, symbols, diagnostics);
161
230
  return;
162
231
  case 'byte-function':
163
232
  case 'unary':
164
- validateExpression(expression.expression, span, labels, diagnostics);
233
+ validateExpression(expression.expression, span, symbols, diagnostics);
165
234
  return;
166
235
  case 'binary':
167
- validateExpression(expression.left, span, labels, diagnostics);
168
- validateExpression(expression.right, span, labels, diagnostics);
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, labels, diagnostics);
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, labels, diagnostics);
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, labels, diagnostics) {
187
- const label = lookupLabel(labels, name);
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(labels, name) {
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
  }
@@ -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: { sourceName: line.sourceName, line: line.line, column: firstColumn(line.text) },
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 = /^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) {
@@ -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) {
@@ -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
+ }