@jhlagado/azm 0.2.8 → 0.2.10

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 (34) hide show
  1. package/README.md +75 -15
  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/register-contracts/report.js +15 -3
  10. package/dist/src/register-contracts/smartCommentParsing.d.ts +1 -0
  11. package/dist/src/register-contracts/smartCommentParsing.js +42 -7
  12. package/dist/src/register-contracts/smartComments.d.ts +2 -2
  13. package/dist/src/register-contracts/smartComments.js +3 -4
  14. package/dist/src/source/logical-lines.d.ts +3 -0
  15. package/dist/src/source/source-span.d.ts +2 -0
  16. package/dist/src/syntax/parse-directive-statement.d.ts +1 -6
  17. package/dist/src/syntax/parse-directive-statement.js +3 -1
  18. package/dist/src/syntax/parse-layout-declarations.js +11 -2
  19. package/dist/src/syntax/parse-line.js +18 -2
  20. package/dist/src/tooling/api.js +1 -1
  21. package/docs/codebase/01-orientation-and-repository-layout.md +192 -0
  22. package/docs/codebase/02-source-loading-and-parsing.md +263 -0
  23. package/docs/codebase/03-assembly-and-z80-emission.md +251 -0
  24. package/docs/codebase/04-ops-and-register-contracts.md +237 -0
  25. package/docs/codebase/05-interfaces-and-output-artifacts.md +253 -0
  26. package/docs/codebase/06-verification-and-maintenance.md +202 -0
  27. package/docs/codebase/appendices/a-directory-file-reference.md +253 -0
  28. package/docs/codebase/appendices/b-compile-flow-reference.md +103 -0
  29. package/docs/codebase/appendices/c-public-surface-reference.md +106 -0
  30. package/docs/codebase/appendices/index.md +16 -0
  31. package/docs/codebase/index.md +46 -0
  32. package/package.json +2 -3
  33. package/docs/reference/cli.md +0 -158
  34. package/docs/reference/tooling-api.md +0 -320
package/README.md CHANGED
@@ -120,9 +120,7 @@ Use `@Name:` for callable routine entries. The `@` marks a register contracts
120
120
  routine boundary; call sites still write the symbol name without `@`:
121
121
 
122
122
  ```asm
123
- ;! in A
124
- ;! out A
125
- ;! clobbers BC
123
+ ;! in A; out A; clobbers BC
126
124
  @MxMask:
127
125
  LD C,A
128
126
  OR A
@@ -335,18 +333,78 @@ azm -I include -I vendor program.asm
335
333
  Included source contributes labels, constants, enums, types, ops and routines to
336
334
  the same assembly.
337
335
 
336
+ ## Imports
337
+
338
+ `.import` is AZM's module-style source composition directive:
339
+
340
+ ```asm
341
+ ; main.asm
342
+ .import "keyboard.asm"
343
+
344
+ @Start:
345
+ call ReadKey
346
+ ret
347
+ ```
348
+
349
+ ```asm
350
+ ; keyboard.asm
351
+ @ReadKey:
352
+ call ScanMatrix
353
+ ret
354
+
355
+ ScanMatrix:
356
+ xor a
357
+ ret
358
+ ```
359
+
360
+ Imported source assembles at the point where `.import` appears, so native
361
+ `.bin`, `.hex` and `.d8.json` output contain the imported bytes as part of the
362
+ same program. Paths resolve like includes: relative to the importing file first,
363
+ then through `-I` include directories.
364
+
365
+ The difference is visibility. In an imported file, labels written with `@` are
366
+ public exports. Code outside `keyboard.asm` can call `ReadKey`, using the name
367
+ without `@`. Plain labels in an imported file, such as `ScanMatrix`, are private
368
+ to that imported file or import unit. The imported file may call its own private
369
+ helpers, but outside references fail with a direct visibility diagnostic.
370
+
371
+ `.include` remains textual. Included text belongs to the including source unit
372
+ and is intended for shared constants, declarations and compatibility source.
373
+ Use `.import` when a source file should behave like a module with public `@`
374
+ entry points and private implementation labels.
375
+
376
+ Repeated imports of the same resolved file are idempotent: the first import
377
+ loads and emits the module, later imports of that same file are skipped.
378
+ Repeated includes are still textual and repeat every time. Recursive includes or
379
+ imports are rejected with source diagnostics.
380
+
381
+ Register contracts use the same `@` routine boundaries across imported files.
382
+ Imported public routines are analyzed as internal routines under `--rc strict`,
383
+ and private helpers called inside an imported public routine are summarized as
384
+ part of the same program analysis.
385
+
386
+ ASM80-compatible lowered `.z80` output does not yet support imported source
387
+ units. If `--asm80` is requested for a program that uses `.import`, AZM reports
388
+ an explicit `AZMN_ASM80` diagnostic instead of silently flattening the module
389
+ boundary. Use native `.bin`, `.hex` and `.d8.json` output for imported programs.
390
+
338
391
  ## Register Contracts
339
392
 
340
393
  Register contracts check whether subroutines preserve the register values that their
341
394
  callers still need. It is designed to catch register collisions, a common source
342
395
  of assembly bugs.
343
396
 
397
+ The benefit is practical: AZM can stop a plausible-looking routine at compile
398
+ time when it reads a register after calling code that may clobber it. In larger
399
+ Z80 projects this encourages smaller routines, clearer `@` boundaries, explicit
400
+ helper outputs, and proof or test harnesses that stay honest under
401
+ `--rc strict`. The friction is intentional: strict contracts make hidden
402
+ register and stack assumptions visible before they become debugger sessions.
403
+
344
404
  Routine entry labels start with `@`:
345
405
 
346
406
  ```asm
347
- ;! in A,HL
348
- ;! out carry
349
- ;! clobbers B
407
+ ;! in A,HL; out carry; clobbers B
350
408
  @CheckTile:
351
409
  ld b,(hl)
352
410
  cp b
@@ -361,9 +419,11 @@ name:
361
419
  ```
362
420
 
363
421
  AZMDoc register contract comments use `;!` and may record inputs, outputs,
364
- clobbered registers and preserved registers. `clobbers B` means the routine may
365
- change `B`. `preserves B` means the value that enters in `B` is still present
366
- when the routine returns.
422
+ clobbered registers and preserved registers. Separate clauses on the same line
423
+ with semicolons. Older one-clause-per-line comments are still accepted, but AZM
424
+ generated annotations use the compact single-line form. `clobbers B` means the
425
+ routine may change `B`. `preserves B` means the value that enters in `B` is
426
+ still present when the routine returns.
367
427
 
368
428
  Run the analysis with:
369
429
 
@@ -498,8 +558,8 @@ The main switches are:
498
558
  | `-V, --version` | Print package version. |
499
559
  | `-h, --help` | Print CLI help. |
500
560
 
501
- See [docs/reference/cli.md](docs/reference/cli.md) for the complete option
502
- reference.
561
+ See the [AZM Engineering Manual](docs/codebase/) for the maintained codebase,
562
+ CLI and package-interface reference.
503
563
 
504
564
  ## Output Artifacts
505
565
 
@@ -618,8 +678,8 @@ Public tooling types include `Diagnostic`, `LoadedProgram`,
618
678
  `AnalyzeProgramResult`, `LoadProgramResult`, `RegisterContractsCandidateDiagnostic`
619
679
  and the Debug80 map artifact types `D8mArtifact`, `D8mJson` and `D8mSymbol`.
620
680
 
621
- See [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for current
622
- API notes.
681
+ See the [public surface reference](docs/codebase/appendices/c-public-surface-reference.md)
682
+ for current API notes.
623
683
 
624
684
  ## Development
625
685
 
@@ -634,8 +694,8 @@ npm run test:azm:corpus
634
694
  npm test
635
695
  ```
636
696
 
637
- The live source map is maintained in
638
- [docs/reference/source-overview.md](docs/reference/source-overview.md).
697
+ The live source map is maintained in the
698
+ [AZM Engineering Manual](docs/codebase/).
639
699
 
640
700
  ## License
641
701
 
@@ -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
+ }