@jhlagado/azm 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +95 -70
  2. package/dist/src/api-compile.js +1 -1
  3. package/dist/src/assembly/address-planning.js +2 -0
  4. package/dist/src/assembly/program-emission.js +1 -0
  5. package/dist/src/expansion/op-expansion.js +1 -0
  6. package/dist/src/model/source-item.d.ts +6 -0
  7. package/dist/src/outputs/write-asm80.js +122 -5
  8. package/dist/src/register-care/analyze.js +36 -8
  9. package/dist/src/register-care/annotate.d.ts +11 -0
  10. package/dist/src/register-care/annotate.js +76 -0
  11. package/dist/src/register-care/annotations.js +33 -146
  12. package/dist/src/register-care/fix.d.ts +2 -0
  13. package/dist/src/register-care/fix.js +52 -0
  14. package/dist/src/register-care/instruction-shape.d.ts +11 -0
  15. package/dist/src/register-care/instruction-shape.js +129 -0
  16. package/dist/src/register-care/liveness.js +15 -7
  17. package/dist/src/register-care/profiles.js +4 -0
  18. package/dist/src/register-care/programModel.js +79 -13
  19. package/dist/src/register-care/report.d.ts +2 -1
  20. package/dist/src/register-care/report.js +91 -34
  21. package/dist/src/register-care/routine-summaries.d.ts +6 -0
  22. package/dist/src/register-care/routine-summaries.js +89 -0
  23. package/dist/src/register-care/sourceText.d.ts +8 -0
  24. package/dist/src/register-care/sourceText.js +15 -0
  25. package/dist/src/register-care/summaries.d.ts +3 -3
  26. package/dist/src/register-care/summaries.js +42 -75
  27. package/dist/src/register-care/summary.d.ts +3 -0
  28. package/dist/src/register-care/summary.js +474 -0
  29. package/dist/src/register-care/types.d.ts +6 -1
  30. package/dist/src/source/strip-line-comment.d.ts +2 -0
  31. package/dist/src/source/strip-line-comment.js +26 -0
  32. package/dist/src/syntax/parse-diagnostics.d.ts +12 -0
  33. package/dist/src/syntax/parse-diagnostics.js +18 -0
  34. package/dist/src/syntax/parse-line.js +63 -10
  35. package/docs/reference/tooling-api.md +13 -6
  36. package/package.json +4 -2
package/README.md CHANGED
@@ -1,47 +1,54 @@
1
1
  # AZM
2
2
 
3
- AZM is a Z80 assembler in the ASM80 tradition: plain assembly input, predictable
4
- object output, and modern safety tooling for projects that still want to see the
5
- machine.
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, listings, Debug80 maps, and optional
6
+ ASM80-compatible lowered source.
6
7
 
7
- The project goal is a good assembler, not a high-level language. AZM keeps
8
- labels, directives, instructions, branches, data bytes, register effects, and
9
- generated metadata visible in source and artifacts.
8
+ The user manual is the AZM book in the Debug80 documentation site:
10
9
 
11
- ## Product boundary
10
+ [AZM Assembler Manual](https://jhlagado.github.io/debug80-docs/azm-book/book4/)
12
11
 
13
- AZM keeps:
12
+ ## What AZM Is
14
13
 
15
- - ASM80-style `.asm` / `.z80` source as the input baseline
16
- - `.asmi` external interface files for register-care contracts
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
+ - compatibility spelling for common undotted directive heads such as `ORG`,
26
+ `EQU`, `DB`, `DW`, and `DS`
17
27
  - textual `.include`
18
- - directive aliases for importing common assembler spellings
19
- - register-care analysis, compact AZMDoc comments, and `.asmi` external
20
- contracts
21
- - AST-level `op` extensions
22
- - enums as constant namespaces
28
+ - register-care contracts, AZMDoc comments, and `.asmi` external interfaces
29
+ - `op` definitions for structured inline instruction idioms
30
+ - enums and qualified enum constants
23
31
  - `.type` / `.union` layout metadata
24
32
  - compile-time layout constants such as `sizeof(...)`, `offset(...)`, scalar
25
33
  layout sizes, and constant-only layout casts
26
- - assembler data directives including `.db`, `.dw`, `.ds`, `.cstr`, `.pstr`,
27
- and `.istr`
34
+ - data directives including `.db`, `.dw`, `.ds`, `.cstr`, `.pstr`, and `.istr`
28
35
 
29
- AZM `.asm` and `.z80` source rejects old ZAX high-level features such as
30
- modules/imports, `func`, formal arguments, locals, typed assignment/storage
31
- lowering, structured control, generated frames, typed storage blocks, and named
32
- section blocks. Those inherited paths are removal work, not product
33
- compatibility.
36
+ AZM does not implement text macros, local labels, modules/imports, `func`,
37
+ formal arguments, generated stack frames, structured control flow, typed
38
+ assignment lowering, hidden typed load/store lowering, or named section blocks.
39
+ Those features belong to older high-level ZAX-era code paths, not current AZM
40
+ source.
34
41
 
35
42
  ## Install
36
43
 
37
- Requires Node.js 20+.
44
+ AZM requires Node.js 20 or newer.
38
45
 
39
46
  ```sh
40
47
  npm install -g @jhlagado/azm
41
- azm path/to/program.z80
48
+ azm path/to/program.asm
42
49
  ```
43
50
 
44
- From a checkout, use the local CLI after building:
51
+ From a checkout, build first and then use the local CLI:
45
52
 
46
53
  ```sh
47
54
  npm ci
@@ -49,56 +56,70 @@ npm run build
49
56
  npm run azm -- examples/hello.asm
50
57
  ```
51
58
 
52
- Output files for each compiled source:
59
+ ## Command Line
53
60
 
54
- | Extension | Contents |
55
- | ---------- | ------------------------- |
56
- | `.hex` | Intel HEX |
57
- | `.bin` | Flat binary |
58
- | `.lst` | Byte dump plus symbols |
59
- | `.z80` | Plain Z80 source emission |
60
- | `.d8.json` | Debug80 map |
61
+ Basic use writes the default artifact set next to the source file:
61
62
 
62
- Small input example:
63
+ ```sh
64
+ azm program.asm
65
+ ```
63
66
 
64
- ```asm
65
- ORG 0100H
66
- START:
67
- LD A,42
68
- RET
67
+ Write a specific primary output:
68
+
69
+ ```sh
70
+ azm --type bin --output build/program.bin program.asm
71
+ azm --type hex --output build/program.hex program.asm
69
72
  ```
70
73
 
71
- Compile a binary and listing:
74
+ Add include search paths:
72
75
 
73
76
  ```sh
74
- azm --type bin --output build/start.bin start.asm
77
+ azm -I include -I vendor program.asm
78
+ ```
79
+
80
+ Run register-care analysis:
81
+
82
+ ```sh
83
+ azm --rc audit --reg-report program.asm
84
+ azm --rc error --interface monitor.asmi program.asm
85
+ ```
86
+
87
+ See [docs/reference/cli.md](docs/reference/cli.md) for the complete option
88
+ reference.
89
+
90
+ ## Output Artifacts
91
+
92
+ By default, AZM writes the requested primary output plus useful side artifacts
93
+ using the same base path.
94
+
95
+ | Extension | Contents |
96
+ | -------------- | --------------------------------------------- |
97
+ | `.hex` | Intel HEX |
98
+ | `.bin` | flat binary |
99
+ | `.lst` | listing with bytes and symbols |
100
+ | `.d8.json` | Debug80 map |
101
+ | `.z80` | ASM80-compatible lowered source when enabled |
102
+ | `.regcare.txt` | register-care report when enabled |
103
+ | `.asmi` | inferred register-care interface when enabled |
104
+
105
+ The `.z80` output is a generated compatibility artifact for ASM80-style
106
+ workflows and comparison tooling. BIN, HEX, listings, Debug80 maps, and
107
+ register-care reports are the normal production outputs.
108
+
109
+ ## Small Example
110
+
111
+ ```asm
112
+ .org 0100H
113
+
114
+ @START:
115
+ ld a,42
116
+ ret
75
117
  ```
76
118
 
77
- ```text
78
- azm [options] <entry.asm|entry.z80>
79
-
80
- Options:
81
- -o, --output <file> Primary output path (must match --type extension)
82
- -t, --type <type> Primary output type: hex|bin (default: hex)
83
- -n, --nolist Suppress .lst
84
- --nobin Suppress .bin
85
- --nohex Suppress .hex
86
- --nod8m Suppress .d8.json
87
- --asm80 Emit assembler-valid lowered source (.z80)
88
- --source-root <d> Normalize D8 source paths relative to this directory
89
- --case-style <m> Case-style lint mode: off|upper|lower|consistent
90
- --rc <m> Register-care mode: off|audit|warn|error|strict
91
- --reg-report Emit .regcare.txt report
92
- --reg-interface Emit inferred register-care interface (.asmi)
93
- --fix Apply conservative register-care source fixes
94
- --contracts Update source AZM contract blocks in place
95
- --accept-out <r:c> Promote inferred output candidate while annotating
96
- --interface <file> Load register-care interface contracts
97
- --reg-profile <p> Register-care profile: mon3
98
- --aliases <file> Load project directive alias JSON (repeatable)
99
- -I, --include <dir> Add include search path (repeatable)
100
- -V, --version Print version
101
- -h, --help Show help
119
+ Compile it:
120
+
121
+ ```sh
122
+ azm --type bin --output build/start.bin start.asm
102
123
  ```
103
124
 
104
125
  ## Programmatic API
@@ -130,21 +151,25 @@ const result = await compile(
130
151
  console.log(result.diagnostics);
131
152
  ```
132
153
 
133
- See [docs/reference/cli.md](docs/reference/cli.md) for the full CLI reference
134
- and [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for the
135
- current API notes.
154
+ See [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for current
155
+ API notes.
136
156
 
137
- ## Verification
157
+ ## Development
138
158
 
139
159
  Useful local verification lanes:
140
160
 
141
161
  ```sh
142
162
  npm run build
163
+ npm run typecheck
164
+ npm run lint
143
165
  npm run test:azm:alpha
144
166
  npm run test:azm:corpus
145
167
  npm test
146
168
  ```
147
169
 
170
+ The live source map is maintained in
171
+ [docs/reference/source-overview.md](docs/reference/source-overview.md).
172
+
148
173
  ## License
149
174
 
150
175
  GPL-3.0-only. See [LICENSE](LICENSE).
@@ -183,7 +183,7 @@ export async function compile(entryFile, options = {}, deps = { formats: default
183
183
  line: error.item.span.line,
184
184
  column: error.item.span.column,
185
185
  });
186
- return { diagnostics, artifacts: [] };
186
+ return { diagnostics, artifacts };
187
187
  }
188
188
  throw error;
189
189
  }
@@ -58,6 +58,8 @@ function buildAddressStateOnce(items, diagnostics, previous, reportUnknown) {
58
58
  case 'enum':
59
59
  defineEnumMembers(equates, labels, layouts, enumNames, enumNamesLower, item.name, item.members, item.span, diagnostics);
60
60
  break;
61
+ case 'comment':
62
+ break;
61
63
  case 'label':
62
64
  defineLabel(labels, equates, layouts, enumNamesLower, item.name, placementAddress(placement), item.span, diagnostics);
63
65
  break;
@@ -117,6 +117,7 @@ export function emitProgramImage(items, addressState, symbols, diagnostics) {
117
117
  }
118
118
  break;
119
119
  }
120
+ case 'comment':
120
121
  case 'equ':
121
122
  case 'label':
122
123
  case 'enum':
@@ -663,6 +663,7 @@ function renameSourceItem(item, localLabelMap) {
663
663
  ...item,
664
664
  instruction: renameInstructionExpressions(item.instruction, localLabelMap),
665
665
  };
666
+ case 'comment':
666
667
  case 'end':
667
668
  case 'enum':
668
669
  case 'type':
@@ -14,6 +14,12 @@ export type SourceItem = {
14
14
  } | {
15
15
  readonly kind: 'label';
16
16
  readonly name: string;
17
+ readonly isEntry?: boolean;
18
+ readonly span: SourceSpan;
19
+ } | {
20
+ readonly kind: 'comment';
21
+ readonly text: string;
22
+ readonly origin: 'user' | 'generated';
17
23
  readonly span: SourceSpan;
18
24
  } | {
19
25
  readonly kind: 'db';
@@ -74,6 +74,11 @@ function formatItem(item, evalContext, state) {
74
74
  ? undefined
75
75
  : withImplicitOrg(state, `${item.name} EQU ${expression}`, 0);
76
76
  }
77
+ case 'comment':
78
+ return {
79
+ text: item.origin === 'user' ? `; ${item.text}` : `; AZM: ${item.text}`,
80
+ size: 0,
81
+ };
77
82
  case 'label':
78
83
  return withImplicitOrg(state, `${item.name}:`, 0);
79
84
  case 'db':
@@ -239,6 +244,16 @@ function formatInstruction(instruction, evalContext) {
239
244
  case 'res':
240
245
  case 'set':
241
246
  return formatBitOp(instruction, evalContext);
247
+ case 'rlc':
248
+ case 'rrc':
249
+ case 'rl':
250
+ case 'rr':
251
+ case 'sla':
252
+ case 'sra':
253
+ case 'sll':
254
+ case 'sls':
255
+ case 'srl':
256
+ return formatRotateShift(instruction, evalContext);
242
257
  case 'in':
243
258
  return formatIn(instruction, evalContext);
244
259
  case 'out':
@@ -264,6 +279,11 @@ function formatInstruction(instruction, evalContext) {
264
279
  return formatBranch(`call ${instruction.condition},`, instruction.expression, evalContext);
265
280
  case 'djnz':
266
281
  return formatBranch('djnz', instruction.expression, evalContext);
282
+ case 'push':
283
+ case 'pop':
284
+ return { text: `${instruction.mnemonic} ${instruction.register}` };
285
+ case 'ret-cc':
286
+ return { text: `ret ${instruction.condition}` };
267
287
  default:
268
288
  return undefined;
269
289
  }
@@ -282,7 +302,7 @@ function formatAlu(mnemonic, source, evalContext) {
282
302
  return { text: `${mnemonic} ${operand}` };
283
303
  }
284
304
  function formatAluOperand(source, evalContext) {
285
- if (source.kind === 'reg8') {
305
+ if (source.kind === 'reg8' || source.kind === 'reg-half-index') {
286
306
  return source.register;
287
307
  }
288
308
  if (source.kind === 'reg-indirect' && source.register === 'hl') {
@@ -330,6 +350,18 @@ function formatLd(target, source, evalContext) {
330
350
  if (target.kind === 'reg8' && source.kind === 'reg8') {
331
351
  return { text: `ld ${target.register}, ${source.register}` };
332
352
  }
353
+ if (target.kind === 'reg8' && source.kind === 'reg-half-index') {
354
+ return { text: `ld ${target.register}, ${source.register}` };
355
+ }
356
+ if (target.kind === 'reg8' && target.register === 'a' && source.kind === 'special8') {
357
+ return { text: `ld a, ${source.register}` };
358
+ }
359
+ if (target.kind === 'special8' && source.kind === 'reg8' && source.register === 'a') {
360
+ return { text: `ld ${target.register}, a` };
361
+ }
362
+ if (target.kind === 'reg-index16' && source.kind === 'imm') {
363
+ return formatLdText(target.register, formatExpression(source.expression, evalContext, 'word'));
364
+ }
333
365
  if (target.kind === 'reg16' && source.kind === 'imm') {
334
366
  return formatLdText(target.register, formatExpression(source.expression, evalContext, 'word'));
335
367
  }
@@ -345,12 +377,44 @@ function formatLd(target, source, evalContext) {
345
377
  source.register === 'a') {
346
378
  return { text: `ld (${target.register}), a` };
347
379
  }
348
- if (target.kind === 'reg8' && target.register === 'a' && source.kind === 'mem-abs') {
349
- return formatLdText('a', formatParenthesizedExpression(source.expression, evalContext, 'auto'));
380
+ if (target.kind === 'reg8' && source.kind === 'reg-indirect' && source.register === 'hl') {
381
+ return { text: `ld ${target.register}, (hl)` };
382
+ }
383
+ if (target.kind === 'reg-indirect' && target.register === 'hl' && source.kind === 'reg8') {
384
+ return { text: `ld (hl), ${source.register}` };
385
+ }
386
+ if (target.kind === 'reg-indirect' && target.register === 'hl' && source.kind === 'imm') {
387
+ const value = formatExpression(source.expression, evalContext, 'byte');
388
+ return value === undefined ? undefined : { text: `ld (hl), ${value}` };
389
+ }
390
+ if (target.kind === 'reg8' && source.kind === 'indexed') {
391
+ const memory = formatIndexedMemory(source.register, source.displacement, evalContext);
392
+ return memory === undefined ? undefined : { text: `ld ${target.register}, ${memory}` };
350
393
  }
351
- if (target.kind === 'mem-abs' && source.kind === 'reg8' && source.register === 'a') {
394
+ if (target.kind === 'indexed' && source.kind === 'reg8') {
395
+ const memory = formatIndexedMemory(target.register, target.displacement, evalContext);
396
+ return memory === undefined ? undefined : { text: `ld ${memory}, ${source.register}` };
397
+ }
398
+ if (target.kind === 'reg8' && source.kind === 'mem-abs') {
399
+ return formatLdText(target.register, formatParenthesizedExpression(source.expression, evalContext, 'auto'));
400
+ }
401
+ if (target.kind === 'mem-abs' && source.kind === 'reg8') {
352
402
  const targetText = formatParenthesizedExpression(target.expression, evalContext, 'auto');
353
- return targetText === undefined ? undefined : { text: `ld ${targetText}, a` };
403
+ return targetText === undefined
404
+ ? undefined
405
+ : { text: `ld ${targetText}, ${source.register}` };
406
+ }
407
+ if (target.kind === 'mem-abs' && source.kind === 'reg16') {
408
+ const targetText = formatParenthesizedExpression(target.expression, evalContext, 'auto');
409
+ return targetText === undefined
410
+ ? undefined
411
+ : { text: `ld ${targetText}, ${source.register}` };
412
+ }
413
+ if (target.kind === 'mem-abs' && source.kind === 'reg-index16') {
414
+ const targetText = formatParenthesizedExpression(target.expression, evalContext, 'auto');
415
+ return targetText === undefined
416
+ ? undefined
417
+ : { text: `ld ${targetText}, ${source.register}` };
354
418
  }
355
419
  return undefined;
356
420
  }
@@ -383,6 +447,37 @@ function formatExpression(expression, evalContext, width) {
383
447
  if (expression.kind === 'symbol') {
384
448
  return expression.name;
385
449
  }
450
+ if (expression.kind === 'type-size') {
451
+ const value = evaluateLoweredConstant(expression, evalContext);
452
+ return value === undefined ? expression.typeExpr.name : formatLoweredNumber(value, width);
453
+ }
454
+ if (expression.kind === 'current-location') {
455
+ return '$';
456
+ }
457
+ if (expression.kind === 'unary') {
458
+ const inner = formatExpression(expression.expression, evalContext, width);
459
+ if (inner === undefined) {
460
+ return undefined;
461
+ }
462
+ switch (expression.operator) {
463
+ case '+':
464
+ return inner;
465
+ case '-':
466
+ return `-${inner}`;
467
+ case '~': {
468
+ const value = evaluateLoweredConstant(expression, evalContext);
469
+ return value === undefined ? undefined : formatLoweredNumber(value, width);
470
+ }
471
+ }
472
+ }
473
+ if (expression.kind === 'binary') {
474
+ const left = formatExpression(expression.left, evalContext, width);
475
+ const right = formatExpression(expression.right, evalContext, width);
476
+ if (left === undefined || right === undefined) {
477
+ return undefined;
478
+ }
479
+ return `${left}${expression.operator}${right}`;
480
+ }
386
481
  return undefined;
387
482
  }
388
483
  function evaluateLoweredConstant(expression, evalContext) {
@@ -391,6 +486,17 @@ function evaluateLoweredConstant(expression, evalContext) {
391
486
  return expression.value;
392
487
  case 'symbol':
393
488
  return evalContext.constants.get(expression.name);
489
+ case 'type-size': {
490
+ const constant = evalContext.constants.get(expression.typeExpr.name);
491
+ if (constant !== undefined) {
492
+ return constant;
493
+ }
494
+ return evaluateExpression(expression, {}, new Map(), silentSpan, [], {
495
+ currentLocation: 0,
496
+ layouts: evalContext.layouts,
497
+ reportUnknown: false,
498
+ });
499
+ }
394
500
  case 'sizeof':
395
501
  return evaluateExpression(expression, {}, new Map(), silentSpan, [], {
396
502
  currentLocation: 0,
@@ -452,6 +558,17 @@ function formatLoweredNumber(value, width) {
452
558
  const minWidth = width === 'word' || (width === 'auto' && normalized > 0xff) ? 4 : 2;
453
559
  return `$${digits.padStart(minWidth, '0')}`;
454
560
  }
561
+ function formatRotateShift(instruction, evalContext) {
562
+ const operand = formatBitOperand(instruction.operand, evalContext);
563
+ if (operand === undefined) {
564
+ return undefined;
565
+ }
566
+ const parts = [operand];
567
+ if (instruction.destination) {
568
+ parts.push(instruction.destination.register);
569
+ }
570
+ return { text: `${instruction.mnemonic} ${parts.join(', ')}` };
571
+ }
455
572
  function formatBitOp(instruction, evalContext) {
456
573
  const bit = formatLoweredNumber(instruction.bit, 'byte');
457
574
  const operand = formatBitOperand(instruction.operand, evalContext);
@@ -5,6 +5,14 @@ import { autoFixableCandidateKeys } 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';
8
+ function candidateMessageWithFixability(candidate, autoFixable) {
9
+ const carriers = candidate.carriers.join(',');
10
+ const expectation = candidate.carriers.length === 1 ? candidate.carriers[0] : `{${carriers}}`;
11
+ const base = `CALL ${candidate.routine} writes ${carriers} and caller reads it later`;
12
+ return autoFixable
13
+ ? `${base}; generated contracts promote this to \`out ${expectation}\` automatically.`
14
+ : `${base}; manual review required before adding \`; expects out ${expectation}\` because the later read is not a simple direct continuation.`;
15
+ }
8
16
  export function analyzeRegisterCare(loaded, options) {
9
17
  const file = loaded.program.files[0];
10
18
  const items = file?.items ?? [];
@@ -16,8 +24,8 @@ export function analyzeRegisterCare(loaded, options) {
16
24
  contractMap.set(contract.name, contract);
17
25
  }
18
26
  }
19
- let summaries = buildSummaries(program.routines, contractMap);
20
27
  const profileSummaries = buildProfileSummaries(options.registerCareProfile);
28
+ let summaries = buildSummaries(program.routines, contractMap, profileSummaries);
21
29
  summaries = withAcceptedOutputs(summaries, options.acceptedOutputCandidates);
22
30
  const allSummaries = [...summaries, ...profileSummaries];
23
31
  const summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
@@ -41,10 +49,14 @@ export function analyzeRegisterCare(loaded, options) {
41
49
  const conflicts = analyzed.conflicts;
42
50
  const outputCandidates = analyzed.outputCandidates;
43
51
  const outputCandidateFixability = buildOutputCandidateFixability(program.routines, outputCandidates, autoFixableCandidateKeys);
44
- const outputCandidatesWithFixability = outputCandidates.map((candidate) => ({
45
- ...candidate,
46
- autoFixable: outputCandidateFixability.get(outputCandidateKey(candidate.file, candidate.line, candidate.column)) ?? false,
47
- }));
52
+ const outputCandidatesWithFixability = outputCandidates.map((candidate) => {
53
+ const autoFixable = outputCandidateFixability.get(outputCandidateKey(candidate.file, candidate.line, candidate.column)) ?? false;
54
+ return {
55
+ ...candidate,
56
+ autoFixable,
57
+ message: candidateMessageWithFixability(candidate, autoFixable),
58
+ };
59
+ });
48
60
  if (options.mode !== 'audit') {
49
61
  for (const conflict of conflicts) {
50
62
  diagnostics.push({
@@ -58,7 +70,7 @@ export function analyzeRegisterCare(loaded, options) {
58
70
  }
59
71
  }
60
72
  if (options.mode === 'strict') {
61
- diagnostics.push(...unknownBoundaryDiagnostics(program.directCalls, knownRoutines));
73
+ diagnostics.push(...unknownBoundaryDiagnostics(program.directBoundaries, knownRoutines));
62
74
  }
63
75
  const reportModel = {
64
76
  entryFile: loaded.program.entryFile,
@@ -67,10 +79,26 @@ export function analyzeRegisterCare(loaded, options) {
67
79
  conflicts,
68
80
  outputCandidates: outputCandidatesWithFixability,
69
81
  ...(options.registerCareProfile !== undefined ? { profile: options.registerCareProfile } : {}),
70
- unknownCalls: options.mode === 'off' ? [] : unknownCallList(program.directCalls, knownRoutines),
82
+ unknownCalls: options.mode === 'off' ? [] : unknownCallList(program.directBoundaries, knownRoutines),
71
83
  };
84
+ const summariesForAnnotations = new Map(summariesByName);
85
+ const outputCandidatesByRoutine = new Map();
86
+ for (const candidate of outputCandidatesWithFixability) {
87
+ const existing = outputCandidatesByRoutine.get(candidate.routine) ?? [];
88
+ for (const unit of candidate.carriers) {
89
+ if (!existing.includes(unit))
90
+ existing.push(unit);
91
+ }
92
+ outputCandidatesByRoutine.set(candidate.routine, existing);
93
+ }
94
+ for (const [name, summary] of summariesForAnnotations) {
95
+ const candidates = outputCandidatesByRoutine.get(name);
96
+ if (candidates !== undefined && candidates.length > 0) {
97
+ summariesForAnnotations.set(name, { ...summary, outputCandidates: candidates });
98
+ }
99
+ }
72
100
  const annotations = options.emitAnnotations
73
- ? buildAnnotations(loaded, program.routines, summariesByName, outputCandidatesWithFixability, {
101
+ ? buildAnnotations(loaded, program.routines, summariesForAnnotations, outputCandidatesWithFixability, {
74
102
  fixOutputCandidates: options.fixRegisterContracts === true,
75
103
  outputCandidateFixability,
76
104
  outputCandidateKey,
@@ -0,0 +1,11 @@
1
+ import type { RegisterCareRoutine, RoutineSummary } from './types.js';
2
+ export interface RegisterCareAnnotatedFile {
3
+ path: string;
4
+ text: string;
5
+ }
6
+ interface RegisterCareAnnotationInput {
7
+ routine: RegisterCareRoutine;
8
+ summary: RoutineSummary;
9
+ }
10
+ export declare function annotateRegisterCareContracts(sourceTexts: ReadonlyMap<string, string>, routines: RegisterCareAnnotationInput[]): RegisterCareAnnotatedFile[];
11
+ export {};
@@ -0,0 +1,76 @@
1
+ import { renderRegisterCareSourceBlock } from './report.js';
2
+ import { joinSourceLines, splitSourceLines } from './sourceText.js';
3
+ const GENERATED_COMPACT_LINE_RE = /^\s*;\s*!\s*(?:in|out|maybe-out|clobbers|preserves)(?:\s|$)/i;
4
+ function isCommentLine(line) {
5
+ return /^\s*;/.test(line);
6
+ }
7
+ function isGeneratedCompactLine(line) {
8
+ return GENERATED_COMPACT_LINE_RE.test(line);
9
+ }
10
+ function precedingCommentBlockStart(lines, labelIndex) {
11
+ let index = labelIndex - 1;
12
+ if (index < 0 || !isCommentLine(lines[index] ?? ''))
13
+ return undefined;
14
+ while (index >= 0 && isCommentLine(lines[index] ?? ''))
15
+ index -= 1;
16
+ return index + 1;
17
+ }
18
+ function generatedBlockBeforeLabel(lines, labelIndex) {
19
+ let compactStart = labelIndex;
20
+ while (compactStart > 0 && isGeneratedCompactLine(lines[compactStart - 1] ?? '')) {
21
+ compactStart -= 1;
22
+ }
23
+ if (compactStart < labelIndex)
24
+ return { start: compactStart, end: labelIndex - 1 };
25
+ return undefined;
26
+ }
27
+ function hasPrecedingCommentBlock(lines, labelIndex) {
28
+ return precedingCommentBlockStart(lines, labelIndex) !== undefined;
29
+ }
30
+ function isExplicitEntryRoutine(routine) {
31
+ return routine.entryLabels?.includes(routine.name) === true;
32
+ }
33
+ function annotateFile(source, routines) {
34
+ const sourceLines = splitSourceLines(source);
35
+ const { lines } = sourceLines;
36
+ const sorted = [...routines].sort((a, b) => b.routine.span.start.line - a.routine.span.start.line);
37
+ for (const item of sorted) {
38
+ const labelIndex = item.routine.span.start.line - 1;
39
+ if (labelIndex < 0 || labelIndex > lines.length)
40
+ continue;
41
+ const block = renderRegisterCareSourceBlock(item.summary);
42
+ const hasContractContent = block.length > 0;
43
+ const existing = generatedBlockBeforeLabel(lines, labelIndex);
44
+ if (existing) {
45
+ lines.splice(existing.start, existing.end - existing.start + 1, ...(hasContractContent ? block : []));
46
+ continue;
47
+ }
48
+ if (!hasContractContent)
49
+ continue;
50
+ if (!isExplicitEntryRoutine(item.routine) && !hasPrecedingCommentBlock(lines, labelIndex)) {
51
+ continue;
52
+ }
53
+ lines.splice(labelIndex, 0, ...block);
54
+ }
55
+ return joinSourceLines(sourceLines);
56
+ }
57
+ export function annotateRegisterCareContracts(sourceTexts, routines) {
58
+ const byFile = new Map();
59
+ for (const item of routines) {
60
+ if (!sourceTexts.has(item.routine.span.file))
61
+ continue;
62
+ const items = byFile.get(item.routine.span.file) ?? [];
63
+ items.push(item);
64
+ byFile.set(item.routine.span.file, items);
65
+ }
66
+ const out = [];
67
+ for (const [file, items] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
68
+ const source = sourceTexts.get(file);
69
+ if (source === undefined)
70
+ continue;
71
+ const text = annotateFile(source, items);
72
+ if (text !== source)
73
+ out.push({ path: file, text });
74
+ }
75
+ return out;
76
+ }