@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.
- package/README.md +68 -6
- 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/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
|
@@ -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 [
|
|
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 [
|
|
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
|
-
[
|
|
699
|
+
The live source map is maintained in the
|
|
700
|
+
[AZM Engineering Manual](docs/codebase/).
|
|
639
701
|
|
|
640
702
|
## License
|
|
641
703
|
|
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
|
+
}
|
|
@@ -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[];
|
|
@@ -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
|
|
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);
|