@jhlagado/azm 0.2.2 → 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 CHANGED
@@ -1,53 +1,14 @@
1
1
  # AZM
2
2
 
3
- AZM is the Z80 assembler used by the Debug80 toolchain. It assembles plain
4
- `.asm` and `.z80` source into machine-code artifacts for hardware, emulators,
5
- and Debug80: Intel HEX, flat binary, Debug80 maps, and optional
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
- The user manual is the AZM book in the Debug80 documentation site:
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` | inferred register-care interface when enabled |
415
+ | `.asmi` | register-care interface when enabled |
112
416
 
113
- The `.z80` output is a generated compatibility artifact for ASM80-style
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
- ```asm
120
- .org 0100H
419
+ `@jhlagado/azm` exposes stable Node entry points for tools. Import from these
420
+ package paths:
121
421
 
122
- @START:
123
- ld a,42
124
- ret
125
- ```
422
+ - `@jhlagado/azm`
423
+ - `@jhlagado/azm/tooling`
424
+ - `@jhlagado/azm/compile`
126
425
 
127
- Compile it:
426
+ Install the package:
128
427
 
129
428
  ```sh
130
- azm --type bin --output build/start.bin start.asm
429
+ npm install @jhlagado/azm
131
430
  ```
132
431
 
133
- ## Programmatic API
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
- `@jhlagado/azm` exposes Node entry points for tools:
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
- - `@jhlagado/azm`
138
- - `@jhlagado/azm/tooling`
139
- - `@jhlagado/azm/compile`
464
+ `loadProgram()` also accepts `preloadedText` for an unsaved editor buffer and
465
+ `signal` for cancellation.
140
466
 
141
- Minimal compile example:
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
 
@@ -26,19 +26,7 @@ export function parseNextSourceItems(lines, options = {}) {
26
26
  if (afterTopLevelEnd && !isPostEndParseAllowed(line.text)) {
27
27
  continue;
28
28
  }
29
- const colonLayoutHeader = /^([A-Za-z_][A-Za-z0-9_]*):\s*\.(type|union)\s*$/.exec(stripLineComment(line.text).trim());
30
- if (colonLayoutHeader) {
31
- const directive = colonLayoutHeader[2] ?? 'type';
32
- diagnostics.push(parseDiagnostic(line, `Use "${colonLayoutHeader[1] ?? ''} .${directive}" for layouts; colon labels mark addresses.`));
33
- const endDirective = directive === 'union' ? '.endunion' : '.endtype';
34
- for (index += 1; index < pendingLines.length; index += 1) {
35
- if (stripLineComment(pendingLines[index].text).trim() === endDirective) {
36
- break;
37
- }
38
- }
39
- continue;
40
- }
41
- const nameLeftTypeAlias = /^([A-Za-z_][A-Za-z0-9_]*)\s+\.typealias\s+(.+)$/.exec(stripLineComment(line.text).trim());
29
+ const nameLeftTypeAlias = /^([A-Za-z_][A-Za-z0-9_]*)(?::\s*|\s+)\.typealias\s+(.+)$/.exec(stripLineComment(line.text).trim());
42
30
  if (nameLeftTypeAlias) {
43
31
  const typeExprText = nameLeftTypeAlias[2] ?? '';
44
32
  const typeExpr = parseTypeExpr(typeExprText);
@@ -60,7 +48,7 @@ export function parseNextSourceItems(lines, options = {}) {
60
48
  diagnostics.push(parseDiagnostic(line, `Use "${typeAlias[1] ?? ''} .typealias ..." for type aliases.`));
61
49
  continue;
62
50
  }
63
- const nameLeftLayoutHeader = /^([A-Za-z_][A-Za-z0-9_]*)\s+\.(type|union)\s*$/.exec(stripLineComment(line.text).trim());
51
+ const nameLeftLayoutHeader = /^([A-Za-z_][A-Za-z0-9_]*)(?::\s*|\s+)\.(type|union)\s*$/.exec(stripLineComment(line.text).trim());
64
52
  const prefixLayoutHeader = /^\.(type|union)\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/.exec(stripLineComment(line.text).trim());
65
53
  if (prefixLayoutHeader) {
66
54
  const directive = prefixLayoutHeader[1] ?? 'type';
@@ -202,11 +190,7 @@ function evaluateConditionalExpression(line, expressionText, equates, locationDe
202
190
  }
203
191
  function recordConditionalEquate(line, equates, locationDependentEquates, directiveAliasPolicy) {
204
192
  const text = normalizeDirectiveAlias(stripLineComment(line.text), directiveAliasPolicy).trim();
205
- const statement = /^(@?[A-Za-z_.$?][A-Za-z0-9_.$?]*):\s*(.+)$/.exec(text);
206
- if (statement && /^\.equ\b/.test(statement[2] ?? '')) {
207
- return;
208
- }
209
- const equ = /^([A-Za-z_.$?][A-Za-z0-9_.$?]*)\s+\.equ\s+(.+)$/.exec(text);
193
+ const equ = /^([A-Za-z_.$?][A-Za-z0-9_.$?]*)(?::\s*|\s+)\.equ\s+(.+)$/.exec(text);
210
194
  if (!equ) {
211
195
  return;
212
196
  }
@@ -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
- const allSummaries = [...summaries, ...profileSummaries];
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 analyzed = shouldBuildOutputCandidates
44
- ? {
45
- conflicts: program.routines.flatMap((routine) => findRegisterCareConflicts(routine, summariesByName, smartComments)),
46
- outputCandidates: findCallerOutputCandidateObservations(program.routines, summariesByName),
47
- }
48
- : { conflicts: [], outputCandidates: [] };
49
- const conflicts = analyzed.conflicts;
50
- const outputCandidates = analyzed.outputCandidates;
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.reads)
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));
@@ -14,9 +14,9 @@ export function parseLogicalLine(line, options = {}) {
14
14
  const labelName = normalizeEntryLabelName(rawLabel);
15
15
  const isEntry = rawLabel.startsWith('@');
16
16
  const statementText = labelWithStatement[2] ?? '';
17
- const invalidDeclaration = parseColonDeclarationError(labelName, statementText);
18
- if (invalidDeclaration) {
19
- return { items: [], diagnostics: [parseError(line, invalidDeclaration)] };
17
+ const declaration = parseColonDeclaration(line, labelName, statementText, span);
18
+ if (declaration) {
19
+ return withLineComment(line, declaration);
20
20
  }
21
21
  const parsedStatement = parseCanonicalStatement(line, statementText, span);
22
22
  return withLineComment(line, {
@@ -254,21 +254,14 @@ function parseCanonicalStatement(line, text, span) {
254
254
  }
255
255
  return { items: [], diagnostics: [parseError(line, `unsupported source line: ${text}`)] };
256
256
  }
257
- function parseColonDeclarationError(name, statementText) {
258
- if (/^\.equ\b/.test(statementText)) {
259
- return `Use "${name} .equ ..." for constants; colon labels mark addresses.`;
260
- }
261
- if (/^\.enum\b/.test(statementText)) {
262
- return `Use "${name} .enum ..." for enums; colon labels mark addresses.`;
263
- }
264
- if (/^\.typealias\b/.test(statementText)) {
265
- return `Use "${name} .typealias ..." for type aliases; colon labels mark addresses.`;
266
- }
267
- if (/^\.type\b/.test(statementText)) {
268
- return `Use "${name} .type" for layouts; colon labels mark addresses.`;
257
+ function parseColonDeclaration(line, name, statementText, span) {
258
+ const equ = /^\.equ\s+(.+)$/.exec(statementText);
259
+ if (equ) {
260
+ return parseEquItem(line, name, equ[1] ?? '', span);
269
261
  }
270
- if (/^\.union\b/.test(statementText)) {
271
- return `Use "${name} .union" for layouts; colon labels mark addresses.`;
262
+ const enumDecl = /^\.enum\s+(.+)$/.exec(statementText);
263
+ if (enumDecl) {
264
+ return parseEnumItem(line, name, enumDecl[1] ?? '', span);
272
265
  }
273
266
  return undefined;
274
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhlagado/azm",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "AZM assembler for the Z80 family (Node.js CLI)",
5
5
  "license": "GPL-3.0-only",
6
6
  "engines": {