@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.
Files changed (40) hide show
  1. package/README.md +21 -9
  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/register-contracts/report.js +15 -3
  7. package/dist/src/register-contracts/smartCommentParsing.d.ts +1 -0
  8. package/dist/src/register-contracts/smartCommentParsing.js +42 -7
  9. package/dist/src/register-contracts/smartComments.d.ts +2 -2
  10. package/dist/src/register-contracts/smartComments.js +3 -4
  11. package/dist/src/source/instruction-chain.d.ts +5 -0
  12. package/dist/src/source/instruction-chain.js +75 -0
  13. package/dist/src/syntax/names.d.ts +18 -0
  14. package/dist/src/syntax/names.js +44 -0
  15. package/dist/src/syntax/parse-data-directives.d.ts +17 -0
  16. package/dist/src/syntax/parse-data-directives.js +147 -0
  17. package/dist/src/syntax/parse-declaration-directives.d.ts +18 -0
  18. package/dist/src/syntax/parse-declaration-directives.js +90 -0
  19. package/dist/src/syntax/parse-diagnostics.d.ts +8 -0
  20. package/dist/src/syntax/parse-diagnostics.js +15 -0
  21. package/dist/src/syntax/parse-directive-statement.d.ts +1 -5
  22. package/dist/src/syntax/parse-directive-statement.js +19 -259
  23. package/dist/src/syntax/parse-instruction-chain.d.ts +22 -0
  24. package/dist/src/syntax/parse-instruction-chain.js +62 -0
  25. package/dist/src/syntax/parse-layout-declarations.js +9 -18
  26. package/dist/src/syntax/parse-layout-expression.js +4 -3
  27. package/dist/src/syntax/parse-line.js +20 -31
  28. package/dist/src/syntax/parse-location-directives.d.ts +7 -0
  29. package/dist/src/syntax/parse-location-directives.js +15 -0
  30. package/dist/src/syntax/statement-classification.d.ts +2 -0
  31. package/dist/src/syntax/statement-classification.js +24 -0
  32. package/dist/src/tooling/case-style.js +42 -26
  33. package/docs/codebase/02-source-loading-and-parsing.md +28 -8
  34. package/docs/codebase/04-ops-and-register-contracts.md +24 -3
  35. package/docs/codebase/05-interfaces-and-output-artifacts.md +10 -0
  36. package/docs/codebase/06-verification-and-maintenance.md +9 -3
  37. package/docs/codebase/appendices/a-directory-file-reference.md +17 -10
  38. package/docs/codebase/appendices/b-compile-flow-reference.md +3 -2
  39. package/docs/codebase/index.md +4 -0
  40. 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
- ;! in A
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. `clobbers B` means the routine may
427
- change `B`. `preserves B` means the value that enters in `B` is still present
428
- when the routine returns.
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
 
@@ -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
  }
@@ -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) && !FLAG_UNITS.has(unit));
71
+ const clobbers = summary.mayWrite.filter((unit) => !relationOut.has(unit));
63
72
  if (clobbers.length > 0)
64
- out.push({ keyword: 'clobbers', carriers: contractCarrierList(clobbers) });
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
- return sourceContractEntries(summary).map((entry) => `;! ${entry.keyword.padEnd(10)}${entry.carriers}`);
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 (!match)
44
- return undefined;
45
- const tag = match[1].toLowerCase();
46
- if (!CONTRACT_COMMENT_KINDS.has(tag))
47
- return undefined;
48
- return parseCarrierComment(tag, match[2]?.trim());
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 = parseSmartCommentLine(`;${text}`);
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,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;