@jhlagado/azm 0.2.3 → 0.2.4
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 +423 -64
- package/dist/src/register-care/analyze.js +62 -11
- package/dist/src/register-care/fix.js +45 -1
- package/dist/src/register-care/liveness.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,14 @@
|
|
|
1
1
|
# AZM
|
|
2
2
|
|
|
3
|
-
AZM is the Z80 assembler used by the Debug80 toolchain. It assembles
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ASM80-compatible lowered source.
|
|
3
|
+
AZM is the Z80 assembler used by the Debug80 toolchain. It assembles `.asm`
|
|
4
|
+
and `.z80` source into Intel HEX, flat binary and Debug80 map artifacts for
|
|
5
|
+
hardware, emulators and Debug80.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
This README is the condensed manual. The full AZM book is on the Debug80
|
|
8
|
+
documentation site:
|
|
9
9
|
|
|
10
10
|
[AZM Assembler Manual](https://jhlagado.github.io/debug80-docs/azm-book/book4/)
|
|
11
11
|
|
|
12
|
-
## What AZM Is
|
|
13
|
-
|
|
14
|
-
AZM is an assembler, not a high-level language or macro preprocessor. Source is
|
|
15
|
-
intended to stay close to the machine: labels, directives, instructions, data,
|
|
16
|
-
register contracts, and generated artifacts remain visible.
|
|
17
|
-
|
|
18
|
-
AZM keeps the parts of the original assembler that matter for real Z80 work:
|
|
19
|
-
|
|
20
|
-
- Z80 instructions with case-insensitive mnemonics and registers
|
|
21
|
-
- case-sensitive labels and symbols
|
|
22
|
-
- global labels, with `@NAME:` labels marking routine entries for register-care
|
|
23
|
-
analysis
|
|
24
|
-
- canonical dotted directives such as `.org`, `.equ`, `.db`, `.dw`, and `.ds`
|
|
25
|
-
- exact compatibility spelling for common undotted directive heads such as
|
|
26
|
-
`ORG`, `EQU`, `DB`, `DW`, and `DS`
|
|
27
|
-
- lowercase, case-sensitive canonical dotted directives; compatibility spellings
|
|
28
|
-
are handled as directive aliases, not as canonical AZM style
|
|
29
|
-
- colon labels are address labels only; declarations use name-left forms such as
|
|
30
|
-
`Name .equ`, `Name .enum`, `Name .type`, `Name .union`, and
|
|
31
|
-
`Name .typealias`
|
|
32
|
-
- textual `.include`
|
|
33
|
-
- conditional source inclusion with lowercase `.if`, `.else`, and `.endif`
|
|
34
|
-
- register-care contracts, AZMDoc comments, and `.asmi` external interfaces
|
|
35
|
-
- `op` definitions for structured inline instruction idioms
|
|
36
|
-
- enums and qualified enum constants
|
|
37
|
-
- `.type` / `.union` layout metadata and `Name .typealias TypeExpr` layout aliases
|
|
38
|
-
- compile-time layout constants such as `sizeof(...)`, `offset(...)`, scalar
|
|
39
|
-
layout sizes, constant-only layout casts, `LSB(...)`, and `MSB(...)`
|
|
40
|
-
- case-sensitive AZM function names with documented spelling, such as
|
|
41
|
-
`sizeof(...)`, `offset(...)`, `LSB(...)`, and `MSB(...)`
|
|
42
|
-
- data directives including `.db`, `.dw`, `.ds`, `.cstr`, `.pstr`, and `.istr`
|
|
43
|
-
- single quotes are character literals; double quotes are strings
|
|
44
|
-
|
|
45
|
-
AZM does not implement text macros, local labels, modules/imports, `func`,
|
|
46
|
-
formal arguments, generated stack frames, runtime structured control flow, typed
|
|
47
|
-
assignment lowering, hidden typed load/store lowering, or named section blocks.
|
|
48
|
-
Those features belong to older high-level ZAX-era code paths, not current AZM
|
|
49
|
-
source.
|
|
50
|
-
|
|
51
12
|
## Install
|
|
52
13
|
|
|
53
14
|
AZM requires Node.js 20 or newer.
|
|
@@ -65,8 +26,299 @@ npm run build
|
|
|
65
26
|
npm run azm -- examples/hello.asm
|
|
66
27
|
```
|
|
67
28
|
|
|
29
|
+
## First Program
|
|
30
|
+
|
|
31
|
+
```asm
|
|
32
|
+
.org $0100
|
|
33
|
+
|
|
34
|
+
@Start:
|
|
35
|
+
ld a,42
|
|
36
|
+
ret
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Assemble it:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
azm start.asm
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`.org` means origin. It sets the assembly address for the bytes that follow.
|
|
46
|
+
`@Start:` is an address label and also a public routine entry for register-care
|
|
47
|
+
analysis. The Z80 instructions assemble at `$0100`.
|
|
48
|
+
|
|
49
|
+
## Source Style
|
|
50
|
+
|
|
51
|
+
AZM source is built from labels, declarations, directives, Z80 instructions,
|
|
52
|
+
data definitions, layout declarations, register-care comments and optional
|
|
53
|
+
inline `op` definitions.
|
|
54
|
+
|
|
55
|
+
Canonical AZM directives are lowercase and dotted:
|
|
56
|
+
|
|
57
|
+
```asm
|
|
58
|
+
.org
|
|
59
|
+
.equ
|
|
60
|
+
.db
|
|
61
|
+
.dw
|
|
62
|
+
.ds
|
|
63
|
+
.field
|
|
64
|
+
.type
|
|
65
|
+
.endtype
|
|
66
|
+
.union
|
|
67
|
+
.endunion
|
|
68
|
+
.typealias
|
|
69
|
+
.enum
|
|
70
|
+
.include
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Z80 instruction mnemonics and registers are case-insensitive. Labels, constants,
|
|
74
|
+
enum names, type names and AZM function names are case-sensitive.
|
|
75
|
+
|
|
76
|
+
Use a colon for address labels:
|
|
77
|
+
|
|
78
|
+
```asm
|
|
79
|
+
Loop:
|
|
80
|
+
djnz Loop
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use name-left declarations for constants, enums, records, unions and type
|
|
84
|
+
aliases:
|
|
85
|
+
|
|
86
|
+
```asm
|
|
87
|
+
COUNT .equ 8
|
|
88
|
+
Colour .enum Red, Green, Blue
|
|
89
|
+
SpriteArray .typealias Sprite[16]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Constants often use upper case with underscores. Labels and routine names are
|
|
93
|
+
clearer in PascalCase or camelCase:
|
|
94
|
+
|
|
95
|
+
```asm
|
|
96
|
+
SCREEN_WIDTH .equ 32
|
|
97
|
+
|
|
98
|
+
DrawSprite:
|
|
99
|
+
ret
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Literals
|
|
103
|
+
|
|
104
|
+
AZM accepts the usual Z80 numeric forms:
|
|
105
|
+
|
|
106
|
+
```asm
|
|
107
|
+
$FF ; hexadecimal
|
|
108
|
+
0FFH ; hexadecimal with trailing H
|
|
109
|
+
%10101010 ; binary
|
|
110
|
+
42 ; decimal
|
|
111
|
+
'A' ; character literal
|
|
112
|
+
"HELLO" ; string literal
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A trailing `H` hexadecimal literal must start with a decimal digit, so `0FFH`
|
|
116
|
+
is hexadecimal 255. Double quotes are used for strings. Single quotes are used
|
|
117
|
+
for character literals.
|
|
118
|
+
|
|
119
|
+
`$` also names the current assembly address when it appears as a bare
|
|
120
|
+
expression term:
|
|
121
|
+
|
|
122
|
+
```asm
|
|
123
|
+
TableStart:
|
|
124
|
+
.db 1,2,3,4
|
|
125
|
+
TableEnd:
|
|
126
|
+
TABLE_SIZE .equ $ - TableStart
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Data and Storage
|
|
130
|
+
|
|
131
|
+
`.db` emits bytes. `.dw` emits 16-bit words in Z80 little-endian order, with the
|
|
132
|
+
least significant byte stored first. `.ds` reserves storage.
|
|
133
|
+
|
|
134
|
+
```asm
|
|
135
|
+
Message:
|
|
136
|
+
.db "READY",0
|
|
137
|
+
|
|
138
|
+
Vector:
|
|
139
|
+
.dw Handler
|
|
140
|
+
|
|
141
|
+
Buffer:
|
|
142
|
+
.ds 32
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
String directives encode common string layouts:
|
|
146
|
+
|
|
147
|
+
```asm
|
|
148
|
+
NameC:
|
|
149
|
+
.cstr "READY" ; C string, terminated by zero
|
|
150
|
+
|
|
151
|
+
NameP:
|
|
152
|
+
.pstr "READY" ; Pascal string, length byte first
|
|
153
|
+
|
|
154
|
+
NameI:
|
|
155
|
+
.istr "READY" ; high bit set on final character
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Layout Types
|
|
159
|
+
|
|
160
|
+
AZM has assembler-time layout declarations for records, unions and arrays. They
|
|
161
|
+
describe byte layout so the assembler can calculate sizes, field offsets and
|
|
162
|
+
structured addresses.
|
|
163
|
+
|
|
164
|
+
Start with explicit fields:
|
|
165
|
+
|
|
166
|
+
```asm
|
|
167
|
+
Sprite .type
|
|
168
|
+
x .field byte
|
|
169
|
+
y .field byte
|
|
170
|
+
tile .field byte
|
|
171
|
+
flags .field byte
|
|
172
|
+
.endtype
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each `.field` receives a layout type expression. `byte` allocates one byte and
|
|
176
|
+
`word` allocates two bytes. Arrays use square brackets:
|
|
177
|
+
|
|
178
|
+
```asm
|
|
179
|
+
Palette .type
|
|
180
|
+
entries .field byte[16]
|
|
181
|
+
.endtype
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`sizeof` gives the byte size of a type expression. `offset` gives the byte
|
|
185
|
+
offset of a field path:
|
|
186
|
+
|
|
187
|
+
```asm
|
|
188
|
+
SPRITE_SIZE .equ sizeof(Sprite)
|
|
189
|
+
FLAGS_OFF .equ offset(Sprite, flags)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Type aliases give a reusable name to a layout expression:
|
|
193
|
+
|
|
194
|
+
```asm
|
|
195
|
+
SpriteArray .typealias Sprite[16]
|
|
196
|
+
|
|
197
|
+
Sprites:
|
|
198
|
+
.ds SpriteArray
|
|
199
|
+
|
|
200
|
+
SPRITES_SIZE .equ sizeof(SpriteArray)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A type alias is transparent. `SpriteArray` means `Sprite[16]` anywhere a layout
|
|
204
|
+
type expression is valid.
|
|
205
|
+
|
|
206
|
+
Layout casts apply a type to an address expression so fields can be addressed by
|
|
207
|
+
name:
|
|
208
|
+
|
|
209
|
+
```asm
|
|
210
|
+
ld hl,<SpriteArray>Sprites[3].flags
|
|
211
|
+
ld a,(<SpriteArray>Sprites[3].tile)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The cast performs assembler-time address calculation. Runtime indexing still
|
|
215
|
+
uses Z80 instructions.
|
|
216
|
+
|
|
217
|
+
## Enums
|
|
218
|
+
|
|
219
|
+
Enums are grouped constants. Members are qualified by the enum name:
|
|
220
|
+
|
|
221
|
+
```asm
|
|
222
|
+
Colour .enum Red, Green, Blue
|
|
223
|
+
|
|
224
|
+
.db Colour.Red
|
|
225
|
+
.db Colour.Green
|
|
226
|
+
.db Colour.Blue
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
In this example `Colour.Red` is `0`, `Colour.Green` is `1` and `Colour.Blue` is
|
|
230
|
+
`2`.
|
|
231
|
+
|
|
232
|
+
## Includes
|
|
233
|
+
|
|
234
|
+
`.include` inserts another source file at the current point:
|
|
235
|
+
|
|
236
|
+
```asm
|
|
237
|
+
.include "hardware.asm"
|
|
238
|
+
.include "sprites.asm"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Include search paths are added with `-I`:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
azm -I include -I vendor program.asm
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Included source contributes labels, constants, enums, types, ops and routines to
|
|
248
|
+
the same assembly.
|
|
249
|
+
|
|
250
|
+
## Register Care
|
|
251
|
+
|
|
252
|
+
Register care checks whether subroutines preserve the register values that their
|
|
253
|
+
callers still need. It is designed to catch register collisions, a common source
|
|
254
|
+
of assembly bugs.
|
|
255
|
+
|
|
256
|
+
Routine entry labels start with `@`:
|
|
257
|
+
|
|
258
|
+
```asm
|
|
259
|
+
;! in A,HL
|
|
260
|
+
;! out carry
|
|
261
|
+
;! clobbers B
|
|
262
|
+
@CheckTile:
|
|
263
|
+
ld b,(hl)
|
|
264
|
+
cp b
|
|
265
|
+
ret
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The label is written as `@CheckTile:` at the routine entry. Calls use the public
|
|
269
|
+
name:
|
|
270
|
+
|
|
271
|
+
```asm
|
|
272
|
+
call CheckTile
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
AZMDoc register-care comments use `;!` and may record inputs, outputs,
|
|
276
|
+
clobbered registers and preserved registers. `clobbers B` means the routine may
|
|
277
|
+
change `B`. `preserves B` means the value that enters in `B` is still present
|
|
278
|
+
when the routine returns.
|
|
279
|
+
|
|
280
|
+
Run the analysis with:
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
azm --rc audit --reg-report program.asm
|
|
284
|
+
azm --rc error --interface monitor.asmi program.asm
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The main modes are `audit`, `warn`, `error` and `strict`. AZM can also emit
|
|
288
|
+
register-care reports and `.asmi` interface files for externally assembled
|
|
289
|
+
routines.
|
|
290
|
+
|
|
291
|
+
## Ops and Aliases
|
|
292
|
+
|
|
293
|
+
`op` definitions name short inline instruction idioms:
|
|
294
|
+
|
|
295
|
+
```asm
|
|
296
|
+
op clear_a()
|
|
297
|
+
xor a
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
clear_a
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The operation expands inline at the use site.
|
|
304
|
+
|
|
305
|
+
AZM also has directive aliases for common legacy source. Native AZM style uses
|
|
306
|
+
lowercase dotted directives such as `.org`, `.equ`, `.db`, `.dw` and `.ds`.
|
|
307
|
+
Legacy source can use familiar undotted directive heads such as `ORG`, `EQU`,
|
|
308
|
+
`DB`, `DW` and `DS`.
|
|
309
|
+
|
|
68
310
|
## Command Line
|
|
69
311
|
|
|
312
|
+
The command form is:
|
|
313
|
+
|
|
314
|
+
```sh
|
|
315
|
+
azm [options] <entry.asm|entry.z80>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The entry file is the final argument. Source entries use `.asm` or `.z80`.
|
|
319
|
+
External register-care interfaces use `.asmi` and are loaded with
|
|
320
|
+
`--interface`.
|
|
321
|
+
|
|
70
322
|
Basic use writes the default artifact set next to the source file:
|
|
71
323
|
|
|
72
324
|
```sh
|
|
@@ -86,13 +338,65 @@ Add include search paths:
|
|
|
86
338
|
azm -I include -I vendor program.asm
|
|
87
339
|
```
|
|
88
340
|
|
|
341
|
+
Normalize Debug80 map source paths against the project root:
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
azm --source-root . --output build/program.hex src/program.asm
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Load project directive aliases:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
azm --aliases azm.aliases.json program.asm
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Suppress selected default artifacts:
|
|
354
|
+
|
|
355
|
+
```sh
|
|
356
|
+
azm --nod8m program.asm
|
|
357
|
+
azm --nobin program.asm
|
|
358
|
+
azm --nohex program.asm
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Generate ASM80-compatible lowered source:
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
azm --asm80 program.asm
|
|
365
|
+
```
|
|
366
|
+
|
|
89
367
|
Run register-care analysis:
|
|
90
368
|
|
|
91
369
|
```sh
|
|
92
370
|
azm --rc audit --reg-report program.asm
|
|
93
371
|
azm --rc error --interface monitor.asmi program.asm
|
|
372
|
+
azm --contracts --rc audit program.asm
|
|
94
373
|
```
|
|
95
374
|
|
|
375
|
+
The main switches are:
|
|
376
|
+
|
|
377
|
+
| Option | Meaning |
|
|
378
|
+
| --------------------------------------------- | ------------------------------------------------------------- |
|
|
379
|
+
| `-o, --output <file>` | Primary output path. The extension matches `--type`. |
|
|
380
|
+
| `-t, --type <hex\|bin>` | Primary output type. Default: `hex`. |
|
|
381
|
+
| `--nobin` | Skip `.bin` output. |
|
|
382
|
+
| `--nohex` | Skip `.hex` output. |
|
|
383
|
+
| `--nod8m` | Skip `.d8.json` output. |
|
|
384
|
+
| `--asm80` | Write lowered assembler source as `.z80`. |
|
|
385
|
+
| `--source-root <dir>` | Emit project-relative source paths in `.d8.json`. |
|
|
386
|
+
| `--case-style <mode>` | Lint mnemonic, register and op-head case style. |
|
|
387
|
+
| `--rc, --register-care <mode>` | Register-care mode: `off`, `audit`, `warn`, `error`, `strict`. |
|
|
388
|
+
| `--reg-report, --emit-register-report` | Write `.regcare.txt`. |
|
|
389
|
+
| `--reg-interface, --emit-register-interface` | Write inferred `.asmi` interface metadata. |
|
|
390
|
+
| `--contracts, --annotate-register-contracts` | Update AZMDoc contract comments in source. |
|
|
391
|
+
| `--fix` | Apply conservative register-care source fixes. |
|
|
392
|
+
| `--accept-out <routine:carrier>` | Promote an inferred output candidate while annotating. |
|
|
393
|
+
| `--interface <file>` | Load external register-care contracts from `.asmi`. |
|
|
394
|
+
| `--reg-profile, --register-profile <profile>` | Register-care profile. Currently `mon3`. |
|
|
395
|
+
| `--aliases <file>` | Load project directive alias JSON. |
|
|
396
|
+
| `-I, --include <dir>` | Add an include search path. |
|
|
397
|
+
| `-V, --version` | Print package version. |
|
|
398
|
+
| `-h, --help` | Print CLI help. |
|
|
399
|
+
|
|
96
400
|
See [docs/reference/cli.md](docs/reference/cli.md) for the complete option
|
|
97
401
|
reference.
|
|
98
402
|
|
|
@@ -108,37 +412,60 @@ using the same base path.
|
|
|
108
412
|
| `.d8.json` | Debug80 map |
|
|
109
413
|
| `.z80` | ASM80-compatible lowered source when enabled |
|
|
110
414
|
| `.regcare.txt` | register-care report when enabled |
|
|
111
|
-
| `.asmi` |
|
|
415
|
+
| `.asmi` | register-care interface when enabled |
|
|
112
416
|
|
|
113
|
-
|
|
114
|
-
workflows and comparison tooling. BIN, HEX, Debug80 maps, and
|
|
115
|
-
register-care reports are the normal production outputs.
|
|
116
|
-
|
|
117
|
-
## Small Example
|
|
417
|
+
## Programmatic API
|
|
118
418
|
|
|
119
|
-
|
|
120
|
-
|
|
419
|
+
`@jhlagado/azm` exposes stable Node entry points for tools. Import from these
|
|
420
|
+
package paths:
|
|
121
421
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```
|
|
422
|
+
- `@jhlagado/azm`
|
|
423
|
+
- `@jhlagado/azm/tooling`
|
|
424
|
+
- `@jhlagado/azm/compile`
|
|
126
425
|
|
|
127
|
-
|
|
426
|
+
Install the package:
|
|
128
427
|
|
|
129
428
|
```sh
|
|
130
|
-
|
|
429
|
+
npm install @jhlagado/azm
|
|
131
430
|
```
|
|
132
431
|
|
|
133
|
-
|
|
432
|
+
Use `@jhlagado/azm/tooling` when an editor, linter or debugger integration
|
|
433
|
+
needs parsing, diagnostics, symbols, semantic checks or register-care facts in
|
|
434
|
+
memory:
|
|
134
435
|
|
|
135
|
-
|
|
436
|
+
```ts
|
|
437
|
+
import {
|
|
438
|
+
analyzeProgram,
|
|
439
|
+
analyzeRegisterCareForTools,
|
|
440
|
+
loadProgram,
|
|
441
|
+
} from '@jhlagado/azm/tooling';
|
|
442
|
+
|
|
443
|
+
const loaded = await loadProgram({
|
|
444
|
+
entryFile: '/abs/path/to/main.asm',
|
|
445
|
+
includeDirs: ['/abs/path/to/includes'],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (loaded.loadedProgram) {
|
|
449
|
+
const analysis = analyzeProgram(loaded.loadedProgram, {
|
|
450
|
+
caseStyle: 'consistent',
|
|
451
|
+
requireMain: false,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const registerCare = analyzeRegisterCareForTools(loaded.loadedProgram, {
|
|
455
|
+
mode: 'audit',
|
|
456
|
+
registerCareProfile: 'mon3',
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
console.log(analysis.diagnostics);
|
|
460
|
+
console.log(registerCare.candidateDiagnostics);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
136
463
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- `@jhlagado/azm/compile`
|
|
464
|
+
`loadProgram()` also accepts `preloadedText` for an unsaved editor buffer and
|
|
465
|
+
`signal` for cancellation.
|
|
140
466
|
|
|
141
|
-
|
|
467
|
+
Use `@jhlagado/azm/compile` when a tool needs assembled bytes, Intel HEX,
|
|
468
|
+
Debug80 map data or other artifacts in memory:
|
|
142
469
|
|
|
143
470
|
```ts
|
|
144
471
|
import { compile, defaultFormatWriters } from '@jhlagado/azm/compile';
|
|
@@ -146,18 +473,50 @@ import { compile, defaultFormatWriters } from '@jhlagado/azm/compile';
|
|
|
146
473
|
const result = await compile(
|
|
147
474
|
'/abs/path/to/main.asm',
|
|
148
475
|
{
|
|
476
|
+
includeDirs: ['/abs/path/to/includes'],
|
|
149
477
|
outputType: 'hex',
|
|
478
|
+
emitBin: true,
|
|
479
|
+
emitHex: true,
|
|
480
|
+
emitD8m: true,
|
|
150
481
|
sourceRoot: '/abs/path/to/project',
|
|
151
482
|
d8mInputs: {
|
|
152
483
|
hex: '/abs/path/to/project/build/main.hex',
|
|
484
|
+
bin: '/abs/path/to/project/build/main.bin',
|
|
153
485
|
},
|
|
486
|
+
registerCare: 'audit',
|
|
487
|
+
registerCareInterfaces: ['/abs/path/to/monitor.asmi'],
|
|
154
488
|
},
|
|
155
489
|
{ formats: defaultFormatWriters },
|
|
156
490
|
);
|
|
157
491
|
|
|
158
492
|
console.log(result.diagnostics);
|
|
493
|
+
|
|
494
|
+
const d8m = result.artifacts.find((artifact) => artifact.kind === 'd8m');
|
|
495
|
+
const binary = result.artifacts.find((artifact) => artifact.kind === 'bin');
|
|
496
|
+
console.log(d8m, binary);
|
|
159
497
|
```
|
|
160
498
|
|
|
499
|
+
The compile API returns artifacts in memory. The CLI uses the same writers to
|
|
500
|
+
write those artifacts to disk.
|
|
501
|
+
|
|
502
|
+
Common programmatic options include:
|
|
503
|
+
|
|
504
|
+
| Option | Use |
|
|
505
|
+
| ---------------------------- | --------------------------------------------------------- |
|
|
506
|
+
| `includeDirs` | Include search paths, like repeated `-I`. |
|
|
507
|
+
| `directiveAliasFiles` | Project directive alias JSON files. |
|
|
508
|
+
| `sourceRoot` | Stable project-relative paths in Debug80 maps. |
|
|
509
|
+
| `d8mInputs` | Intended artifact paths recorded in Debug80 map metadata. |
|
|
510
|
+
| `outputType` | Primary output type, `hex` or `bin`. |
|
|
511
|
+
| `emitBin`, `emitHex`, `emitD8m` | Select in-memory artifact kinds. |
|
|
512
|
+
| `emitAsm80` | Request lowered `.z80` artifact. |
|
|
513
|
+
| `registerCare` | Register-care mode. |
|
|
514
|
+
| `registerCareInterfaces` | External `.asmi` contract files. |
|
|
515
|
+
|
|
516
|
+
Public tooling types include `Diagnostic`, `LoadedProgram`,
|
|
517
|
+
`AnalyzeProgramResult`, `LoadProgramResult`, `RegisterCareCandidateDiagnostic`
|
|
518
|
+
and the Debug80 map artifact types `D8mArtifact`, `D8mJson` and `D8mSymbol`.
|
|
519
|
+
|
|
161
520
|
See [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for current
|
|
162
521
|
API notes.
|
|
163
522
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildRegisterCareProgramModel } from './programModel.js';
|
|
2
2
|
import { buildRoutineContracts, parseSmartComments } from './smartComments.js';
|
|
3
3
|
import { renderRegisterCareInterface, renderRegisterCareReport } from './report.js';
|
|
4
|
-
import { autoFixableCandidateKeys } from './fix.js';
|
|
4
|
+
import { autoFixableCandidateKeys, findExpectOutFixesForCandidates } from './fix.js';
|
|
5
5
|
import { findCallerOutputCandidateObservations, findRegisterCareConflicts, } from './liveness.js';
|
|
6
6
|
import { buildAnnotations } from './annotations.js';
|
|
7
7
|
import { buildOutputCandidateFixability, buildProfileSummaries, buildProfileSummaryLookup, buildSummaries, buildSummaryByName, outputCandidateKey, routineNames, unknownBoundaryDiagnostics, unknownCallList, withAcceptedOutputs, } from './summaries.js';
|
|
@@ -27,8 +27,7 @@ export function analyzeRegisterCare(loaded, options) {
|
|
|
27
27
|
const profileSummaries = buildProfileSummaries(options.registerCareProfile);
|
|
28
28
|
let summaries = buildSummaries(program.routines, contractMap, profileSummaries);
|
|
29
29
|
summaries = withAcceptedOutputs(summaries, options.acceptedOutputCandidates);
|
|
30
|
-
|
|
31
|
-
const summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
|
|
30
|
+
let summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
|
|
32
31
|
const knownRoutines = new Set(routineNames(program.routines));
|
|
33
32
|
for (const [name] of contractMap) {
|
|
34
33
|
knownRoutines.add(name);
|
|
@@ -40,14 +39,18 @@ export function analyzeRegisterCare(loaded, options) {
|
|
|
40
39
|
const shouldBuildOutputCandidates = options.mode !== 'off' ||
|
|
41
40
|
options.emitAnnotations === true ||
|
|
42
41
|
options.fixRegisterContracts === true;
|
|
43
|
-
const
|
|
44
|
-
?
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
const outputCandidates = shouldBuildOutputCandidates
|
|
43
|
+
? findCallerOutputCandidateObservations(program.routines, summariesByName)
|
|
44
|
+
: [];
|
|
45
|
+
const autoAcceptedOutputs = autoAcceptedOutputCandidateMap(program.routines, outputCandidates, loaded.sourceTexts);
|
|
46
|
+
if (autoAcceptedOutputs.size > 0) {
|
|
47
|
+
summaries = withAcceptedOutputs(summaries, autoAcceptedOutputs);
|
|
48
|
+
summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
|
|
49
|
+
}
|
|
50
|
+
const allSummaries = [...summaries, ...profileSummaries];
|
|
51
|
+
const conflicts = shouldBuildOutputCandidates
|
|
52
|
+
? program.routines.flatMap((routine) => findRegisterCareConflicts(routine, summariesByName, smartComments))
|
|
53
|
+
: [];
|
|
51
54
|
const outputCandidateFixability = buildOutputCandidateFixability(program.routines, outputCandidates, autoFixableCandidateKeys);
|
|
52
55
|
const outputCandidatesWithFixability = outputCandidates.map((candidate) => {
|
|
53
56
|
const autoFixable = outputCandidateFixability.get(outputCandidateKey(candidate.file, candidate.line, candidate.column)) ?? false;
|
|
@@ -113,3 +116,51 @@ export function analyzeRegisterCare(loaded, options) {
|
|
|
113
116
|
...(reportModel.unknownCalls.length > 0 ? { unknownCalls: reportModel.unknownCalls } : {}),
|
|
114
117
|
};
|
|
115
118
|
}
|
|
119
|
+
function autoAcceptedOutputCandidateMap(routines, outputCandidates, sourceTexts) {
|
|
120
|
+
const out = new Map();
|
|
121
|
+
const sourceMaybeOut = sourceMaybeOutByRoutine(routines, sourceTexts);
|
|
122
|
+
for (const fix of findExpectOutFixesForCandidates([...routines], [...outputCandidates])) {
|
|
123
|
+
const declaredMaybeOut = sourceMaybeOut.get(fix.routine) ?? [];
|
|
124
|
+
const eligibleCarriers = fix.carriers.filter((carrier) => declaredMaybeOut.includes(carrier));
|
|
125
|
+
if (eligibleCarriers.length === 0)
|
|
126
|
+
continue;
|
|
127
|
+
const carriers = out.get(fix.routine) ?? [];
|
|
128
|
+
for (const carrier of eligibleCarriers) {
|
|
129
|
+
if (!carriers.includes(carrier))
|
|
130
|
+
carriers.push(carrier);
|
|
131
|
+
}
|
|
132
|
+
out.set(fix.routine, carriers);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function sourceMaybeOutByRoutine(routines, sourceTexts) {
|
|
137
|
+
const out = new Map();
|
|
138
|
+
for (const routine of routines) {
|
|
139
|
+
const source = sourceTexts.get(routine.span.file);
|
|
140
|
+
if (source === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
const lines = source.split(/\r?\n/);
|
|
143
|
+
const units = [];
|
|
144
|
+
for (let index = routine.span.start.line - 2; index >= 0; index -= 1) {
|
|
145
|
+
const text = lines[index] ?? '';
|
|
146
|
+
if (!/^\s*;/.test(text))
|
|
147
|
+
break;
|
|
148
|
+
const match = /^\s*;\s*!\s*maybe-out\s+(.+)$/i.exec(text);
|
|
149
|
+
if (!match)
|
|
150
|
+
continue;
|
|
151
|
+
for (const token of match[1].split(',')) {
|
|
152
|
+
const unit = token.trim();
|
|
153
|
+
if (unit.length > 0 && !units.includes(unit))
|
|
154
|
+
units.push(unit);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (units.length === 0)
|
|
158
|
+
continue;
|
|
159
|
+
out.set(routine.name, units);
|
|
160
|
+
for (const label of routine.labels)
|
|
161
|
+
out.set(label, units);
|
|
162
|
+
for (const label of routine.entryLabels)
|
|
163
|
+
out.set(label, units);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
@@ -11,6 +11,50 @@ function isUnconditionalDirectCall(item) {
|
|
|
11
11
|
effect.control.target !== undefined &&
|
|
12
12
|
!effect.control.conditional);
|
|
13
13
|
}
|
|
14
|
+
function operandReadsUnit(operand, unit) {
|
|
15
|
+
switch (operand.kind) {
|
|
16
|
+
case 'reg8':
|
|
17
|
+
return operand.register?.toLowerCase() === unit.toLowerCase();
|
|
18
|
+
case 'reg16':
|
|
19
|
+
case 'reg-index16': {
|
|
20
|
+
return operand.register !== undefined && registerNameReadsUnit(operand.register, unit);
|
|
21
|
+
}
|
|
22
|
+
case 'reg-half-index':
|
|
23
|
+
return operand.register?.toLowerCase() === unit.toLowerCase();
|
|
24
|
+
case 'reg-indirect':
|
|
25
|
+
case 'indexed':
|
|
26
|
+
return operand.register !== undefined && registerNameReadsUnit(operand.register, unit);
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function registerNameReadsUnit(registerName, unit) {
|
|
32
|
+
const register = registerName.toLowerCase();
|
|
33
|
+
return ((register === 'bc' && (unit === 'B' || unit === 'C')) ||
|
|
34
|
+
(register === 'de' && (unit === 'D' || unit === 'E')) ||
|
|
35
|
+
(register === 'hl' && (unit === 'H' || unit === 'L')) ||
|
|
36
|
+
(register === 'ix' && (unit === 'IXH' || unit === 'IXL')) ||
|
|
37
|
+
(register === 'iy' && (unit === 'IYH' || unit === 'IYL')) ||
|
|
38
|
+
(register === 'sp' && (unit === 'SPH' || unit === 'SPL')) ||
|
|
39
|
+
(register === 'af' && unit === 'A'));
|
|
40
|
+
}
|
|
41
|
+
function instructionDataReadsUnit(instruction, unit) {
|
|
42
|
+
switch (instruction.mnemonic) {
|
|
43
|
+
case 'ld':
|
|
44
|
+
return operandReadsUnit(instruction.source, unit);
|
|
45
|
+
case 'push':
|
|
46
|
+
return registerNameReadsUnit(instruction.register, unit);
|
|
47
|
+
case 'out':
|
|
48
|
+
if (instruction.source.kind === 'zero')
|
|
49
|
+
return false;
|
|
50
|
+
return operandReadsUnit(instruction.source, unit);
|
|
51
|
+
case 'inc':
|
|
52
|
+
case 'dec':
|
|
53
|
+
return operandReadsUnit(instruction.operand, unit);
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
14
58
|
function continuationReads(routine, callIndex, carriers) {
|
|
15
59
|
const labels = labelIndex(routine);
|
|
16
60
|
const confirmed = new Set();
|
|
@@ -37,7 +81,7 @@ function continuationReads(routine, callIndex, carriers) {
|
|
|
37
81
|
const writes = new Set(effect.writes);
|
|
38
82
|
const remaining = [];
|
|
39
83
|
for (const unit of pending) {
|
|
40
|
-
if (reads.has(unit)) {
|
|
84
|
+
if (reads.has(unit) && instructionDataReadsUnit(item.instruction, unit)) {
|
|
41
85
|
confirmed.add(unit);
|
|
42
86
|
continue;
|
|
43
87
|
}
|
|
@@ -88,10 +88,29 @@ function transferLiveBefore(item, effect, boundary, summary, liveAfter, hints) {
|
|
|
88
88
|
for (const unit of effect.writes)
|
|
89
89
|
live.delete(unit);
|
|
90
90
|
}
|
|
91
|
-
for (const unit of effect
|
|
91
|
+
for (const unit of semanticReadsForLiveness(item, effect, liveAfter))
|
|
92
92
|
live.add(unit);
|
|
93
93
|
return live;
|
|
94
94
|
}
|
|
95
|
+
function semanticReadsForLiveness(item, effect, liveAfter) {
|
|
96
|
+
if (item.instruction.mnemonic === 'or' &&
|
|
97
|
+
item.instruction.source.kind === 'reg8' &&
|
|
98
|
+
item.instruction.source.register === 'a' &&
|
|
99
|
+
!liveAfter.has('zero') &&
|
|
100
|
+
!liveAfter.has('sign') &&
|
|
101
|
+
!liveAfter.has('parity')) {
|
|
102
|
+
return effect.reads.filter((unit) => unit !== 'A');
|
|
103
|
+
}
|
|
104
|
+
if (item.instruction.mnemonic === 'and' &&
|
|
105
|
+
item.instruction.source.kind === 'reg8' &&
|
|
106
|
+
item.instruction.source.register === 'a' &&
|
|
107
|
+
!liveAfter.has('zero') &&
|
|
108
|
+
!liveAfter.has('sign') &&
|
|
109
|
+
!liveAfter.has('parity')) {
|
|
110
|
+
return effect.reads.filter((unit) => unit !== 'A');
|
|
111
|
+
}
|
|
112
|
+
return effect.reads;
|
|
113
|
+
}
|
|
95
114
|
function liveSetsForRoutine(routine, summaries, hints = []) {
|
|
96
115
|
const labels = labelIndex(routine);
|
|
97
116
|
const effects = routine.instructions.map((item) => getZ80InstructionEffect(item.instruction));
|