@jhlagado/azm 0.2.8 → 0.2.9

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 (29) hide show
  1. package/README.md +68 -6
  2. package/dist/src/api-compile.js +27 -0
  3. package/dist/src/assembly/assemble-program.js +5 -0
  4. package/dist/src/assembly/import-visibility.d.ts +3 -0
  5. package/dist/src/assembly/import-visibility.js +204 -0
  6. package/dist/src/node/source-host.js +40 -13
  7. package/dist/src/outputs/write-asm80.js +4 -0
  8. package/dist/src/register-contracts/programModel-routines.js +33 -17
  9. package/dist/src/source/logical-lines.d.ts +3 -0
  10. package/dist/src/source/source-span.d.ts +2 -0
  11. package/dist/src/syntax/parse-directive-statement.d.ts +1 -6
  12. package/dist/src/syntax/parse-directive-statement.js +3 -1
  13. package/dist/src/syntax/parse-layout-declarations.js +11 -2
  14. package/dist/src/syntax/parse-line.js +18 -2
  15. package/dist/src/tooling/api.js +1 -1
  16. package/docs/codebase/01-orientation-and-repository-layout.md +192 -0
  17. package/docs/codebase/02-source-loading-and-parsing.md +263 -0
  18. package/docs/codebase/03-assembly-and-z80-emission.md +251 -0
  19. package/docs/codebase/04-ops-and-register-contracts.md +237 -0
  20. package/docs/codebase/05-interfaces-and-output-artifacts.md +253 -0
  21. package/docs/codebase/06-verification-and-maintenance.md +202 -0
  22. package/docs/codebase/appendices/a-directory-file-reference.md +253 -0
  23. package/docs/codebase/appendices/b-compile-flow-reference.md +103 -0
  24. package/docs/codebase/appendices/c-public-surface-reference.md +106 -0
  25. package/docs/codebase/appendices/index.md +16 -0
  26. package/docs/codebase/index.md +46 -0
  27. package/package.json +2 -3
  28. package/docs/reference/cli.md +0 -158
  29. package/docs/reference/tooling-api.md +0 -320
package/README.md CHANGED
@@ -335,12 +335,74 @@ azm -I include -I vendor program.asm
335
335
  Included source contributes labels, constants, enums, types, ops and routines to
336
336
  the same assembly.
337
337
 
338
+ ## Imports
339
+
340
+ `.import` is AZM's module-style source composition directive:
341
+
342
+ ```asm
343
+ ; main.asm
344
+ .import "keyboard.asm"
345
+
346
+ @Start:
347
+ call ReadKey
348
+ ret
349
+ ```
350
+
351
+ ```asm
352
+ ; keyboard.asm
353
+ @ReadKey:
354
+ call ScanMatrix
355
+ ret
356
+
357
+ ScanMatrix:
358
+ xor a
359
+ ret
360
+ ```
361
+
362
+ Imported source assembles at the point where `.import` appears, so native
363
+ `.bin`, `.hex` and `.d8.json` output contain the imported bytes as part of the
364
+ same program. Paths resolve like includes: relative to the importing file first,
365
+ then through `-I` include directories.
366
+
367
+ The difference is visibility. In an imported file, labels written with `@` are
368
+ public exports. Code outside `keyboard.asm` can call `ReadKey`, using the name
369
+ without `@`. Plain labels in an imported file, such as `ScanMatrix`, are private
370
+ to that imported file or import unit. The imported file may call its own private
371
+ helpers, but outside references fail with a direct visibility diagnostic.
372
+
373
+ `.include` remains textual. Included text belongs to the including source unit
374
+ and is intended for shared constants, declarations and compatibility source.
375
+ Use `.import` when a source file should behave like a module with public `@`
376
+ entry points and private implementation labels.
377
+
378
+ Repeated imports of the same resolved file are idempotent: the first import
379
+ loads and emits the module, later imports of that same file are skipped.
380
+ Repeated includes are still textual and repeat every time. Recursive includes or
381
+ imports are rejected with source diagnostics.
382
+
383
+ Register contracts use the same `@` routine boundaries across imported files.
384
+ Imported public routines are analyzed as internal routines under `--rc strict`,
385
+ and private helpers called inside an imported public routine are summarized as
386
+ part of the same program analysis.
387
+
388
+ ASM80-compatible lowered `.z80` output does not yet support imported source
389
+ units. If `--asm80` is requested for a program that uses `.import`, AZM reports
390
+ an explicit `AZMN_ASM80` diagnostic instead of silently flattening the module
391
+ boundary. Use native `.bin`, `.hex` and `.d8.json` output for imported programs.
392
+
338
393
  ## Register Contracts
339
394
 
340
395
  Register contracts check whether subroutines preserve the register values that their
341
396
  callers still need. It is designed to catch register collisions, a common source
342
397
  of assembly bugs.
343
398
 
399
+ The benefit is practical: AZM can stop a plausible-looking routine at compile
400
+ time when it reads a register after calling code that may clobber it. In larger
401
+ Z80 projects this encourages smaller routines, clearer `@` boundaries, explicit
402
+ helper outputs, and proof or test harnesses that stay honest under
403
+ `--rc strict`. The friction is intentional: strict contracts make hidden
404
+ register and stack assumptions visible before they become debugger sessions.
405
+
344
406
  Routine entry labels start with `@`:
345
407
 
346
408
  ```asm
@@ -498,8 +560,8 @@ The main switches are:
498
560
  | `-V, --version` | Print package version. |
499
561
  | `-h, --help` | Print CLI help. |
500
562
 
501
- See [docs/reference/cli.md](docs/reference/cli.md) for the complete option
502
- reference.
563
+ See the [AZM Engineering Manual](docs/codebase/) for the maintained codebase,
564
+ CLI and package-interface reference.
503
565
 
504
566
  ## Output Artifacts
505
567
 
@@ -618,8 +680,8 @@ Public tooling types include `Diagnostic`, `LoadedProgram`,
618
680
  `AnalyzeProgramResult`, `LoadProgramResult`, `RegisterContractsCandidateDiagnostic`
619
681
  and the Debug80 map artifact types `D8mArtifact`, `D8mJson` and `D8mSymbol`.
620
682
 
621
- See [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for current
622
- API notes.
683
+ See the [public surface reference](docs/codebase/appendices/c-public-surface-reference.md)
684
+ for current API notes.
623
685
 
624
686
  ## Development
625
687
 
@@ -634,8 +696,8 @@ npm run test:azm:corpus
634
696
  npm test
635
697
  ```
636
698
 
637
- The live source map is maintained in
638
- [docs/reference/source-overview.md](docs/reference/source-overview.md).
699
+ The live source map is maintained in the
700
+ [AZM Engineering Manual](docs/codebase/).
639
701
 
640
702
  ## License
641
703
 
@@ -59,6 +59,10 @@ export async function compile(entryFile, options = {}, deps = { formats: default
59
59
  ? !isSuppressedUnknownSymbolInRegisterContractsMode(diagnostic, directCalls)
60
60
  : true));
61
61
  const artifacts = [];
62
+ if (hasErrors(diagnostics)) {
63
+ sortDiagnosticsInPlace(diagnostics);
64
+ return { diagnostics, artifacts };
65
+ }
62
66
  if (analyzeRegisterContractsNow) {
63
67
  const registerContracts = await runRegisterContracts(loaded.loadedProgram, options);
64
68
  artifacts.push(...registerContracts.artifacts);
@@ -97,6 +101,7 @@ function hasErrors(diagnostics) {
97
101
  return diagnostics.some((diagnostic) => diagnostic.severity === 'error');
98
102
  }
99
103
  function sortDiagnosticsInPlace(diagnostics) {
104
+ dedupeDiagnosticsInPlace(diagnostics);
100
105
  diagnostics.sort((left, right) => {
101
106
  const lineDelta = (left.line ?? 0) - (right.line ?? 0);
102
107
  if (lineDelta !== 0) {
@@ -105,3 +110,25 @@ function sortDiagnosticsInPlace(diagnostics) {
105
110
  return (left.column ?? 0) - (right.column ?? 0);
106
111
  });
107
112
  }
113
+ function dedupeDiagnosticsInPlace(diagnostics) {
114
+ const seen = new Set();
115
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
116
+ const diagnostic = diagnostics[index];
117
+ const key = diagnosticKey(diagnostic);
118
+ if (seen.has(key)) {
119
+ diagnostics.splice(index, 1);
120
+ continue;
121
+ }
122
+ seen.add(key);
123
+ }
124
+ }
125
+ function diagnosticKey(diagnostic) {
126
+ return [
127
+ diagnostic.severity,
128
+ diagnostic.code,
129
+ diagnostic.message,
130
+ diagnostic.sourceName ?? '',
131
+ diagnostic.line ?? '',
132
+ diagnostic.column ?? '',
133
+ ].join('\0');
134
+ }
@@ -1,4 +1,5 @@
1
1
  import { buildAddressState, resolveSymbols } from './address-planning.js';
2
+ import { validateImportVisibility } from './import-visibility.js';
2
3
  import { emitProgramImage } from './program-emission.js';
3
4
  function emptyAssemblyResult(diagnostics, partial = {}) {
4
5
  return {
@@ -13,6 +14,10 @@ function emptyAssemblyResult(diagnostics, partial = {}) {
13
14
  }
14
15
  export function assembleProgram(items) {
15
16
  const diagnostics = [];
17
+ validateImportVisibility(items, diagnostics);
18
+ if (diagnostics.length > 0) {
19
+ return emptyAssemblyResult(diagnostics);
20
+ }
16
21
  const addressState = buildAddressState(items, diagnostics);
17
22
  if (diagnostics.length > 0) {
18
23
  return emptyAssemblyResult(diagnostics, { origin: addressState.origin });
@@ -0,0 +1,3 @@
1
+ import type { Diagnostic } from '../model/diagnostic.js';
2
+ import type { SourceItem } from '../model/source-item.js';
3
+ export declare function validateImportVisibility(items: readonly SourceItem[], diagnostics: Diagnostic[]): void;
@@ -0,0 +1,204 @@
1
+ import { diagnostic } from '../semantics/diagnostics.js';
2
+ export function validateImportVisibility(items, diagnostics) {
3
+ const labels = collectLabelVisibility(items);
4
+ for (const item of items) {
5
+ validateItemReferences(item, labels, diagnostics);
6
+ }
7
+ }
8
+ function collectLabelVisibility(items) {
9
+ const labels = new Map();
10
+ const importedSourceUnits = importedUnitNames(items);
11
+ for (const item of items) {
12
+ if (item.kind !== 'label')
13
+ continue;
14
+ labels.set(item.name, {
15
+ name: item.name,
16
+ definingSourceUnit: item.span.sourceUnit,
17
+ definingSourceName: item.span.sourceName,
18
+ public: isPublicLabel(item, importedSourceUnits),
19
+ });
20
+ }
21
+ return labels;
22
+ }
23
+ function importedUnitNames(items) {
24
+ const units = new Set();
25
+ for (const item of items) {
26
+ if (item.span.sourceRelation === 'import' && item.span.sourceUnit !== undefined) {
27
+ units.add(item.span.sourceUnit);
28
+ }
29
+ }
30
+ return units;
31
+ }
32
+ function isPublicLabel(item, importedSourceUnits) {
33
+ return (item.isEntry === true ||
34
+ item.span.sourceUnit === undefined ||
35
+ !importedSourceUnits.has(item.span.sourceUnit));
36
+ }
37
+ function validateItemReferences(item, labels, diagnostics) {
38
+ switch (item.kind) {
39
+ case 'org':
40
+ validateExpression(item.expression, item.span, labels, diagnostics);
41
+ return;
42
+ case 'equ':
43
+ validateExpression(item.expression, item.span, labels, diagnostics);
44
+ return;
45
+ case 'db':
46
+ for (const value of item.values) {
47
+ validateDataValue(value, item.span, labels, diagnostics);
48
+ }
49
+ return;
50
+ case 'dw':
51
+ for (const value of item.values) {
52
+ validateExpression(value, item.span, labels, diagnostics);
53
+ }
54
+ return;
55
+ case 'ds':
56
+ validateExpression(item.size, item.span, labels, diagnostics);
57
+ if (item.fill !== undefined) {
58
+ validateExpression(item.fill, item.span, labels, diagnostics);
59
+ }
60
+ return;
61
+ case 'align':
62
+ validateExpression(item.alignment, item.span, labels, diagnostics);
63
+ return;
64
+ case 'binfrom':
65
+ case 'binto':
66
+ validateExpression(item.expression, item.span, labels, diagnostics);
67
+ return;
68
+ case 'instruction':
69
+ validateInstruction(item.instruction, item.span, labels, diagnostics);
70
+ return;
71
+ case 'label':
72
+ case 'comment':
73
+ case 'end':
74
+ case 'enum':
75
+ case 'type':
76
+ case 'type-alias':
77
+ case 'string-data':
78
+ return;
79
+ }
80
+ }
81
+ function validateDataValue(value, span, labels, diagnostics) {
82
+ if ('kind' in value && value.kind === 'string-fragment')
83
+ return;
84
+ validateExpression(value, span, labels, diagnostics);
85
+ }
86
+ function validateInstruction(instruction, span, labels, diagnostics) {
87
+ for (const expression of instructionExpressions(instruction)) {
88
+ validateExpression(expression, span, labels, diagnostics);
89
+ }
90
+ }
91
+ function instructionExpressions(instruction) {
92
+ switch (instruction.mnemonic) {
93
+ case 'ld-a-imm':
94
+ case 'jp':
95
+ case 'call':
96
+ case 'jr':
97
+ case 'djnz':
98
+ return [instruction.expression];
99
+ case 'jp-cc':
100
+ case 'call-cc':
101
+ case 'jr-cc':
102
+ return [instruction.expression];
103
+ case 'ld':
104
+ return [...operandExpressions(instruction.target), ...operandExpressions(instruction.source)];
105
+ case 'in':
106
+ return instruction.port.kind === 'imm' ? [instruction.port.expression] : [];
107
+ case 'out':
108
+ return instruction.port.kind === 'imm' ? [instruction.port.expression] : [];
109
+ case 'inc':
110
+ case 'dec':
111
+ return 'displacement' in instruction.operand ? [instruction.operand.displacement] : [];
112
+ case 'bit':
113
+ case 'res':
114
+ case 'set':
115
+ case 'rlc':
116
+ case 'rrc':
117
+ case 'rl':
118
+ case 'rr':
119
+ case 'sla':
120
+ case 'sra':
121
+ case 'sll':
122
+ case 'sls':
123
+ case 'srl':
124
+ return 'displacement' in instruction.operand ? [instruction.operand.displacement] : [];
125
+ case 'add':
126
+ if ('source' in instruction && 'target' in instruction) {
127
+ return [
128
+ ...operandExpressions(instruction.target),
129
+ ...operandExpressions(instruction.source),
130
+ ];
131
+ }
132
+ return 'source' in instruction ? operandExpressions(instruction.source) : [];
133
+ case 'adc':
134
+ case 'sbc':
135
+ return 'source' in instruction ? operandExpressions(instruction.source) : [];
136
+ case 'sub':
137
+ case 'and':
138
+ case 'or':
139
+ case 'xor':
140
+ case 'cp':
141
+ return operandExpressions(instruction.source);
142
+ default:
143
+ return [];
144
+ }
145
+ }
146
+ function operandExpressions(operand) {
147
+ switch (operand.kind) {
148
+ case 'mem-abs':
149
+ case 'imm':
150
+ return [operand.expression];
151
+ case 'indexed':
152
+ return [operand.displacement];
153
+ default:
154
+ return [];
155
+ }
156
+ }
157
+ function validateExpression(expression, span, labels, diagnostics) {
158
+ switch (expression.kind) {
159
+ case 'symbol':
160
+ validateSymbolReference(expression.name, span, labels, diagnostics);
161
+ return;
162
+ case 'byte-function':
163
+ case 'unary':
164
+ validateExpression(expression.expression, span, labels, diagnostics);
165
+ return;
166
+ case 'binary':
167
+ validateExpression(expression.left, span, labels, diagnostics);
168
+ validateExpression(expression.right, span, labels, diagnostics);
169
+ return;
170
+ case 'layout-cast':
171
+ validateExpression(expression.base, span, labels, diagnostics);
172
+ for (const part of expression.path) {
173
+ if (part.kind === 'index') {
174
+ validateExpression(part.expression, span, labels, diagnostics);
175
+ }
176
+ }
177
+ return;
178
+ case 'number':
179
+ case 'current-location':
180
+ case 'type-size':
181
+ case 'sizeof':
182
+ case 'offset':
183
+ return;
184
+ }
185
+ }
186
+ function validateSymbolReference(name, referenceSpan, labels, diagnostics) {
187
+ const label = lookupLabel(labels, name);
188
+ if (!label || label.public)
189
+ return;
190
+ if (referenceSpan.sourceUnit === label.definingSourceUnit)
191
+ return;
192
+ diagnostics.push(diagnostic(referenceSpan, `symbol "${name}" is private to ${label.definingSourceName}; export it with @${label.name} or keep the reference inside that file`));
193
+ }
194
+ function lookupLabel(labels, name) {
195
+ const direct = labels.get(name);
196
+ if (direct)
197
+ return direct;
198
+ const lowerName = name.toLowerCase();
199
+ for (const [key, label] of labels) {
200
+ if (key.toLowerCase() === lowerName)
201
+ return label;
202
+ }
203
+ return undefined;
204
+ }
@@ -18,6 +18,7 @@ export async function expandSourceForTooling(options) {
18
18
  }
19
19
  const sourceTexts = new Map();
20
20
  const sourceLineComments = new Map();
21
+ const loadedImports = new Set();
21
22
  const includeDirs = (options.includeDirs ?? []).map((path) => normalize(path));
22
23
  const lines = await expandFile({
23
24
  sourcePath: entryFile,
@@ -25,7 +26,10 @@ export async function expandSourceForTooling(options) {
25
26
  sourceTexts,
26
27
  sourceLineComments,
27
28
  diagnostics,
28
- includeStack: [],
29
+ loadedImports,
30
+ sourceStack: [],
31
+ sourceUnit: entryFile,
32
+ sourceRelation: 'entry',
29
33
  ...(options.preloadedText !== undefined ? { preloadedText: options.preloadedText } : {}),
30
34
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
31
35
  });
@@ -37,11 +41,11 @@ export async function expandSourceForTooling(options) {
37
41
  async function expandFile(options) {
38
42
  options.signal?.throwIfAborted();
39
43
  const sourcePath = normalize(options.sourcePath);
40
- if (options.includeStack.includes(sourcePath)) {
44
+ if (options.sourceStack.some((entry) => entry.sourcePath === sourcePath)) {
41
45
  options.diagnostics.push({
42
46
  severity: 'error',
43
47
  code: 'AZMN_SOURCE',
44
- message: `recursive include: ${sourcePath}`,
48
+ message: `recursive ${options.sourceRelation}: ${sourcePath}`,
45
49
  sourceName: sourcePath,
46
50
  });
47
51
  return undefined;
@@ -54,28 +58,36 @@ async function expandFile(options) {
54
58
  const output = [];
55
59
  for (const line of scanLogicalLines(createSourceFile(sourcePath, text))) {
56
60
  recordLineComment(options.sourceLineComments, line);
57
- const includePath = parseIncludePath(line.text);
58
- if (!includePath) {
59
- output.push(line);
61
+ const directive = parseSourceLoadDirective(line.text);
62
+ if (!directive) {
63
+ output.push(withSourceOwnership(line, options.sourceUnit, options.sourceRelation));
60
64
  continue;
61
65
  }
62
- const result = await resolveInclude(sourcePath, includePath, options.includeDirs);
66
+ const result = await resolveSourcePath(sourcePath, directive.path, options.includeDirs);
63
67
  if (result.resolved === undefined) {
64
68
  options.diagnostics.push({
65
69
  severity: 'error',
66
70
  code: 'AZMN_SOURCE',
67
- message: `Failed to resolve include "${includePath}" from "${sourcePath}". Tried:\n${result.searchCandidates.map((candidate) => `- ${candidate}`).join('\n')}`,
71
+ message: `Failed to resolve ${directive.kind} "${directive.path}" from "${sourcePath}". Tried:\n${result.searchCandidates.map((candidate) => `- ${candidate}`).join('\n')}`,
68
72
  sourceName: sourcePath,
69
73
  line: line.line,
70
74
  column: firstColumn(line.text),
71
75
  });
72
76
  continue;
73
77
  }
78
+ if (directive.kind === 'import' && options.loadedImports.has(result.resolved)) {
79
+ continue;
80
+ }
81
+ if (directive.kind === 'import') {
82
+ options.loadedImports.add(result.resolved);
83
+ }
74
84
  const { preloadedText: _preloadedText, ...includeOptions } = options;
75
85
  const included = await expandFile({
76
86
  ...includeOptions,
77
87
  sourcePath: result.resolved,
78
- includeStack: [...options.includeStack, sourcePath],
88
+ sourceStack: [...options.sourceStack, { sourcePath }],
89
+ sourceUnit: directive.kind === 'import' ? result.resolved : options.sourceUnit,
90
+ sourceRelation: directive.kind,
79
91
  });
80
92
  if (included !== undefined) {
81
93
  output.push(...included);
@@ -83,6 +95,13 @@ async function expandFile(options) {
83
95
  }
84
96
  return output;
85
97
  }
98
+ function withSourceOwnership(line, sourceUnit, sourceRelation) {
99
+ return {
100
+ ...line,
101
+ sourceUnit,
102
+ sourceRelation,
103
+ };
104
+ }
86
105
  function recordLineComment(comments, line) {
87
106
  const commentText = lineComment(line.text);
88
107
  if (commentText === undefined) {
@@ -112,7 +131,7 @@ async function readSourceText(options, sourcePath) {
112
131
  return undefined;
113
132
  }
114
133
  }
115
- async function resolveInclude(importer, includePath, includeDirs) {
134
+ async function resolveSourcePath(importer, includePath, includeDirs) {
116
135
  const candidates = [
117
136
  join(dirname(importer), includePath),
118
137
  ...includeDirs.map((dir) => join(dir, includePath)),
@@ -128,9 +147,17 @@ async function resolveInclude(importer, includePath, includeDirs) {
128
147
  }
129
148
  return { searchCandidates: candidates.map(normalize) };
130
149
  }
131
- function parseIncludePath(text) {
132
- const match = /^\.?include\s+"([^"]+)"\s*$/i.exec(stripLineComment(text).trim());
133
- return match?.[1];
150
+ function parseSourceLoadDirective(text) {
151
+ const trimmed = stripLineComment(text).trim();
152
+ const include = /^\.?include\s+"([^"]+)"\s*$/i.exec(trimmed);
153
+ if (include) {
154
+ return { kind: 'include', path: include[1] ?? '' };
155
+ }
156
+ const sourceImport = /^\.import\s+"([^"]+)"\s*$/.exec(trimmed);
157
+ if (sourceImport) {
158
+ return { kind: 'import', path: sourceImport[1] ?? '' };
159
+ }
160
+ return undefined;
134
161
  }
135
162
  function lineComment(text) {
136
163
  const prefix = stripLineComment(text);
@@ -31,6 +31,10 @@ export class UnsupportedAsm80LoweringError extends Error {
31
31
  }
32
32
  export function writeAsm80(items, symbols, opts = {}) {
33
33
  void opts;
34
+ const importedItem = items.find((item) => item.span.sourceRelation === 'import');
35
+ if (importedItem !== undefined) {
36
+ throw new UnsupportedAsm80LoweringError('lowered .z80 output does not yet support .import source units', importedItem);
37
+ }
34
38
  const evalContext = {
35
39
  constants: collectConstants(symbols),
36
40
  symbols: collectSymbolValues(symbols),
@@ -49,8 +49,8 @@ function flushRoutine(routines, state, constants) {
49
49
  span: routineSpan(state, end),
50
50
  });
51
51
  }
52
- function resetAndStart(routines, state, constants, item) {
53
- flushRoutine(routines, state, constants);
52
+ function resetAndStart(routines, state, context, item) {
53
+ flushRoutine(routines, state, context.constants);
54
54
  Object.assign(state, emptyState());
55
55
  startRoutine(state, item);
56
56
  }
@@ -60,69 +60,85 @@ function appendDirectCall(directCalls, item) {
60
60
  return;
61
61
  pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, item.span.sourceName, item.span.line, item.span.column);
62
62
  }
63
- function handleInstruction(state, directCalls, item, constants) {
63
+ function handleInstruction(state, directCalls, item, context) {
64
64
  if (state.routineName === undefined || state.sourceName === undefined)
65
65
  return;
66
66
  if (item.span.sourceName !== state.sourceName)
67
67
  return;
68
- state.instructions.push(toInstruction(item, state.labels, constants));
68
+ state.instructions.push(toInstruction(item, state.labels, context.constants));
69
69
  appendDirectCall(directCalls, item);
70
70
  }
71
- function handleGlobalLabel(routines, state, item, constants, filesWithEntryLabels) {
71
+ function handleGlobalLabel(routines, state, item, context) {
72
72
  if (state.routineName === undefined) {
73
- if (shouldIgnoreNonEntryLabel(item, filesWithEntryLabels))
73
+ if (shouldIgnoreNonEntryLabel(item, context))
74
74
  return;
75
75
  startRoutine(state, item);
76
76
  return;
77
77
  }
78
78
  if (isDifferentRoutineSource(state, item)) {
79
- resetAndStart(routines, state, constants, item);
79
+ resetAndStart(routines, state, context, item);
80
80
  return;
81
81
  }
82
82
  if (state.instructions.length > 0) {
83
- if (shouldKeepPostInstructionAlias(item, filesWithEntryLabels)) {
83
+ if (shouldKeepPostInstructionAlias(item, context)) {
84
84
  appendRoutineLabel(state, item);
85
85
  return;
86
86
  }
87
- resetAndStart(routines, state, constants, item);
87
+ resetAndStart(routines, state, context, item);
88
88
  return;
89
89
  }
90
90
  appendRoutineLabel(state, item);
91
91
  }
92
- function shouldIgnoreNonEntryLabel(item, filesWithEntryLabels) {
93
- return filesWithEntryLabels.has(item.span.sourceName) && item.isEntry !== true;
92
+ function shouldIgnoreNonEntryLabel(item, context) {
93
+ return (context.filesWithEntryLabels.has(item.span.sourceName) &&
94
+ item.isEntry !== true &&
95
+ !context.directCallTargets.has(item.name));
94
96
  }
95
97
  function isDifferentRoutineSource(state, item) {
96
98
  return state.sourceName === undefined || state.sourceName !== item.span.sourceName;
97
99
  }
98
- function shouldKeepPostInstructionAlias(item, filesWithEntryLabels) {
99
- return shouldIgnoreNonEntryLabel(item, filesWithEntryLabels);
100
+ function shouldKeepPostInstructionAlias(item, context) {
101
+ return shouldIgnoreNonEntryLabel(item, context);
100
102
  }
101
103
  function appendRoutineLabel(state, item) {
102
104
  state.labels.push(item.name);
103
105
  if (item.isEntry === true)
104
106
  state.entryLabels.push(item.name);
105
107
  }
106
- function handleLabel(routines, state, item, constants, filesWithEntryLabels) {
108
+ function handleLabel(routines, state, item, context) {
107
109
  if (!isGlobalLabel(item.name)) {
108
110
  if (state.routineName !== undefined)
109
111
  state.labels.push(item.name);
110
112
  return;
111
113
  }
112
- handleGlobalLabel(routines, state, item, constants, filesWithEntryLabels);
114
+ handleGlobalLabel(routines, state, item, context);
113
115
  }
114
116
  export function buildRoutinesAndDirectCalls(items, constants, filesWithEntryLabels) {
115
117
  const routines = [];
116
118
  const directCalls = [];
119
+ const context = {
120
+ constants,
121
+ filesWithEntryLabels,
122
+ directCallTargets: collectDirectCallTargets(items),
123
+ };
117
124
  const state = emptyState();
118
125
  for (const item of items) {
119
126
  if (item.kind === 'instruction') {
120
- handleInstruction(state, directCalls, item, constants);
127
+ handleInstruction(state, directCalls, item, context);
121
128
  }
122
129
  else if (item.kind === 'label') {
123
- handleLabel(routines, state, item, constants, filesWithEntryLabels);
130
+ handleLabel(routines, state, item, context);
124
131
  }
125
132
  }
126
133
  flushRoutine(routines, state, constants);
127
134
  return { routines, directCalls };
128
135
  }
136
+ function collectDirectCallTargets(items) {
137
+ const targets = new Set();
138
+ for (const item of items) {
139
+ const target = instructionCallTarget(item);
140
+ if (target !== undefined)
141
+ targets.add(target);
142
+ }
143
+ return targets;
144
+ }
@@ -3,5 +3,8 @@ export interface LogicalLine {
3
3
  readonly sourceName: string;
4
4
  readonly line: number;
5
5
  readonly text: string;
6
+ readonly sourceUnit?: string;
7
+ readonly sourceRelation?: SourceRelation;
6
8
  }
9
+ export type SourceRelation = 'entry' | 'include' | 'import';
7
10
  export declare function scanLogicalLines(source: SourceFile): LogicalLine[];
@@ -2,4 +2,6 @@ export interface SourceSpan {
2
2
  readonly sourceName: string;
3
3
  readonly line: number;
4
4
  readonly column: number;
5
+ readonly sourceUnit?: string;
6
+ readonly sourceRelation?: 'entry' | 'include' | 'import';
5
7
  }
@@ -1,14 +1,9 @@
1
1
  import type { LogicalLine } from '../source/logical-lines.js';
2
+ import type { SourceSpan } from '../source/source-span.js';
2
3
  import type { ParseLineResult } from './parse-line.js';
3
- type SourceSpan = {
4
- readonly sourceName: string;
5
- readonly line: number;
6
- readonly column: number;
7
- };
8
4
  export declare function parseDirectiveStatement(line: LogicalLine, text: string, span: SourceSpan): ParseLineResult | undefined;
9
5
  export declare function parseColonDeclaration(line: LogicalLine, name: string, statementText: string, span: {
10
6
  readonly sourceName: string;
11
7
  readonly line: number;
12
8
  readonly column: number;
13
9
  }): ParseLineResult | undefined;
14
- export {};
@@ -144,7 +144,9 @@ function parseDsDirective(line, valueText, span) {
144
144
  };
145
145
  }
146
146
  function validateDsValueList(line, parts) {
147
- return parts.length < 1 || parts.length > 2 ? parseError(line, `invalid .ds value list`) : undefined;
147
+ return parts.length < 1 || parts.length > 2
148
+ ? parseError(line, `invalid .ds value list`)
149
+ : undefined;
148
150
  }
149
151
  function parseDsSize(line, sizeText) {
150
152
  const size = parseTypeSizeExpression(sizeText) ?? parseExpression(sizeText);