@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.
- package/README.md +95 -70
- package/dist/src/api-compile.js +1 -1
- package/dist/src/assembly/address-planning.js +2 -0
- package/dist/src/assembly/program-emission.js +1 -0
- package/dist/src/expansion/op-expansion.js +1 -0
- package/dist/src/model/source-item.d.ts +6 -0
- package/dist/src/outputs/write-asm80.js +122 -5
- package/dist/src/register-care/analyze.js +36 -8
- package/dist/src/register-care/annotate.d.ts +11 -0
- package/dist/src/register-care/annotate.js +76 -0
- package/dist/src/register-care/annotations.js +33 -146
- package/dist/src/register-care/fix.d.ts +2 -0
- package/dist/src/register-care/fix.js +52 -0
- package/dist/src/register-care/instruction-shape.d.ts +11 -0
- package/dist/src/register-care/instruction-shape.js +129 -0
- package/dist/src/register-care/liveness.js +15 -7
- package/dist/src/register-care/profiles.js +4 -0
- package/dist/src/register-care/programModel.js +79 -13
- package/dist/src/register-care/report.d.ts +2 -1
- package/dist/src/register-care/report.js +91 -34
- package/dist/src/register-care/routine-summaries.d.ts +6 -0
- package/dist/src/register-care/routine-summaries.js +89 -0
- package/dist/src/register-care/sourceText.d.ts +8 -0
- package/dist/src/register-care/sourceText.js +15 -0
- package/dist/src/register-care/summaries.d.ts +3 -3
- package/dist/src/register-care/summaries.js +42 -75
- package/dist/src/register-care/summary.d.ts +3 -0
- package/dist/src/register-care/summary.js +474 -0
- package/dist/src/register-care/types.d.ts +6 -1
- package/dist/src/source/strip-line-comment.d.ts +2 -0
- package/dist/src/source/strip-line-comment.js +26 -0
- package/dist/src/syntax/parse-diagnostics.d.ts +12 -0
- package/dist/src/syntax/parse-diagnostics.js +18 -0
- package/dist/src/syntax/parse-line.js +63 -10
- package/docs/reference/tooling-api.md +13 -6
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,47 +1,54 @@
|
|
|
1
1
|
# AZM
|
|
2
2
|
|
|
3
|
-
AZM is
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
10
|
+
[AZM Assembler Manual](https://jhlagado.github.io/debug80-docs/azm-book/book4/)
|
|
12
11
|
|
|
13
|
-
AZM
|
|
12
|
+
## What AZM Is
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
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
|
-
-
|
|
27
|
-
and `.istr`
|
|
34
|
+
- data directives including `.db`, `.dw`, `.ds`, `.cstr`, `.pstr`, and `.istr`
|
|
28
35
|
|
|
29
|
-
AZM
|
|
30
|
-
|
|
31
|
-
lowering,
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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.
|
|
48
|
+
azm path/to/program.asm
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
From a checkout, use the local CLI
|
|
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
|
-
|
|
59
|
+
## Command Line
|
|
53
60
|
|
|
54
|
-
|
|
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
|
-
|
|
63
|
+
```sh
|
|
64
|
+
azm program.asm
|
|
65
|
+
```
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
+
Add include search paths:
|
|
72
75
|
|
|
73
76
|
```sh
|
|
74
|
-
azm
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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/
|
|
134
|
-
|
|
135
|
-
current API notes.
|
|
154
|
+
See [docs/reference/tooling-api.md](docs/reference/tooling-api.md) for current
|
|
155
|
+
API notes.
|
|
136
156
|
|
|
137
|
-
##
|
|
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).
|
package/dist/src/api-compile.js
CHANGED
|
@@ -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;
|
|
@@ -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' &&
|
|
349
|
-
return
|
|
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 === '
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
+
}
|