@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.
- package/README.md +75 -15
- package/dist/src/api-compile.js +27 -0
- package/dist/src/assembly/assemble-program.js +5 -0
- package/dist/src/assembly/import-visibility.d.ts +3 -0
- package/dist/src/assembly/import-visibility.js +204 -0
- package/dist/src/node/source-host.js +40 -13
- package/dist/src/outputs/write-asm80.js +4 -0
- package/dist/src/register-contracts/programModel-routines.js +33 -17
- package/dist/src/register-contracts/report.js +15 -3
- package/dist/src/register-contracts/smartCommentParsing.d.ts +1 -0
- package/dist/src/register-contracts/smartCommentParsing.js +42 -7
- package/dist/src/register-contracts/smartComments.d.ts +2 -2
- package/dist/src/register-contracts/smartComments.js +3 -4
- package/dist/src/source/logical-lines.d.ts +3 -0
- package/dist/src/source/source-span.d.ts +2 -0
- package/dist/src/syntax/parse-directive-statement.d.ts +1 -6
- package/dist/src/syntax/parse-directive-statement.js +3 -1
- package/dist/src/syntax/parse-layout-declarations.js +11 -2
- package/dist/src/syntax/parse-line.js +18 -2
- package/dist/src/tooling/api.js +1 -1
- package/docs/codebase/01-orientation-and-repository-layout.md +192 -0
- package/docs/codebase/02-source-loading-and-parsing.md +263 -0
- package/docs/codebase/03-assembly-and-z80-emission.md +251 -0
- package/docs/codebase/04-ops-and-register-contracts.md +237 -0
- package/docs/codebase/05-interfaces-and-output-artifacts.md +253 -0
- package/docs/codebase/06-verification-and-maintenance.md +202 -0
- package/docs/codebase/appendices/a-directory-file-reference.md +253 -0
- package/docs/codebase/appendices/b-compile-flow-reference.md +103 -0
- package/docs/codebase/appendices/c-public-surface-reference.md +106 -0
- package/docs/codebase/appendices/index.md +16 -0
- package/docs/codebase/index.md +46 -0
- package/package.json +2 -3
- package/docs/reference/cli.md +0 -158
- 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
|
-
;!
|
|
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.
|
|
365
|
-
|
|
366
|
-
|
|
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 [
|
|
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 [
|
|
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
|
-
[
|
|
697
|
+
The live source map is maintained in the
|
|
698
|
+
[AZM Engineering Manual](docs/codebase/).
|
|
639
699
|
|
|
640
700
|
## License
|
|
641
701
|
|
package/dist/src/api-compile.js
CHANGED
|
@@ -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,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
|
-
|
|
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.
|
|
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
|
|
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
|
|
58
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
-
const
|
|
133
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
71
|
+
function handleGlobalLabel(routines, state, item, context) {
|
|
72
72
|
if (state.routineName === undefined) {
|
|
73
|
-
if (shouldIgnoreNonEntryLabel(item,
|
|
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,
|
|
79
|
+
resetAndStart(routines, state, context, item);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
if (state.instructions.length > 0) {
|
|
83
|
-
if (shouldKeepPostInstructionAlias(item,
|
|
83
|
+
if (shouldKeepPostInstructionAlias(item, context)) {
|
|
84
84
|
appendRoutineLabel(state, item);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
resetAndStart(routines, state,
|
|
87
|
+
resetAndStart(routines, state, context, item);
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
appendRoutineLabel(state, item);
|
|
91
91
|
}
|
|
92
|
-
function shouldIgnoreNonEntryLabel(item,
|
|
93
|
-
return filesWithEntryLabels.has(item.span.sourceName) &&
|
|
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,
|
|
99
|
-
return shouldIgnoreNonEntryLabel(item,
|
|
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,
|
|
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,
|
|
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,
|
|
127
|
+
handleInstruction(state, directCalls, item, context);
|
|
121
128
|
}
|
|
122
129
|
else if (item.kind === 'label') {
|
|
123
|
-
handleLabel(routines, state, item,
|
|
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
|
+
}
|