@jhlagado/azm 0.2.10 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +14 -0
  2. package/dist/src/core/compile.js +97 -1
  3. package/dist/src/expansion/op-expansion.js +47 -9
  4. package/dist/src/outputs/d8-files.js +1 -0
  5. package/dist/src/outputs/types.d.ts +1 -0
  6. package/dist/src/source/instruction-chain.d.ts +5 -0
  7. package/dist/src/source/instruction-chain.js +75 -0
  8. package/dist/src/syntax/names.d.ts +18 -0
  9. package/dist/src/syntax/names.js +44 -0
  10. package/dist/src/syntax/parse-data-directives.d.ts +17 -0
  11. package/dist/src/syntax/parse-data-directives.js +147 -0
  12. package/dist/src/syntax/parse-declaration-directives.d.ts +18 -0
  13. package/dist/src/syntax/parse-declaration-directives.js +90 -0
  14. package/dist/src/syntax/parse-diagnostics.d.ts +8 -0
  15. package/dist/src/syntax/parse-diagnostics.js +15 -0
  16. package/dist/src/syntax/parse-directive-statement.d.ts +1 -5
  17. package/dist/src/syntax/parse-directive-statement.js +19 -259
  18. package/dist/src/syntax/parse-instruction-chain.d.ts +22 -0
  19. package/dist/src/syntax/parse-instruction-chain.js +62 -0
  20. package/dist/src/syntax/parse-layout-declarations.js +9 -18
  21. package/dist/src/syntax/parse-layout-expression.js +4 -3
  22. package/dist/src/syntax/parse-line.js +20 -31
  23. package/dist/src/syntax/parse-location-directives.d.ts +7 -0
  24. package/dist/src/syntax/parse-location-directives.js +15 -0
  25. package/dist/src/syntax/statement-classification.d.ts +2 -0
  26. package/dist/src/syntax/statement-classification.js +24 -0
  27. package/dist/src/tooling/case-style.js +42 -26
  28. package/docs/codebase/02-source-loading-and-parsing.md +28 -8
  29. package/docs/codebase/04-ops-and-register-contracts.md +24 -3
  30. package/docs/codebase/05-interfaces-and-output-artifacts.md +10 -0
  31. package/docs/codebase/06-verification-and-maintenance.md +9 -3
  32. package/docs/codebase/appendices/a-directory-file-reference.md +17 -10
  33. package/docs/codebase/appendices/b-compile-flow-reference.md +3 -2
  34. package/docs/codebase/index.md +4 -0
  35. package/package.json +1 -1
@@ -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.
@@ -74,6 +74,11 @@ These tests compare AZM behaviour against ASM80 expectations, lowered output and
74
74
  real-program fixtures. For an assembler, a one-byte difference is a behavioural
75
75
  change.
76
76
 
77
+ The chained-instruction feature currently lives in `test/asm80/` because it
78
+ crosses parser, op expansion, D8 map output and case-style linting in one user
79
+ visible source shape. `multi_instruction_lines.test.ts` is the focused
80
+ acceptance suite for that contract.
81
+
77
82
  ## Fixtures and Helpers
78
83
 
79
84
  `test/fixtures/` contains small source programs named after the issue or
@@ -115,11 +120,11 @@ Use this map when choosing a verification lane:
115
120
 
116
121
  | Change | Tests |
117
122
  | --------------------------- | -------------------------------------------------------------------- |
118
- | Parser or expression syntax | `test/unit/syntax/**`, relevant integration tests. |
123
+ | Parser or expression syntax | `test/unit/syntax/**`, relevant integration tests and `test/asm80/multi_instruction_lines.test.ts` when the change affects chained physical lines. |
119
124
  | Source loading or tooling provenance | `test/integration/stage-11-tooling-api.test.ts`, relevant unit tests in `test/unit/source/**`. |
120
125
  | Z80 instruction support | `test/unit/z80/**`, diagnostic matrices, ASM80 parity when relevant. |
121
126
  | Layout semantics | layout integration tests and output tests. |
122
- | Ops | `test/unit/expansion/**`, op integration tests. |
127
+ | Ops | `test/unit/expansion/**`, op integration tests and `test/asm80/multi_instruction_lines.test.ts` for chained bodies and invocation segments. |
123
128
  | Register contracts | register contract unit, integration and CLI tests. |
124
129
  | CLI options | `test/cli/**`. |
125
130
  | Output artifacts | `test/unit/outputs/**`, CLI artifact tests. |
@@ -145,7 +150,8 @@ Ask what kind of fact the change affects:
145
150
  `core/compile.ts`.
146
151
  - Assembler-time facts belong in `assembly/` and `semantics/`.
147
152
  - Instruction forms belong in `z80/`.
148
- - Inline source generation belongs in `expansion/`.
153
+ - Instruction-chain splitting belongs in `source/`, while inline source
154
+ generation and op template expansion belong in `expansion/`.
149
155
  - Routine contracts and liveness belong in `register-contracts/`.
150
156
  - Artifact shape belongs in `outputs/`.
151
157
  - User commands belong in `cli/`.
@@ -172,21 +172,28 @@ you need to find the owner of a behaviour quickly.
172
172
  | `logical-lines.ts` | Splits source text into logical line records. |
173
173
  | `source-span.ts` | Defines source span shape. |
174
174
  | `line-comment-scanner.ts` | Finds semicolon comments while respecting quoted text. |
175
+ | `instruction-chain.ts` | Splits spaced-backslash instruction lines into segments. |
175
176
  | `strip-line-comment.ts` | Removes semicolon comments while respecting quoted text. |
176
177
 
177
178
  ## `src/syntax/`
178
179
 
179
180
  | File | Role |
180
181
  | ------------------------------ | ----------------------------------------------------- |
181
- | `parse-line.ts` | Parses single logical lines into source items. |
182
- | `expression-tokenizer.ts` | Tokenizes expression text. |
183
- | `parse-token-expression.ts` | Parses tokenized expressions into ASTs. |
184
- | `parse-expression.ts` | Public expression parse wrapper. |
185
- | `parse-directive-statement.ts` | Parses directive statements with structured operands. |
186
- | `parse-layout-declarations.ts` | Parses layout declaration forms. |
187
- | `parse-layout-expression.ts` | Parses layout type expressions. |
188
- | `parse-diagnostics.ts` | Shared parse diagnostic helpers. |
189
- | `directive-aliases.ts` | Built-in and project directive alias policy. |
182
+ | `parse-line.ts` | Parses single logical lines into source items. |
183
+ | `expression-tokenizer.ts` | Tokenizes expression text. |
184
+ | `parse-token-expression.ts` | Parses tokenized expressions into ASTs. |
185
+ | `parse-expression.ts` | Public expression parse wrapper. |
186
+ | `names.ts` | Shared identifier, label and entry-label parsing primitives. |
187
+ | `parse-instruction-chain.ts` | Parses spaced-backslash chained instruction segments. |
188
+ | `statement-classification.ts` | Shared chained-line and op-invocation classification helpers. |
189
+ | `parse-directive-statement.ts` | Dispatches directive statements to focused directive parsers. |
190
+ | `parse-data-directives.ts` | Parses `.db`, `.dw`, `.ds`, `.cstr`, `.pstr` and `.istr`. |
191
+ | `parse-declaration-directives.ts` | Parses `.equ`, `.enum` and quoted declaration payloads. |
192
+ | `parse-location-directives.ts` | Parses `.org`, `.align`, `.binfrom` and `.binto`. |
193
+ | `parse-layout-declarations.ts` | Parses layout declaration forms. |
194
+ | `parse-layout-expression.ts` | Parses layout type expressions. |
195
+ | `parse-diagnostics.ts` | Shared parse diagnostic helpers. |
196
+ | `directive-aliases.ts` | Built-in and project directive alias policy. |
190
197
 
191
198
  ## `src/tooling/`
192
199
 
@@ -232,7 +239,7 @@ you need to find the owner of a behaviour quickly.
232
239
  | `test/integration/` | Cross-stage compiler tests. |
233
240
  | `test/integration/register-contracts/` | End-to-end register contract tests. |
234
241
  | `test/cli/` | CLI option, artifact and exit-code contracts. |
235
- | `test/asm80/` | ASM80 compatibility and real-program acceptance. |
242
+ | `test/asm80/` | ASM80 compatibility, real-program acceptance and chained-line behaviour. |
236
243
  | `test/differential/` | Differential comparison fixtures and runners. |
237
244
  | `test/fixtures/` | Source fixture programs. |
238
245
  | `test/helpers/` | Shared test setup and helpers. |
@@ -30,9 +30,10 @@ compile(entryFile, options, deps)
30
30
  parseNextSourceItems()
31
31
  applyConditionalAssembly()
32
32
  collect op definitions
33
+ expand op invocations
34
+ split chained instruction lines
33
35
  tokenize and parse expressions
34
36
  parse layouts, aliases, enums, directives and instructions
35
- expand op invocations
36
37
  analyzeProgramNext()
37
38
  assembleProgram() for symbols
38
39
  lintCaseStyleNext()
@@ -98,6 +99,6 @@ tooling integrations that still use the older name.
98
99
  | Parsing | logical lines | source items |
99
100
  | Analysis | source items | diagnostics, symbols |
100
101
  | Register contracts | loaded program | summaries, conflicts, reports |
101
- | Assembly | source items | byte map, symbols, source segments |
102
+ | Assembly | source items | byte map, symbols, source segments with per-item columns |
102
103
  | Outputs | byte map and symbols | artifacts |
103
104
  | CLI | artifacts | files on disk |
@@ -44,3 +44,7 @@ modules. The directory appendix is the current file map for those modules.
44
44
  - [Appendix A - Directory and File Reference](appendices/a-directory-file-reference.md)
45
45
  - [Appendix B - Compile Flow Reference](appendices/b-compile-flow-reference.md)
46
46
  - [Appendix C - Public Surface Reference](appendices/c-public-surface-reference.md)
47
+
48
+ ## Related References
49
+
50
+ - [AZM Grammar Reference](../reference/azm-grammar.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhlagado/azm",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "AZM assembler for the Z80 family (Node.js CLI)",
5
5
  "license": "GPL-3.0-only",
6
6
  "engines": {