@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
@@ -0,0 +1,62 @@
1
+ import { splitInstructionChain } from '../source/instruction-chain.js';
2
+ import { hasLeadingLabel, parseLeadingLabel } from './names.js';
3
+ import { isChainedDirectiveOrDeclaration } from './statement-classification.js';
4
+ export function parseInstructionChain(options) {
5
+ const segments = splitInstructionChain(options.line.text);
6
+ if (segments === undefined)
7
+ return undefined;
8
+ const items = [];
9
+ const diagnostics = [];
10
+ for (let index = 0; index < segments.length; index += 1) {
11
+ const segment = segments[index];
12
+ const parsed = parseInstructionChainSegment(options, segment.text, segment.column, index);
13
+ diagnostics.push(...parsed.diagnostics);
14
+ items.push(...parsed.items);
15
+ }
16
+ options.appendLineComment?.(items, options.line);
17
+ return { items, diagnostics };
18
+ }
19
+ function parseInstructionChainSegment(options, text, column, segmentIndex) {
20
+ if (text.length === 0) {
21
+ return {
22
+ items: [],
23
+ diagnostics: [
24
+ options.makeDiagnostic(options.line, column, 'empty instruction segment in chained line'),
25
+ ],
26
+ };
27
+ }
28
+ if (segmentIndex > 0 && hasLeadingLabel(text)) {
29
+ return {
30
+ items: [],
31
+ diagnostics: [
32
+ options.makeDiagnostic(options.line, column, 'labels are only allowed before the first chained instruction'),
33
+ ],
34
+ };
35
+ }
36
+ const labeled = segmentIndex === 0 ? parseLeadingLabel(text, column) : undefined;
37
+ const statementText = labeled?.statementText ?? text;
38
+ const statementColumn = labeled?.statementColumn ?? column;
39
+ if (statementText.length === 0) {
40
+ return {
41
+ items: [],
42
+ diagnostics: [
43
+ options.makeDiagnostic(options.line, statementColumn, 'empty instruction segment in chained line'),
44
+ ],
45
+ };
46
+ }
47
+ if (isChainedDirectiveOrDeclaration(statementText)) {
48
+ return {
49
+ items: [],
50
+ diagnostics: [
51
+ options.makeDiagnostic(options.line, statementColumn, 'directives must be on their own line; chained lines only support instructions and ops'),
52
+ ],
53
+ };
54
+ }
55
+ const items = [];
56
+ if (labeled) {
57
+ items.push(options.makeLabelItem(labeled, options.line));
58
+ }
59
+ const statement = options.parseStatement(options.line, statementText, statementColumn);
60
+ items.push(...statement.items);
61
+ return { items, diagnostics: statement.diagnostics };
62
+ }
@@ -1,4 +1,6 @@
1
1
  import { stripLineComment } from '../source/strip-line-comment.js';
2
+ import { IDENTIFIER_PATTERN } from './names.js';
3
+ import { firstNonWhitespaceColumn, parseLineError } from './parse-diagnostics.js';
2
4
  import { parseTypeExpr } from './parse-expression.js';
3
5
  export function parseLayoutDeclarationAt(lines, index) {
4
6
  const line = lines[index];
@@ -9,7 +11,7 @@ export function parseLayoutDeclarationAt(lines, index) {
9
11
  if (typeAlias !== undefined) {
10
12
  return { consumedUntilIndex: index, ...typeAlias };
11
13
  }
12
- const prefixLayoutHeader = /^\.(type|union)\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/.exec(text);
14
+ const prefixLayoutHeader = new RegExp(`^\\.(type|union)\\s+(${IDENTIFIER_PATTERN})\\s*$`).exec(text);
13
15
  if (prefixLayoutHeader) {
14
16
  const directive = prefixLayoutHeader[1] ?? 'type';
15
17
  return {
@@ -26,7 +28,7 @@ export function parseLayoutDeclarationAt(lines, index) {
26
28
  return parseLayoutBlock(lines, index, layoutHeader);
27
29
  }
28
30
  function parseTypeAlias(line, text) {
29
- const nameLeftTypeAlias = /^([A-Za-z_][A-Za-z0-9_]*)(?::\s*|\s+)\.typealias\s+(.+)$/.exec(text);
31
+ const nameLeftTypeAlias = new RegExp(`^(${IDENTIFIER_PATTERN})(?::\\s*|\\s+)\\.typealias\\s+(.+)$`).exec(text);
30
32
  if (nameLeftTypeAlias) {
31
33
  const typeExprText = nameLeftTypeAlias[2] ?? '';
32
34
  const typeExpr = parseTypeExpr(typeExprText);
@@ -43,7 +45,7 @@ function parseTypeAlias(line, text) {
43
45
  diagnostics: [],
44
46
  };
45
47
  }
46
- const oldTypeAlias = /^\.type\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/.exec(text);
48
+ const oldTypeAlias = new RegExp(`^\\.type\\s+(${IDENTIFIER_PATTERN})\\s*=\\s*(.+)$`).exec(text);
47
49
  if (oldTypeAlias) {
48
50
  return {
49
51
  diagnostics: [
@@ -54,7 +56,7 @@ function parseTypeAlias(line, text) {
54
56
  return undefined;
55
57
  }
56
58
  function parseNameLeftLayoutHeader(text) {
57
- const match = /^([A-Za-z_][A-Za-z0-9_]*)(?::\s*|\s+)\.(type|union)\s*$/.exec(text);
59
+ const match = new RegExp(`^(${IDENTIFIER_PATTERN})(?::\\s*|\\s+)\\.(type|union)\\s*$`).exec(text);
58
60
  return match
59
61
  ? {
60
62
  directive: match[2] ?? '',
@@ -107,7 +109,7 @@ function spanForLine(line) {
107
109
  return {
108
110
  sourceName: line.sourceName,
109
111
  line: line.line,
110
- column: firstColumn(line.text),
112
+ column: firstNonWhitespaceColumn(line.text),
111
113
  ...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
112
114
  ...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
113
115
  };
@@ -122,7 +124,7 @@ function skipToLayoutEnd(lines, index, directive) {
122
124
  return index;
123
125
  }
124
126
  function parseLayoutField(text) {
125
- const match = /^([A-Za-z_][A-Za-z0-9_]*)\s+(\.(?:field|byte|word|addr))(?:\s+(.+))?$/.exec(text);
127
+ const match = new RegExp(`^(${IDENTIFIER_PATTERN})\\s+(\\.(?:field|byte|word|addr))(?:\\s+(.+))?$`).exec(text);
126
128
  if (!match) {
127
129
  return undefined;
128
130
  }
@@ -174,16 +176,5 @@ function scalarFieldSize(typeName) {
174
176
  }
175
177
  }
176
178
  function parseDiagnostic(line, message) {
177
- return {
178
- severity: 'error',
179
- code: 'AZMN_PARSE',
180
- message,
181
- sourceName: line.sourceName,
182
- line: line.line,
183
- column: firstColumn(line.text),
184
- };
185
- }
186
- function firstColumn(text) {
187
- const match = /\S/.exec(text);
188
- return match ? match.index + 1 : 1;
179
+ return parseLineError(line, message);
189
180
  }
@@ -1,6 +1,7 @@
1
+ import { IDENTIFIER_PATTERN } from './names.js';
1
2
  export function parseTypeExpr(text) {
2
3
  const trimmed = text.trim();
3
- const match = /^([A-Za-z_][A-Za-z0-9_]*)(?:\[\s*([0-9]+)\s*\])?$/.exec(trimmed);
4
+ const match = new RegExp(`^(${IDENTIFIER_PATTERN})(?:\\[\\s*([0-9]+)\\s*\\])?$`).exec(trimmed);
4
5
  if (!match) {
5
6
  return undefined;
6
7
  }
@@ -118,7 +119,7 @@ function parseLayoutCastPathPart(text, parseNestedExpression) {
118
119
  : parseLayoutCastIndex(text, parseNestedExpression);
119
120
  }
120
121
  function parseLayoutCastField(text) {
121
- const field = /^\.([A-Za-z_][A-Za-z0-9_]*)/.exec(text);
122
+ const field = new RegExp(`^\\.(${IDENTIFIER_PATTERN})`).exec(text);
122
123
  return field ? { part: { kind: 'field', name: field[1] ?? '' }, rest: text.slice(field[0].length) } : undefined;
123
124
  }
124
125
  function parseLayoutCastIndex(text, parseNestedExpression) {
@@ -168,7 +169,7 @@ function parseOffsetIndex(text) {
168
169
  : undefined;
169
170
  }
170
171
  function parseOffsetField(text) {
171
- const field = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text);
172
+ const field = new RegExp(`^${IDENTIFIER_PATTERN}`).exec(text);
172
173
  return field
173
174
  ? { part: { kind: 'field', name: field[0] }, rest: text.slice(field[0].length) }
174
175
  : undefined;
@@ -1,5 +1,7 @@
1
1
  import { extractLineComment, stripLineComment } from '../source/strip-line-comment.js';
2
2
  import { normalizeDirectiveAlias } from './directive-aliases.js';
3
+ import { LABEL_NAME_PATTERN, parseEntryLabel } from './names.js';
4
+ import { firstNonWhitespaceColumn, parseLineError } from './parse-diagnostics.js';
3
5
  import { parseColonDeclaration, parseDirectiveStatement } from './parse-directive-statement.js';
4
6
  import { parseZ80Instruction } from '../z80/parse-instruction.js';
5
7
  export function parseLogicalLine(line, options = {}) {
@@ -8,34 +10,38 @@ export function parseLogicalLine(line, options = {}) {
8
10
  return commentOnlyLine(line);
9
11
  }
10
12
  const span = spanForLine(line);
11
- const labelWithStatement = /^(@?[A-Za-z_.$?][A-Za-z0-9_.$?]*):\s*(.+)$/.exec(text);
13
+ const labelWithStatement = new RegExp(`^(@?${LABEL_NAME_PATTERN}):\\s*(.+)$`).exec(text);
12
14
  if (labelWithStatement) {
13
15
  const rawLabel = labelWithStatement[1] ?? '';
14
- const labelName = normalizeEntryLabelName(rawLabel);
15
- const isEntry = rawLabel.startsWith('@');
16
+ const label = parseEntryLabel(rawLabel);
17
+ if (!label)
18
+ return withLineComment(line, parseCanonicalStatement(line, text, span));
16
19
  const statementText = labelWithStatement[2] ?? '';
17
- const declaration = parseColonDeclaration(line, labelName, statementText, span);
20
+ const declaration = parseColonDeclaration(line, label.name, statementText, span);
18
21
  if (declaration) {
19
22
  return withLineComment(line, declaration);
20
23
  }
21
24
  const parsedStatement = parseCanonicalStatement(line, statementText, span);
22
25
  return withLineComment(line, {
23
26
  items: [
24
- { kind: 'label', name: labelName, ...(isEntry ? { isEntry: true } : {}), span },
27
+ { kind: 'label', name: label.name, ...(label.isEntry ? { isEntry: true } : {}), span },
25
28
  ...parsedStatement.items,
26
29
  ],
27
30
  diagnostics: parsedStatement.diagnostics,
28
31
  });
29
32
  }
30
- const labelOnly = /^(@?[A-Za-z_.$?][A-Za-z0-9_.$?]*):$/.exec(text);
33
+ const labelOnly = new RegExp(`^(@?${LABEL_NAME_PATTERN}):$`).exec(text);
31
34
  if (labelOnly) {
32
35
  const rawLabel = labelOnly[1] ?? '';
36
+ const label = parseEntryLabel(rawLabel);
37
+ if (!label)
38
+ return withLineComment(line, parseCanonicalStatement(line, text, span));
33
39
  return withLineComment(line, {
34
40
  items: [
35
41
  {
36
42
  kind: 'label',
37
- name: normalizeEntryLabelName(rawLabel),
38
- ...(rawLabel.startsWith('@') ? { isEntry: true } : {}),
43
+ name: label.name,
44
+ ...(label.isEntry ? { isEntry: true } : {}),
39
45
  span,
40
46
  },
41
47
  ],
@@ -58,7 +64,7 @@ function commentOnlyLine(line) {
58
64
  span: {
59
65
  sourceName: line.sourceName,
60
66
  line: line.line,
61
- column: firstColumn(line.text),
67
+ column: firstNonWhitespaceColumn(line.text),
62
68
  ...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
63
69
  ...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
64
70
  },
@@ -82,7 +88,7 @@ function withLineComment(line, result) {
82
88
  span: {
83
89
  sourceName: line.sourceName,
84
90
  line: line.line,
85
- column: firstColumn(line.text),
91
+ column: firstNonWhitespaceColumn(line.text),
86
92
  ...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
87
93
  ...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
88
94
  },
@@ -106,37 +112,20 @@ function parseCanonicalStatement(line, text, span) {
106
112
  if (instruction?.diagnostics && instruction.diagnostics.length > 0) {
107
113
  return {
108
114
  items: [],
109
- diagnostics: instruction.diagnostics.map((message) => parseError(line, message)),
115
+ diagnostics: instruction.diagnostics.map((message) => parseLineError(line, message)),
110
116
  };
111
117
  }
112
118
  if (instruction?.error) {
113
- return { items: [], diagnostics: [parseError(line, instruction.error)] };
119
+ return { items: [], diagnostics: [parseLineError(line, instruction.error)] };
114
120
  }
115
- return { items: [], diagnostics: [parseError(line, `unsupported source line: ${text}`)] };
121
+ return { items: [], diagnostics: [parseLineError(line, `unsupported source line: ${text}`)] };
116
122
  }
117
123
  function spanForLine(line) {
118
124
  return {
119
125
  sourceName: line.sourceName,
120
126
  line: line.line,
121
- column: firstColumn(line.text),
127
+ column: firstNonWhitespaceColumn(line.text),
122
128
  ...(line.sourceUnit !== undefined ? { sourceUnit: line.sourceUnit } : {}),
123
129
  ...(line.sourceRelation !== undefined ? { sourceRelation: line.sourceRelation } : {}),
124
130
  };
125
131
  }
126
- function normalizeEntryLabelName(raw) {
127
- return raw.startsWith('@') ? raw.slice(1) : raw;
128
- }
129
- function firstColumn(text) {
130
- const match = /\S/.exec(text);
131
- return match ? match.index + 1 : 1;
132
- }
133
- function parseError(line, message) {
134
- return {
135
- severity: 'error',
136
- code: 'AZMN_PARSE',
137
- message,
138
- sourceName: line.sourceName,
139
- line: line.line,
140
- column: firstColumn(line.text),
141
- };
142
- }
@@ -0,0 +1,7 @@
1
+ import type { LogicalLine } from '../source/logical-lines.js';
2
+ import type { ParseLineResult } from './parse-line.js';
3
+ export declare function parseExpressionDirective(line: LogicalLine, kind: 'align' | 'binfrom' | 'binto' | 'org', expressionText: string, span: {
4
+ readonly sourceName: string;
5
+ readonly line: number;
6
+ readonly column: number;
7
+ }): ParseLineResult;
@@ -0,0 +1,15 @@
1
+ import { parseLineError } from './parse-diagnostics.js';
2
+ import { parseExpression } from './parse-expression.js';
3
+ export function parseExpressionDirective(line, kind, expressionText, span) {
4
+ const expression = parseExpression(expressionText);
5
+ if (!expression) {
6
+ return {
7
+ items: [],
8
+ diagnostics: [parseLineError(line, `invalid .${kind} expression: ${expressionText}`)],
9
+ };
10
+ }
11
+ if (kind === 'align') {
12
+ return { items: [{ kind, alignment: expression, span }], diagnostics: [] };
13
+ }
14
+ return { items: [{ kind, expression, span }], diagnostics: [] };
15
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isChainedDirectiveOrDeclaration(text: string): boolean;
2
+ export declare function isPotentialOpInvocationStatement(text: string): boolean;
@@ -0,0 +1,24 @@
1
+ import { IDENTIFIER_PATTERN, LABEL_NAME_PATTERN } from './names.js';
2
+ const CHAIN_DECLARATION_RE = new RegExp(`^${LABEL_NAME_PATTERN}\\s+\\.?(?:equ|enum|type|union|typealias)\\b`, 'i');
3
+ const IDENTIFIER_STATEMENT_RE = new RegExp(`^${IDENTIFIER_PATTERN}(?:\\s+.*)?$`);
4
+ const EQU_DECLARATION_RE = new RegExp(`^${IDENTIFIER_PATTERN}\\s+\\.?equ\\b`, 'i');
5
+ const LAYOUT_DECLARATION_RE = new RegExp(`^${IDENTIFIER_PATTERN}\\s+\\.(?:enum|type|union|typealias|field|byte|word|addr)\\b`);
6
+ export function isChainedDirectiveOrDeclaration(text) {
7
+ return (/^\./.test(text) ||
8
+ /^(?:org|equ|db|dw|ds|align|include|import|binfrom|binto|cstr|pstr|istr|end|enum|type|union|field|byte|word|addr|endtype|endunion|typealias|if|else|endif|op)\b/i.test(text) ||
9
+ CHAIN_DECLARATION_RE.test(text));
10
+ }
11
+ export function isPotentialOpInvocationStatement(text) {
12
+ if (!IDENTIFIER_STATEMENT_RE.test(text))
13
+ return false;
14
+ if (EQU_DECLARATION_RE.test(text))
15
+ return false;
16
+ if (LAYOUT_DECLARATION_RE.test(text))
17
+ return false;
18
+ if (/^(?:op|end|enum|type|union|field|byte|word|addr)\b/i.test(text))
19
+ return false;
20
+ if (/^(?:org|equ|db|dw|ds|align|include|binfrom|binto|cstr|pstr|istr)\b/i.test(text)) {
21
+ return false;
22
+ }
23
+ return true;
24
+ }
@@ -1,4 +1,7 @@
1
+ import { splitInstructionChain } from '../source/instruction-chain.js';
1
2
  import { stripLineComment } from '../source/strip-line-comment.js';
3
+ import { IDENTIFIER_PATTERN } from '../syntax/names.js';
4
+ import { isPotentialOpInvocationStatement } from '../syntax/statement-classification.js';
2
5
  const REGISTER_RE = /(?<![A-Za-z0-9_$])(AF'|AF|BC|DE|HL|SP|IXH|IXL|IYH|IYL|IX|IY|A|B|C|D|E|H|L|I|R)(?![A-Za-z0-9_])/gi;
3
6
  export function lintCaseStyleNext(options) {
4
7
  if (options.mode === 'off')
@@ -23,19 +26,32 @@ function buildSourceLineMap(sourceTexts) {
23
26
  return result;
24
27
  }
25
28
  function lintInstructionLine(rawLine, sourceName, line, mode, state, diagnostics) {
26
- const text = stripLeadingLabels(stripLineComment(rawLine)).trim();
29
+ const segments = splitInstructionChain(rawLine);
30
+ if (segments !== undefined) {
31
+ for (let index = 0; index < segments.length; index += 1) {
32
+ const stripped = index === 0
33
+ ? stripLeadingLabelsWithOffset(segments[index].text)
34
+ : { text: segments[index].text, offset: 0 };
35
+ lintInstructionSegment(stripped.text.trim(), segments[index].column + stripped.offset + firstColumn(stripped.text) - 1, sourceName, line, mode, state, diagnostics);
36
+ }
37
+ return;
38
+ }
39
+ const stripped = stripLeadingLabelsWithOffset(stripLineComment(rawLine));
40
+ lintInstructionSegment(stripped.text.trim(), stripped.offset + firstColumn(stripped.text), sourceName, line, mode, state, diagnostics);
41
+ }
42
+ function lintInstructionSegment(text, baseColumn, sourceName, line, mode, state, diagnostics) {
27
43
  if (text.length === 0)
28
44
  return;
29
45
  const mnemonic = text.split(/\s+/, 1)[0] ?? '';
30
46
  if (mnemonic.length > 0) {
31
- lintToken(mode, state, mnemonic, 'mnemonic', sourceName, line, diagnostics);
47
+ lintToken(mode, state, mnemonic, 'mnemonic', sourceName, line, baseColumn, diagnostics);
32
48
  }
33
49
  const scrubbed = scrubCharLiterals(text);
34
50
  for (const match of scrubbed.matchAll(REGISTER_RE)) {
35
51
  const raw = match[1];
36
52
  if (!raw)
37
53
  continue;
38
- lintToken(mode, state, raw, 'register', sourceName, line, diagnostics);
54
+ lintToken(mode, state, raw, 'register', sourceName, line, baseColumn + match.index, diagnostics);
39
55
  }
40
56
  }
41
57
  function lintSourceLines(sourceLines, instructionLines, mode, state, diagnostics) {
@@ -67,42 +83,30 @@ function shouldLintCaseStyleLine(rawLine, sourceName, line, instructionLines, st
67
83
  isPotentialOpInvocationLine(text));
68
84
  }
69
85
  function isOpHeaderLine(text) {
70
- return /^op\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/i.test(text);
86
+ return new RegExp(`^op\\s+${IDENTIFIER_PATTERN}\\s*\\(`, 'i').test(text);
71
87
  }
72
88
  function isOpEndLine(text) {
73
89
  return /^end\s*$/i.test(text);
74
90
  }
75
91
  function isPotentialOpInvocationLine(text) {
76
- if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\s+.*)?$/.test(text))
77
- return false;
78
- if (/^[A-Za-z_][A-Za-z0-9_]*\s+\.?equ\b/i.test(text))
79
- return false;
80
- if (/^[A-Za-z_][A-Za-z0-9_]*\s+\.(?:enum|type|union|typealias|field|byte|word|addr)\b/.test(text)) {
81
- return false;
82
- }
83
- if (/^(?:op|end|enum|type|union|field|byte|word|addr)\b/i.test(text))
84
- return false;
85
- if (/^(?:org|equ|db|dw|ds|align|include|binfrom|binto|cstr|pstr|istr)\b/i.test(text)) {
86
- return false;
87
- }
88
- return true;
92
+ return isPotentialOpInvocationStatement(text);
89
93
  }
90
94
  function lineKey(sourceName, line) {
91
95
  return `${sourceName}:${line}`;
92
96
  }
93
- function lintToken(mode, state, token, category, sourceName, line, diagnostics) {
97
+ function lintToken(mode, state, token, category, sourceName, line, column, diagnostics) {
94
98
  const style = classifyTokenStyle(token);
95
99
  if (!style)
96
100
  return;
97
101
  if (mode === 'consistent') {
98
- lintConsistentToken(state, style, token, category, sourceName, line, diagnostics);
102
+ lintConsistentToken(state, style, token, category, sourceName, line, column, diagnostics);
99
103
  return;
100
104
  }
101
105
  if (mode === 'off')
102
106
  return;
103
- lintFixedStyleToken(mode, style, token, category, sourceName, line, diagnostics);
107
+ lintFixedStyleToken(mode, style, token, category, sourceName, line, column, diagnostics);
104
108
  }
105
- function lintConsistentToken(state, style, token, category, sourceName, line, diagnostics) {
109
+ function lintConsistentToken(state, style, token, category, sourceName, line, column, diagnostics) {
106
110
  if (!state.consistentStyle && (style === 'upper' || style === 'lower')) {
107
111
  state.consistentStyle = style;
108
112
  return;
@@ -116,10 +120,10 @@ function lintConsistentToken(state, style, token, category, sourceName, line, di
116
120
  message: `Case-style lint: ${category} "${token}" does not match established ${expected}case style under --case-style=consistent.`,
117
121
  sourceName,
118
122
  line,
119
- column: 1,
123
+ column,
120
124
  });
121
125
  }
122
- function lintFixedStyleToken(mode, style, token, category, sourceName, line, diagnostics) {
126
+ function lintFixedStyleToken(mode, style, token, category, sourceName, line, column, diagnostics) {
123
127
  if (style === mode)
124
128
  return;
125
129
  const expectedText = mode === 'upper' ? 'uppercase' : 'lowercase';
@@ -129,7 +133,7 @@ function lintFixedStyleToken(mode, style, token, category, sourceName, line, dia
129
133
  message: `Case-style lint: ${category} "${token}" should be ${expectedText} under --case-style=${mode}.`,
130
134
  sourceName,
131
135
  line,
132
- column: 1,
136
+ column,
133
137
  });
134
138
  }
135
139
  function classifyTokenStyle(token) {
@@ -143,14 +147,26 @@ function classifyTokenStyle(token) {
143
147
  return 'mixed';
144
148
  }
145
149
  function stripLeadingLabels(text) {
150
+ return stripLeadingLabelsWithOffset(text).text;
151
+ }
152
+ function stripLeadingLabelsWithOffset(text) {
146
153
  let remaining = text;
154
+ let offset = 0;
147
155
  while (true) {
148
- const stripped = remaining.replace(/^\s*[A-Za-z_.$?][A-Za-z0-9_.$?]*\s*:\s*/, '');
156
+ const match = /^\s*[A-Za-z_.$?][A-Za-z0-9_.$?]*\s*:\s*/.exec(remaining);
157
+ if (!match)
158
+ return { text: remaining, offset };
159
+ const stripped = remaining.slice(match[0].length);
149
160
  if (stripped === remaining)
150
- return remaining;
161
+ return { text: remaining, offset };
151
162
  remaining = stripped;
163
+ offset += match[0].length;
152
164
  }
153
165
  }
166
+ function firstColumn(text) {
167
+ const match = /\S/.exec(text);
168
+ return match ? match.index + 1 : 1;
169
+ }
154
170
  function scrubCharLiterals(text) {
155
171
  let output = '';
156
172
  let inChar = false;
@@ -15,9 +15,9 @@ tooling and register contracts consume.
15
15
 
16
16
  The loading boundary lives in `src/node/source-host.ts`. The parser is
17
17
  orchestrated by `parseNextSourceItems()` in `src/core/compile.ts`, with
18
- single-line parsing in `src/syntax/parse-line.ts`. Expression and declaration
19
- parsing is split across tokenizer, token-expression, directive and layout
20
- modules in `src/syntax/`.
18
+ single-line parsing in `src/syntax/parse-line.ts`. Expression, name,
19
+ instruction-chain and directive parsing is split across small syntax helpers in
20
+ `src/syntax/`.
21
21
 
22
22
  ## Entry Files and Source Text
23
23
 
@@ -123,6 +123,7 @@ The source helpers are small and important:
123
123
  | `logical-lines.ts` | Splits text into line records. |
124
124
  | `source-span.ts` | Defines the common span shape. |
125
125
  | `line-comment-scanner.ts` | Finds line comments while respecting quoted text. |
126
+ | `instruction-chain.ts` | Finds spaced-backslash separators and segment columns. |
126
127
  | `strip-line-comment.ts` | Removes semicolon comments through the shared scanner. |
127
128
 
128
129
  `strip-line-comment.ts` is used by source-loading directive recognition, layout parsing,
@@ -130,6 +131,15 @@ conditional assembly and single-line parsing. Shared comment handling prevents
130
131
  each stage from inventing a slightly different rule for semicolons inside
131
132
  strings and character literals.
132
133
 
134
+ `src/source/instruction-chain.ts` uses the same quoted-text rules to find
135
+ readable ` \ ` separators without splitting byte and string operands. It
136
+ reports trimmed segment text plus the original 1-based column for each segment,
137
+ so later stages can keep diagnostics and source maps aligned to the exact
138
+ instruction inside a physical line. `src/syntax/parse-instruction-chain.ts`
139
+ then applies the syntax rules: labels are allowed only before the first segment,
140
+ directives and declarations are rejected, and each segment is parsed as an
141
+ instruction or op invocation.
142
+
133
143
  ## Directive Aliases
134
144
 
135
145
  Directive aliases are loaded during `loadProgramNext()`:
@@ -182,13 +192,16 @@ spans to connect emitted bytes back to files and lines.
182
192
  4. Record and union headers collect `.field` declarations until `.endtype` or
183
193
  `.endunion`.
184
194
  5. Visible op invocations expand into ordinary source items.
185
- 6. `parseLogicalLine()` handles single-line labels, directives, data and
186
- instructions.
195
+ 6. Chained instruction lines are split on spaced backslashes and each segment is
196
+ parsed as an instruction or op invocation.
197
+ 7. `parseLogicalLine()` handles remaining single-line labels, directives, data
198
+ and instructions.
187
199
 
188
200
  This order matters. Ops must be collected before invocation expansion. Layout
189
201
  declarations must collect their body lines as one source item. Ordinary
190
202
  instruction parsing should see the lines that remain after those structural
191
- forms have been handled.
203
+ forms have been handled. Chained instruction parsing also needs the op registry
204
+ up front so later segments can expand ops and keep segment-level columns.
192
205
 
193
206
  ## Layout and Declaration Parsing
194
207
 
@@ -237,10 +250,17 @@ assembler-time value based on expression evaluation.
237
250
  `src/syntax/expression-tokenizer.ts` tokenizes expression text.
238
251
  `parse-token-expression.ts` builds expression trees from tokens.
239
252
  `parse-expression.ts` is the public syntax wrapper used by line parsing.
253
+ `names.ts` centralises identifier, label and `@` entry-label parsing so line
254
+ parsing, chained-line parsing, op expansion and tooling share the same name
255
+ rules.
256
+ `statement-classification.ts` distinguishes potential op invocations from
257
+ declarations and rejects directives inside chained instruction segments.
240
258
  `parse-layout-expression.ts` parses layout type expressions used by `.ds`,
241
259
  `.field`, `.typealias`, `sizeof(...)`, `offset(...)` and layout casts.
242
- `parse-directive-statement.ts` parses directive statements that need more than
243
- single-token recognition.
260
+ `parse-directive-statement.ts` dispatches directive statements into focused
261
+ parsers: declaration directives in `parse-declaration-directives.ts`, data and
262
+ string directives in `parse-data-directives.ts` and expression-bearing location
263
+ directives in `parse-location-directives.ts`.
244
264
 
245
265
  The parser produces expression trees from `src/model/expression.ts`.
246
266
  `src/semantics/expression-evaluation.ts` evaluates those trees when the
@@ -67,6 +67,12 @@ The parser handles op invocations before `parseLogicalLine()`. An op head can
67
67
  look like an instruction head at the source level. The expansion stage resolves
68
68
  it before ordinary line parsing.
69
69
 
70
+ Top-level parsing also recognises chained instruction lines separated by a
71
+ spaced backslash. Each segment is parsed independently, with labels only
72
+ allowed before the first segment and directives still restricted to their own
73
+ physical lines. That lets short instruction runs compact onto one source line
74
+ without changing directive or declaration shape.
75
+
70
76
  ## Overloads and Templates
71
77
 
72
78
  Ops support overloads. `op-selection.ts` compares invocation operands against
@@ -84,6 +90,12 @@ from the call site are substituted into the template by
84
90
  `op-instruction-instantiation.ts`. The result is formatted as ordinary source
85
91
  text and parsed through the same line parser used for top-level source.
86
92
 
93
+ Op bodies now accept the same chained-instruction form as top-level source.
94
+ `collectOps()` splits a spaced-backslash body line into template segments before
95
+ it decides whether each segment is a parameterised instruction template, an
96
+ ordinary source-item fragment or a nested op invocation. A first label may lead
97
+ the chain and becomes an ordinary label item in the expanded body.
98
+
87
99
  Local label rewriting lives in `op-local-labels.ts`. A local label in an op
88
100
  expansion becomes unique at the use site so each expansion receives its own
89
101
  generated label. Once the rewritten labels become source items, address planning
@@ -148,7 +160,10 @@ items. Routine-specific extraction is split into
148
160
  `programModel-boundaries.ts` and `programModel-routines.ts`. Together they find
149
161
  routine boundaries, direct calls, labels and instructions. Routine entry labels
150
162
  use `@` in source and become callable public routine names after the marker is
151
- removed.
163
+ removed. When an imported public routine calls private labels inside the same
164
+ import unit, the routine builder keeps those direct call targets in the
165
+ internal routine set so strict analysis can follow imported helper routines
166
+ without exposing them outside the module.
152
167
 
153
168
  `src/register-contracts/smartComments.ts` reads AZMDoc comments from the comment maps
154
169
  captured during loading. Comment-block splitting and token parsing live in
@@ -166,6 +181,8 @@ Contracts can describe:
166
181
  Source comments and external interfaces describe the same kind of fact: a
167
182
  routine contract. Source comments attach to routines in the current program.
168
183
  `.asmi` entries attach to routines whose source is assembled elsewhere.
184
+ The compact source form can place several clauses on one `;!` line, for
185
+ example `;! in A; out A; clobbers F`.
169
186
 
170
187
  ## Effects, Summaries and Liveness
171
188
 
@@ -198,8 +215,12 @@ boundary may leave the stack in an unknown state.
198
215
  ## Reports, Interfaces and Tooling
199
216
 
200
217
  `report.ts` renders human-readable `.regcontracts.txt` reports and `.asmi` interface
201
- metadata. `annotate.ts`, `annotations.ts`, `fix.ts` and `sourceText.ts` support
202
- source updates for generated AZMDoc comments and conservative fixes.
218
+ metadata. It also renders generated source contracts as one compact `;!` line,
219
+ joining `in`, `out`, `maybe-out` and `clobbers` clauses with semicolons.
220
+ When a routine may clobber the full flag set, the source form uses `F` as the
221
+ compact carrier name. `annotate.ts`, `annotations.ts`, `fix.ts` and
222
+ `sourceText.ts` support source updates for generated AZMDoc comments and
223
+ conservative fixes.
203
224
 
204
225
  The CLI can request these behaviours through:
205
226
 
@@ -164,6 +164,11 @@ When `loaded.loadedProgram` is present, the editor can call
164
164
  `analyzeRegisterContractsForTools()` for register contract candidate diagnostics
165
165
  and code actions.
166
166
 
167
+ Case-style linting now understands chained instruction lines. A physical line
168
+ such as `LD A,B \ inc c \ RET` is linted per segment, and warnings report the
169
+ column of the specific mnemonic or register inside the chain rather than the
170
+ start of the line.
171
+
167
172
  ## Artifact Types
168
173
 
169
174
  The output layer uses structured artifact objects from `src/outputs/types.ts`:
@@ -212,6 +217,11 @@ The writer normalizes source paths through `sourceRoot` when provided. It also
212
217
  coalesces source segments and clips them to written ranges so Debug80 receives a
213
218
  clean map of source lines to emitted bytes.
214
219
 
220
+ When one physical line emits multiple chained instructions, each emitted segment
221
+ keeps its own source column in the D8 map. Debug80 can therefore point at the
222
+ exact chained instruction that produced each byte range instead of collapsing
223
+ the whole line to one column.
224
+
215
225
  The D8 map distinguishes addressable symbols from constants. Labels and
216
226
  addressable data carry addresses. Constants carry values. Debug80 can then use
217
227
  addressable symbols for breakpoints and display constants as metadata.