@jhlagado/azm 0.2.12 → 0.2.14
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/dist/src/api-compile.d.ts +7 -1
- package/dist/src/api-compile.js +17 -5
- package/dist/src/api-register-contracts.js +69 -2
- package/dist/src/api-tooling.d.ts +1 -1
- package/dist/src/cli/artifact-files.d.ts +1 -0
- package/dist/src/cli/artifact-files.js +5 -0
- package/dist/src/cli/parse-args.d.ts +6 -1
- package/dist/src/cli/parse-args.js +59 -0
- package/dist/src/cli/run.js +2 -2
- package/dist/src/cli/usage.js +5 -0
- package/dist/src/cli/write-artifacts.d.ts +1 -1
- package/dist/src/cli/write-artifacts.js +15 -2
- package/dist/src/expansion/op-expansion.js +12 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/outputs/types.d.ts +13 -1
- package/dist/src/register-contracts/analyze-helpers.d.ts +6 -1
- package/dist/src/register-contracts/analyze-helpers.js +67 -0
- package/dist/src/register-contracts/analyze.d.ts +8 -1
- package/dist/src/register-contracts/analyze.js +353 -16
- package/dist/src/register-contracts/interfaceContracts.js +45 -0
- package/dist/src/register-contracts/liveness.js +23 -0
- package/dist/src/register-contracts/policy.d.ts +2 -0
- package/dist/src/register-contracts/policy.js +54 -0
- package/dist/src/register-contracts/profiles.d.ts +5 -0
- package/dist/src/register-contracts/profiles.js +32 -2
- package/dist/src/register-contracts/programModel-boundaries.d.ts +5 -1
- package/dist/src/register-contracts/programModel-boundaries.js +20 -5
- package/dist/src/register-contracts/programModel-routines.js +37 -6
- package/dist/src/register-contracts/ratchet.d.ts +3 -0
- package/dist/src/register-contracts/ratchet.js +88 -0
- package/dist/src/register-contracts/report.d.ts +8 -1
- package/dist/src/register-contracts/report.js +174 -0
- package/dist/src/register-contracts/smartCommentParsing.js +22 -0
- package/dist/src/register-contracts/summaries.js +4 -0
- package/dist/src/register-contracts/summary-boundary.js +21 -3
- package/dist/src/register-contracts/summary.js +31 -3
- package/dist/src/register-contracts/tooling.d.ts +2 -1
- package/dist/src/register-contracts/tooling.js +2 -0
- package/dist/src/register-contracts/types.d.ts +159 -0
- package/dist/src/syntax/parse-line.js +3 -0
- package/docs/codebase/02-source-loading-and-parsing.md +10 -6
- package/docs/codebase/04-ops-and-register-contracts.md +58 -4
- package/docs/codebase/05-interfaces-and-output-artifacts.md +69 -6
- package/docs/codebase/06-verification-and-maintenance.md +10 -2
- package/docs/codebase/appendices/a-directory-file-reference.md +3 -1
- package/docs/codebase/appendices/b-compile-flow-reference.md +7 -5
- package/docs/codebase/appendices/c-public-surface-reference.md +19 -5
- package/package.json +1 -1
|
@@ -89,10 +89,14 @@ function mon3ApiServices(overrides) {
|
|
|
89
89
|
'WRITE_SECTOR',
|
|
90
90
|
'RGB_SCAN',
|
|
91
91
|
];
|
|
92
|
-
|
|
92
|
+
const services = new Map(names.map((serviceName, api) => [
|
|
93
93
|
api,
|
|
94
94
|
overrides.get(api) ?? conservativeMon3ApiSummary(api, serviceName),
|
|
95
95
|
]));
|
|
96
|
+
for (const [api, summary] of overrides) {
|
|
97
|
+
services.set(api, summary);
|
|
98
|
+
}
|
|
99
|
+
return services;
|
|
96
100
|
}
|
|
97
101
|
export function rstDispatcherServiceTargetNames(vector, selectorValue) {
|
|
98
102
|
const mon3 = getRegisterContractsProfile('mon3');
|
|
@@ -103,7 +107,10 @@ export function rstDispatcherServiceTargetNames(vector, selectorValue) {
|
|
|
103
107
|
if (value === undefined)
|
|
104
108
|
return [];
|
|
105
109
|
const service = dispatcher.services.get(value);
|
|
106
|
-
|
|
110
|
+
if (service)
|
|
111
|
+
return [service.name];
|
|
112
|
+
const rangeService = dispatcher.rangeServices?.find((entry) => value >= entry.min && (entry.max === undefined || value <= entry.max));
|
|
113
|
+
return rangeService ? [rangeService.summary.name] : [];
|
|
107
114
|
}
|
|
108
115
|
export function getRegisterContractsProfile(name) {
|
|
109
116
|
if (name !== 'mon3')
|
|
@@ -168,6 +175,27 @@ export function getRegisterContractsProfile(name) {
|
|
|
168
175
|
stackBalanced: true,
|
|
169
176
|
hasUnknownStackEffect: false,
|
|
170
177
|
};
|
|
178
|
+
const bankCall = {
|
|
179
|
+
name: mon3ApiTargetName(0x53, 'BANK_CALL'),
|
|
180
|
+
mayRead: ['B', 'C', 'H', 'L'],
|
|
181
|
+
mayWrite: ['A', 'B', 'C', 'D', 'E', 'H', 'L', ...FLAG_UNITS],
|
|
182
|
+
mayOutput: ['A', 'carry'],
|
|
183
|
+
preserved: [],
|
|
184
|
+
valueRelations: [{ out: ['A', 'carry'], from: [] }],
|
|
185
|
+
stackBalanced: true,
|
|
186
|
+
hasUnknownStackEffect: false,
|
|
187
|
+
consumesStackFrame: ['AF', 'DE', 'HL'],
|
|
188
|
+
};
|
|
189
|
+
const tecmateExpansionService = {
|
|
190
|
+
name: 'TECMATE_EXPANSION_SERVICE',
|
|
191
|
+
mayRead: ['C'],
|
|
192
|
+
mayWrite: ['A', ...FLAG_UNITS],
|
|
193
|
+
mayOutput: ['A', 'carry'],
|
|
194
|
+
preserved: ['B', 'C', 'D', 'E', 'H', 'L'],
|
|
195
|
+
valueRelations: [{ out: ['A', 'carry'], from: [] }],
|
|
196
|
+
stackBalanced: true,
|
|
197
|
+
hasUnknownStackEffect: false,
|
|
198
|
+
};
|
|
171
199
|
return {
|
|
172
200
|
name: 'mon3',
|
|
173
201
|
rst: new Map([
|
|
@@ -212,7 +240,9 @@ export function getRegisterContractsProfile(name) {
|
|
|
212
240
|
[16, scanKeys],
|
|
213
241
|
[18, matrixScan],
|
|
214
242
|
[54, parseMatrixScan],
|
|
243
|
+
[0x53, bankCall],
|
|
215
244
|
])),
|
|
245
|
+
rangeServices: [{ min: 0x60, summary: tecmateExpansionService }],
|
|
216
246
|
},
|
|
217
247
|
],
|
|
218
248
|
]),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SourceItem } from '../model/source-item.js';
|
|
2
2
|
import type { RegisterContractsDirectCall } from './types.js';
|
|
3
|
+
type InstructionItem = Extract<SourceItem, {
|
|
4
|
+
readonly kind: 'instruction';
|
|
5
|
+
}>;
|
|
3
6
|
export declare function instructionCallTarget(item: SourceItem): string | undefined;
|
|
4
|
-
export declare function pushDirectBoundary(boundaries: RegisterContractsDirectCall[], target: string, subject: string,
|
|
7
|
+
export declare function pushDirectBoundary(boundaries: RegisterContractsDirectCall[], target: string, subject: string, span: InstructionItem['span']): void;
|
|
5
8
|
export declare function collectFilesWithEntryLabels(items: readonly SourceItem[]): Set<string>;
|
|
6
9
|
export declare function collectDirectTailJumps(items: readonly SourceItem[], filesWithEntryLabels: ReadonlySet<string>): RegisterContractsDirectCall[];
|
|
10
|
+
export {};
|
|
@@ -26,8 +26,22 @@ function isEligibleTailJumpTarget(target, entryNames) {
|
|
|
26
26
|
return false;
|
|
27
27
|
return entryNames === undefined || entryNames.has(target);
|
|
28
28
|
}
|
|
29
|
-
export function pushDirectBoundary(boundaries, target, subject,
|
|
30
|
-
boundaries.push({
|
|
29
|
+
export function pushDirectBoundary(boundaries, target, subject, span) {
|
|
30
|
+
boundaries.push({
|
|
31
|
+
target,
|
|
32
|
+
subject,
|
|
33
|
+
file: span.sourceName,
|
|
34
|
+
line: span.line,
|
|
35
|
+
column: span.column,
|
|
36
|
+
...(span.sourceUnit !== undefined ? { sourceUnit: span.sourceUnit } : {}),
|
|
37
|
+
...(span.sourceRelation !== undefined ? { sourceRelation: span.sourceRelation } : {}),
|
|
38
|
+
...(span.sourceUnitRelation !== undefined
|
|
39
|
+
? { sourceUnitRelation: span.sourceUnitRelation }
|
|
40
|
+
: {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function effectiveInstructionSpan(item) {
|
|
44
|
+
return item.emittedSource?.span ?? item.span;
|
|
31
45
|
}
|
|
32
46
|
export function collectFilesWithEntryLabels(items) {
|
|
33
47
|
return new Set(items
|
|
@@ -52,13 +66,14 @@ export function collectDirectTailJumps(items, filesWithEntryLabels) {
|
|
|
52
66
|
for (const item of items) {
|
|
53
67
|
if (item.kind !== 'instruction')
|
|
54
68
|
continue;
|
|
55
|
-
const
|
|
56
|
-
|
|
69
|
+
const span = effectiveInstructionSpan(item);
|
|
70
|
+
const entryNames = filesWithEntryLabels.has(span.sourceName)
|
|
71
|
+
? entriesByFile.get(span.sourceName)
|
|
57
72
|
: undefined;
|
|
58
73
|
const target = instructionTailJumpTarget(item, entryNames);
|
|
59
74
|
if (target === undefined)
|
|
60
75
|
continue;
|
|
61
|
-
pushDirectBoundary(directTailJumps, target, `JP ${target}`, item
|
|
76
|
+
pushDirectBoundary(directTailJumps, target, `JP ${target}`, effectiveInstructionSpan(item));
|
|
62
77
|
}
|
|
63
78
|
return directTailJumps;
|
|
64
79
|
}
|
|
@@ -2,6 +2,9 @@ import { instructionCallTarget, pushDirectBoundary } from './programModel-bounda
|
|
|
2
2
|
function isGlobalLabel(name) {
|
|
3
3
|
return !name.startsWith('.');
|
|
4
4
|
}
|
|
5
|
+
function isGeneratedOpLabel(name) {
|
|
6
|
+
return name.startsWith('__azm_op_');
|
|
7
|
+
}
|
|
5
8
|
function emptyState() {
|
|
6
9
|
return {
|
|
7
10
|
entryLabels: [],
|
|
@@ -10,17 +13,40 @@ function emptyState() {
|
|
|
10
13
|
};
|
|
11
14
|
}
|
|
12
15
|
function toInstruction(item, labels, constants) {
|
|
16
|
+
const span = effectiveInstructionSpan(item);
|
|
13
17
|
return {
|
|
14
18
|
instruction: item.instruction,
|
|
15
|
-
file:
|
|
16
|
-
line:
|
|
17
|
-
column:
|
|
19
|
+
file: span.sourceName,
|
|
20
|
+
line: span.line,
|
|
21
|
+
column: span.column,
|
|
22
|
+
...(span.sourceUnit !== undefined ? { sourceUnit: span.sourceUnit } : {}),
|
|
23
|
+
...(span.sourceRelation !== undefined ? { sourceRelation: span.sourceRelation } : {}),
|
|
24
|
+
...(span.sourceUnitRelation !== undefined
|
|
25
|
+
? { sourceUnitRelation: span.sourceUnitRelation }
|
|
26
|
+
: {}),
|
|
18
27
|
labels: [...labels],
|
|
19
28
|
constants,
|
|
20
29
|
};
|
|
21
30
|
}
|
|
31
|
+
function effectiveInstructionSpan(item) {
|
|
32
|
+
return item.emittedSource?.span ?? item.span;
|
|
33
|
+
}
|
|
22
34
|
function startRoutine(state, item) {
|
|
23
35
|
state.sourceName = item.span.sourceName;
|
|
36
|
+
if (item.span.sourceUnit !== undefined)
|
|
37
|
+
state.sourceUnit = item.span.sourceUnit;
|
|
38
|
+
else
|
|
39
|
+
delete state.sourceUnit;
|
|
40
|
+
if (item.span.sourceRelation !== undefined)
|
|
41
|
+
state.sourceRelation = item.span.sourceRelation;
|
|
42
|
+
else
|
|
43
|
+
delete state.sourceRelation;
|
|
44
|
+
if (item.span.sourceUnitRelation !== undefined) {
|
|
45
|
+
state.sourceUnitRelation = item.span.sourceUnitRelation;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
delete state.sourceUnitRelation;
|
|
49
|
+
}
|
|
24
50
|
state.routineName = item.name;
|
|
25
51
|
state.entryLabels = item.isEntry === true ? [item.name] : [];
|
|
26
52
|
state.labels = [item.name];
|
|
@@ -32,6 +58,11 @@ function routineSpan(state, end) {
|
|
|
32
58
|
const line = state.routineStartLine ?? 1;
|
|
33
59
|
return {
|
|
34
60
|
file: state.sourceName ?? '',
|
|
61
|
+
...(state.sourceUnit !== undefined ? { sourceUnit: state.sourceUnit } : {}),
|
|
62
|
+
...(state.sourceRelation !== undefined ? { sourceRelation: state.sourceRelation } : {}),
|
|
63
|
+
...(state.sourceUnitRelation !== undefined
|
|
64
|
+
? { sourceUnitRelation: state.sourceUnitRelation }
|
|
65
|
+
: {}),
|
|
35
66
|
start: { line, column: state.routineStartColumn ?? 1 },
|
|
36
67
|
end: { line: end?.line ?? line, column: end?.column ?? state.routineStartColumn ?? 1 },
|
|
37
68
|
};
|
|
@@ -58,12 +89,12 @@ function appendDirectCall(directCalls, item) {
|
|
|
58
89
|
const directTarget = instructionCallTarget(item);
|
|
59
90
|
if (directTarget === undefined)
|
|
60
91
|
return;
|
|
61
|
-
pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, item
|
|
92
|
+
pushDirectBoundary(directCalls, directTarget, `CALL ${directTarget}`, effectiveInstructionSpan(item));
|
|
62
93
|
}
|
|
63
94
|
function handleInstruction(state, directCalls, item, context) {
|
|
64
95
|
if (state.routineName === undefined || state.sourceName === undefined)
|
|
65
96
|
return;
|
|
66
|
-
if (item.
|
|
97
|
+
if (effectiveInstructionSpan(item).sourceName !== state.sourceName)
|
|
67
98
|
return;
|
|
68
99
|
state.instructions.push(toInstruction(item, state.labels, context.constants));
|
|
69
100
|
appendDirectCall(directCalls, item);
|
|
@@ -106,7 +137,7 @@ function appendRoutineLabel(state, item) {
|
|
|
106
137
|
state.entryLabels.push(item.name);
|
|
107
138
|
}
|
|
108
139
|
function handleLabel(routines, state, item, context) {
|
|
109
|
-
if (!isGlobalLabel(item.name)) {
|
|
140
|
+
if (!isGlobalLabel(item.name) || isGeneratedOpLabel(item.name)) {
|
|
110
141
|
if (state.routineName !== undefined)
|
|
111
142
|
state.labels.push(item.name);
|
|
112
143
|
return;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RegisterContractsJsonFinding, RegisterContractsJsonReportModel, RegisterContractsRatchetResult } from './types.js';
|
|
2
|
+
export declare function registerContractsFindingIdentity(finding: RegisterContractsJsonFinding): string;
|
|
3
|
+
export declare function compareRegisterContractsBaseline(current: RegisterContractsJsonReportModel, baseline: RegisterContractsJsonReportModel, baselineFile: string | undefined): RegisterContractsRatchetResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function carriersKey(finding) {
|
|
2
|
+
return [...(finding.carriers ?? [])].sort().join(',');
|
|
3
|
+
}
|
|
4
|
+
export function registerContractsFindingIdentity(finding) {
|
|
5
|
+
const target = finding.callTarget ?? finding.routine ?? finding.subject ?? '';
|
|
6
|
+
return [
|
|
7
|
+
finding.kind,
|
|
8
|
+
target,
|
|
9
|
+
carriersKey(finding),
|
|
10
|
+
].join('|');
|
|
11
|
+
}
|
|
12
|
+
function displayIdentity(finding) {
|
|
13
|
+
return [
|
|
14
|
+
registerContractsFindingIdentity(finding),
|
|
15
|
+
finding.location.file,
|
|
16
|
+
finding.location.line,
|
|
17
|
+
finding.location.column,
|
|
18
|
+
].join('|');
|
|
19
|
+
}
|
|
20
|
+
function ratchetEntries(findings) {
|
|
21
|
+
const out = new Map();
|
|
22
|
+
for (const finding of findings) {
|
|
23
|
+
const identity = registerContractsFindingIdentity(finding);
|
|
24
|
+
const entries = out.get(identity) ?? [];
|
|
25
|
+
entries.push({ identity: displayIdentity(finding), finding });
|
|
26
|
+
out.set(identity, entries);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function findingFingerprint(finding) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
location: finding.location,
|
|
33
|
+
message: finding.message,
|
|
34
|
+
remediationCategory: finding.remediation.category,
|
|
35
|
+
remediationHint: finding.remediation.hint,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function compareRegisterContractsBaseline(current, baseline, baselineFile) {
|
|
39
|
+
const currentEntries = ratchetEntries(current.findings);
|
|
40
|
+
const baselineEntries = ratchetEntries(baseline.findings);
|
|
41
|
+
const identities = new Set([...currentEntries.keys(), ...baselineEntries.keys()]);
|
|
42
|
+
const newFindings = [];
|
|
43
|
+
const removedFindings = [];
|
|
44
|
+
const changedFindings = [];
|
|
45
|
+
for (const identity of identities) {
|
|
46
|
+
const currentGroup = [...(currentEntries.get(identity) ?? [])];
|
|
47
|
+
const baselineGroup = [...(baselineEntries.get(identity) ?? [])];
|
|
48
|
+
const matchedBaseline = new Set();
|
|
49
|
+
const matchedCurrent = new Set();
|
|
50
|
+
for (const [currentIndex, currentEntry] of currentGroup.entries()) {
|
|
51
|
+
const exactIndex = baselineGroup.findIndex((baselineEntry, baselineIndex) => !matchedBaseline.has(baselineIndex) &&
|
|
52
|
+
findingFingerprint(baselineEntry.finding) === findingFingerprint(currentEntry.finding));
|
|
53
|
+
if (exactIndex === -1)
|
|
54
|
+
continue;
|
|
55
|
+
matchedCurrent.add(currentIndex);
|
|
56
|
+
matchedBaseline.add(exactIndex);
|
|
57
|
+
}
|
|
58
|
+
for (const [currentIndex, currentEntry] of currentGroup.entries()) {
|
|
59
|
+
if (matchedCurrent.has(currentIndex))
|
|
60
|
+
continue;
|
|
61
|
+
const changedIndex = baselineGroup.findIndex((_baselineEntry, baselineIndex) => !matchedBaseline.has(baselineIndex));
|
|
62
|
+
if (changedIndex === -1)
|
|
63
|
+
continue;
|
|
64
|
+
const baselineEntry = baselineGroup[changedIndex];
|
|
65
|
+
matchedCurrent.add(currentIndex);
|
|
66
|
+
matchedBaseline.add(changedIndex);
|
|
67
|
+
changedFindings.push({
|
|
68
|
+
identity,
|
|
69
|
+
baseline: baselineEntry.finding,
|
|
70
|
+
current: currentEntry.finding,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const [currentIndex, currentEntry] of currentGroup.entries()) {
|
|
74
|
+
if (!matchedCurrent.has(currentIndex))
|
|
75
|
+
newFindings.push(currentEntry);
|
|
76
|
+
}
|
|
77
|
+
for (const [baselineIndex, baselineEntry] of baselineGroup.entries()) {
|
|
78
|
+
if (!matchedBaseline.has(baselineIndex))
|
|
79
|
+
removedFindings.push(baselineEntry);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...(baselineFile !== undefined ? { baselineFile } : {}),
|
|
84
|
+
newFindings,
|
|
85
|
+
removedFindings,
|
|
86
|
+
changedFindings,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import type { RegisterContractsReportModel, RegisterContractsUnit, RoutineSummary } from './types.js';
|
|
1
|
+
import type { RegisterContractsJsonReportModel, RegisterContractsInferenceModel, RegisterContractsReportModel, RegisterContractsUnit, RoutineSummary } from './types.js';
|
|
2
2
|
export declare function contractCarrierList(units: RegisterContractsUnit[]): string;
|
|
3
3
|
export declare function renderRegisterContractsReport(model: RegisterContractsReportModel): string;
|
|
4
|
+
export declare function buildRegisterContractsJsonReport(model: RegisterContractsReportModel): RegisterContractsJsonReportModel;
|
|
5
|
+
export declare function renderRegisterContractsJsonReport(model: RegisterContractsReportModel): {
|
|
6
|
+
json: RegisterContractsJsonReportModel;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
4
9
|
export declare function renderRegisterContractsInterface(summaries: RoutineSummary[]): string;
|
|
10
|
+
export declare function buildRegisterContractsInference(summaries: readonly RoutineSummary[]): RegisterContractsInferenceModel;
|
|
11
|
+
export declare function renderRegisterContractsInferenceMarkdown(model: RegisterContractsInferenceModel): string;
|
|
5
12
|
export declare function renderRegisterContractsSourceBlock(summary: RoutineSummary): string[];
|
|
@@ -90,11 +90,123 @@ export function renderRegisterContractsReport(model) {
|
|
|
90
90
|
lines.push(`Profile: ${model.profile}`);
|
|
91
91
|
lines.push('');
|
|
92
92
|
appendRoutineSummaries(lines, model);
|
|
93
|
+
appendFindings(lines, model);
|
|
93
94
|
appendConflicts(lines, model);
|
|
94
95
|
appendOutputCandidates(lines, model);
|
|
96
|
+
appendRatchet(lines, model);
|
|
95
97
|
appendUnknownCalls(lines, model);
|
|
96
98
|
return `${lines.join('\n')}\n`;
|
|
97
99
|
}
|
|
100
|
+
export function buildRegisterContractsJsonReport(model) {
|
|
101
|
+
return {
|
|
102
|
+
format: 'azm-register-contracts-report',
|
|
103
|
+
version: 1,
|
|
104
|
+
entryFile: model.entryFile,
|
|
105
|
+
mode: model.mode,
|
|
106
|
+
...(model.profile !== undefined ? { profile: model.profile } : {}),
|
|
107
|
+
summaries: model.summaries,
|
|
108
|
+
findings: (model.findings ?? []).map(jsonFinding),
|
|
109
|
+
...(model.suppressedFindings !== undefined && model.suppressedFindings.length > 0
|
|
110
|
+
? {
|
|
111
|
+
suppressedFindings: model.suppressedFindings.map((item) => ({
|
|
112
|
+
finding: jsonFinding(item.finding),
|
|
113
|
+
suppression: item.suppression,
|
|
114
|
+
})),
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
unknownCalls: model.unknownCalls,
|
|
118
|
+
...(model.ratchet !== undefined ? { ratchet: model.ratchet } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function renderRegisterContractsJsonReport(model) {
|
|
122
|
+
const json = buildRegisterContractsJsonReport(model);
|
|
123
|
+
return { json, text: `${JSON.stringify(json, null, 2)}\n` };
|
|
124
|
+
}
|
|
125
|
+
function jsonFinding(finding) {
|
|
126
|
+
return {
|
|
127
|
+
kind: finding.kind,
|
|
128
|
+
location: jsonLocation(finding),
|
|
129
|
+
message: finding.message,
|
|
130
|
+
...('routine' in finding && finding.routine !== undefined ? { routine: finding.routine } : {}),
|
|
131
|
+
...('callTarget' in finding ? { callTarget: finding.callTarget } : {}),
|
|
132
|
+
...('subject' in finding ? { subject: finding.subject } : {}),
|
|
133
|
+
...(finding.carriers !== undefined ? { carriers: finding.carriers } : {}),
|
|
134
|
+
...('stackBalanced' in finding ? { stackBalanced: finding.stackBalanced } : {}),
|
|
135
|
+
...('hasUnknownStackEffect' in finding && finding.hasUnknownStackEffect !== undefined
|
|
136
|
+
? { hasUnknownStackEffect: finding.hasUnknownStackEffect }
|
|
137
|
+
: {}),
|
|
138
|
+
...('autoFixable' in finding && finding.autoFixable !== undefined
|
|
139
|
+
? { autoFixable: finding.autoFixable }
|
|
140
|
+
: {}),
|
|
141
|
+
remediation: remediationForFinding(finding),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function jsonLocation(finding) {
|
|
145
|
+
return {
|
|
146
|
+
file: finding.file,
|
|
147
|
+
line: finding.line,
|
|
148
|
+
column: finding.column,
|
|
149
|
+
...(finding.sourceUnit !== undefined ? { sourceUnit: finding.sourceUnit } : {}),
|
|
150
|
+
...(finding.sourceRelation !== undefined ? { sourceRelation: finding.sourceRelation } : {}),
|
|
151
|
+
...(finding.sourceUnitRelation !== undefined
|
|
152
|
+
? { sourceUnitRelation: finding.sourceUnitRelation }
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function remediationForFinding(finding) {
|
|
157
|
+
switch (finding.kind) {
|
|
158
|
+
case 'missing_callee_contract':
|
|
159
|
+
case 'external_interface_unknown':
|
|
160
|
+
return {
|
|
161
|
+
category: 'add_contract',
|
|
162
|
+
hint: 'Add a routine body or .asmi extern contract for the boundary target.',
|
|
163
|
+
};
|
|
164
|
+
case 'unknown_control_flow':
|
|
165
|
+
return {
|
|
166
|
+
category: 'review_control_flow',
|
|
167
|
+
hint: 'Keep stack-changing paths inside one @ routine boundary or split the flow into explicit routines.',
|
|
168
|
+
};
|
|
169
|
+
case 'output_candidate':
|
|
170
|
+
return {
|
|
171
|
+
category: 'review_output_contract',
|
|
172
|
+
hint: finding.autoFixable === true
|
|
173
|
+
? 'Generated contracts can promote this candidate to an output.'
|
|
174
|
+
: 'Review the caller and callee before marking this carrier as an output.',
|
|
175
|
+
};
|
|
176
|
+
case 'definite_contract_violation':
|
|
177
|
+
case 'flag_lifetime_risk':
|
|
178
|
+
return {
|
|
179
|
+
category: 'fix_call_or_contract',
|
|
180
|
+
hint: 'Fix the caller liveness issue or update the callee contract if the value is an intentional output.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function appendFindings(lines, model) {
|
|
185
|
+
lines.push('Findings:');
|
|
186
|
+
if (!model.findings || model.findings.length === 0) {
|
|
187
|
+
lines.push(' none');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const finding of model.findings) {
|
|
191
|
+
const carriers = finding.carriers ? `: ${list(finding.carriers)}` : '';
|
|
192
|
+
const target = 'callTarget' in finding ? `: ${finding.callTarget}` : '';
|
|
193
|
+
lines.push(` ${finding.file}:${finding.line}:${finding.column}: ${finding.kind}${target}${carriers}: ${finding.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('Suppressed findings:');
|
|
198
|
+
if (!model.suppressedFindings || model.suppressedFindings.length === 0) {
|
|
199
|
+
lines.push(' none');
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
for (const item of model.suppressedFindings) {
|
|
203
|
+
const finding = item.finding;
|
|
204
|
+
const target = 'callTarget' in finding ? `: ${finding.callTarget}` : '';
|
|
205
|
+
lines.push(` ${finding.file}:${finding.line}:${finding.column}: ${finding.kind}${target}: ${item.suppression.reason}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
lines.push('');
|
|
209
|
+
}
|
|
98
210
|
function appendRoutineSummaries(lines, model) {
|
|
99
211
|
if (model.summaries.length === 0) {
|
|
100
212
|
lines.push('Routines: none', '');
|
|
@@ -149,6 +261,27 @@ function appendUnknownCalls(lines, model) {
|
|
|
149
261
|
}
|
|
150
262
|
lines.push('');
|
|
151
263
|
}
|
|
264
|
+
function appendRatchet(lines, model) {
|
|
265
|
+
if (model.ratchet === undefined)
|
|
266
|
+
return;
|
|
267
|
+
lines.push('Ratchet:');
|
|
268
|
+
if (model.ratchet.baselineFile !== undefined) {
|
|
269
|
+
lines.push(` baseline: ${model.ratchet.baselineFile}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push(` new findings: ${model.ratchet.newFindings.length}`);
|
|
272
|
+
for (const entry of model.ratchet.newFindings) {
|
|
273
|
+
lines.push(` ${entry.finding.location.file}:${entry.finding.location.line}:${entry.finding.location.column}: ${entry.finding.kind}: ${entry.finding.message}`);
|
|
274
|
+
}
|
|
275
|
+
lines.push(` removed findings: ${model.ratchet.removedFindings.length}`);
|
|
276
|
+
for (const entry of model.ratchet.removedFindings) {
|
|
277
|
+
lines.push(` ${entry.finding.location.file}:${entry.finding.location.line}:${entry.finding.location.column}: ${entry.finding.kind}: ${entry.finding.message}`);
|
|
278
|
+
}
|
|
279
|
+
lines.push(` changed findings: ${model.ratchet.changedFindings.length}`);
|
|
280
|
+
for (const entry of model.ratchet.changedFindings) {
|
|
281
|
+
lines.push(` ${entry.current.location.file}:${entry.current.location.line}:${entry.current.location.column}: ${entry.current.kind}: ${entry.current.message}`);
|
|
282
|
+
}
|
|
283
|
+
lines.push('');
|
|
284
|
+
}
|
|
152
285
|
export function renderRegisterContractsInterface(summaries) {
|
|
153
286
|
const lines = [];
|
|
154
287
|
for (const summary of summaries) {
|
|
@@ -160,6 +293,47 @@ export function renderRegisterContractsInterface(summaries) {
|
|
|
160
293
|
}
|
|
161
294
|
return `${lines.join('\n')}\n`;
|
|
162
295
|
}
|
|
296
|
+
export function buildRegisterContractsInference(summaries) {
|
|
297
|
+
return {
|
|
298
|
+
format: 'azm-register-contracts-inference',
|
|
299
|
+
version: 1,
|
|
300
|
+
routines: summaries.map((summary) => {
|
|
301
|
+
const out = relationOutputUnits(summary.valueRelations);
|
|
302
|
+
const outputCandidateCarriers = summary.outputCandidates ?? [];
|
|
303
|
+
return {
|
|
304
|
+
name: summary.name,
|
|
305
|
+
in: summary.mayRead,
|
|
306
|
+
out,
|
|
307
|
+
clobbers: summary.mayWrite.filter((unit) => !out.includes(unit)),
|
|
308
|
+
preserves: summary.preserved,
|
|
309
|
+
confidence: out.length > 0 || summary.mayRead.length > 0 || summary.preserved.length > 0
|
|
310
|
+
? 'inferred'
|
|
311
|
+
: 'draft',
|
|
312
|
+
callerImpact: {
|
|
313
|
+
outputCandidateCount: outputCandidateCarriers.length,
|
|
314
|
+
outputCandidateCarriers,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function renderRegisterContractsInferenceMarkdown(model) {
|
|
321
|
+
const lines = ['# AZM Register Contracts Inference', ''];
|
|
322
|
+
for (const routine of model.routines) {
|
|
323
|
+
lines.push(`## ${routine.name}`);
|
|
324
|
+
lines.push(`- confidence: ${routine.confidence}`);
|
|
325
|
+
lines.push(`- in: ${contractCarrierList(routine.in)}`);
|
|
326
|
+
lines.push(`- out: ${contractCarrierList(routine.out)}`);
|
|
327
|
+
lines.push(`- clobbers: ${contractCarrierList(routine.clobbers)}`);
|
|
328
|
+
lines.push(`- preserves: ${contractCarrierList(routine.preserves)}`);
|
|
329
|
+
lines.push(`- caller impact: ${routine.callerImpact.outputCandidateCount} output candidate carrier(s)`);
|
|
330
|
+
if (routine.callerImpact.outputCandidateCarriers.length > 0) {
|
|
331
|
+
lines.push(`- output candidates: ${contractCarrierList(routine.callerImpact.outputCandidateCarriers)}`);
|
|
332
|
+
}
|
|
333
|
+
lines.push('');
|
|
334
|
+
}
|
|
335
|
+
return `${lines.join('\n')}\n`;
|
|
336
|
+
}
|
|
163
337
|
export function renderRegisterContractsSourceBlock(summary) {
|
|
164
338
|
const entries = sourceContractEntries(summary);
|
|
165
339
|
if (entries.length === 0)
|
|
@@ -4,6 +4,14 @@ const COMPACT_SOURCE_CLAUSE_RE = /^(in|out|clobbers|preserves)(?:\s+(.+))?$/i;
|
|
|
4
4
|
const COMPACT_SOURCE_LINE_RE = /^\s*;\s*!\s*(?:in|out|maybe-out|clobbers|preserves)(?:\s|$)/i;
|
|
5
5
|
const CARRIER_RE = /^\{([^}]+)\}(?:\s+(.+))?$/;
|
|
6
6
|
const CONTRACT_COMMENT_KINDS = new Set(['in', 'out', 'clobbers', 'preserves']);
|
|
7
|
+
const FINDING_KINDS = new Set([
|
|
8
|
+
'definite_contract_violation',
|
|
9
|
+
'flag_lifetime_risk',
|
|
10
|
+
'missing_callee_contract',
|
|
11
|
+
'unknown_control_flow',
|
|
12
|
+
'external_interface_unknown',
|
|
13
|
+
'output_candidate',
|
|
14
|
+
]);
|
|
7
15
|
function parseCarrierPayload(rest) {
|
|
8
16
|
if (!rest)
|
|
9
17
|
return undefined;
|
|
@@ -40,6 +48,9 @@ export function parseSmartCommentLine(line) {
|
|
|
40
48
|
}
|
|
41
49
|
export function parseSmartCommentLines(line) {
|
|
42
50
|
const trimmed = line.trim();
|
|
51
|
+
const rcIgnoreNext = parseRcIgnoreNextComment(trimmed);
|
|
52
|
+
if (rcIgnoreNext !== undefined)
|
|
53
|
+
return [rcIgnoreNext];
|
|
43
54
|
const expectOut = parseExpectOutComment(trimmed);
|
|
44
55
|
if (expectOut !== undefined)
|
|
45
56
|
return [expectOut];
|
|
@@ -56,6 +67,17 @@ export function parseSmartCommentLines(line) {
|
|
|
56
67
|
}
|
|
57
68
|
return [];
|
|
58
69
|
}
|
|
70
|
+
function parseRcIgnoreNextComment(trimmed) {
|
|
71
|
+
const match = /^;?\s*!\s*rc-ignore-next\s+([A-Za-z0-9_]+)\s*:\s*(.+)$/i.exec(trimmed);
|
|
72
|
+
if (match === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
const findingKind = match[1];
|
|
75
|
+
const reason = match[2]?.trim();
|
|
76
|
+
if (!FINDING_KINDS.has(findingKind) || reason === undefined || reason.length === 0) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return { kind: 'rcIgnoreNext', findingKind, reason };
|
|
80
|
+
}
|
|
59
81
|
function parseSemicolonSeparatedSourceComments(trimmed) {
|
|
60
82
|
const sourcePrefix = /^;?\s*!\s*/.exec(trimmed);
|
|
61
83
|
if (sourcePrefix === null)
|
|
@@ -29,6 +29,7 @@ export function buildProfileSummaries(profileName) {
|
|
|
29
29
|
...profile.rstServices.values(),
|
|
30
30
|
...[...profile.rstDispatchers.values()].flatMap((dispatcher) => [
|
|
31
31
|
...dispatcher.services.values(),
|
|
32
|
+
...(dispatcher.rangeServices?.map((rangeService) => rangeService.summary) ?? []),
|
|
32
33
|
]),
|
|
33
34
|
];
|
|
34
35
|
}
|
|
@@ -47,6 +48,9 @@ export function buildProfileSummaryLookup(profileName) {
|
|
|
47
48
|
for (const summary of dispatcher.services.values()) {
|
|
48
49
|
out.set(summary.name, summary);
|
|
49
50
|
}
|
|
51
|
+
for (const rangeService of dispatcher.rangeServices ?? []) {
|
|
52
|
+
out.set(rangeService.summary.name, rangeService.summary);
|
|
53
|
+
}
|
|
50
54
|
}
|
|
51
55
|
return out;
|
|
52
56
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getZ80InstructionEffect } from '../z80/effects.js';
|
|
2
|
-
import { precedingCServiceName } from './boundaryHints.js';
|
|
2
|
+
import { precedingCServiceName, precedingRegisterImmediateValue } from './boundaryHints.js';
|
|
3
3
|
import { instructionHead } from './instruction-head.js';
|
|
4
|
-
import { rstServiceTargetName, rstTargetName } from './profiles.js';
|
|
4
|
+
import { rstDispatcherServiceTargetNames, rstServiceTargetName, rstTargetName } from './profiles.js';
|
|
5
5
|
export function boundarySummary(routine, index, summaries) {
|
|
6
6
|
const item = routine.instructions[index];
|
|
7
7
|
if (!item)
|
|
@@ -35,6 +35,24 @@ function rstBoundarySummary(routine, index, effect, summaries) {
|
|
|
35
35
|
summaries.get(rstTargetName(effect.control.vector)));
|
|
36
36
|
}
|
|
37
37
|
function rstServiceBoundarySummary(routine, index, vector, summaries) {
|
|
38
|
-
const
|
|
38
|
+
const previous = routine.instructions[index - 1];
|
|
39
|
+
const numericService = precedingRegisterImmediateValue(previous, 'C');
|
|
40
|
+
if (numericService !== undefined) {
|
|
41
|
+
const profileTarget = firstSummary(rstDispatcherServiceTargetNames(vector, (register) => register === 'C' ? numericService : undefined), summaries);
|
|
42
|
+
if (profileTarget !== undefined)
|
|
43
|
+
return profileTarget;
|
|
44
|
+
const numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
|
|
45
|
+
if (numericSummary !== undefined)
|
|
46
|
+
return numericSummary;
|
|
47
|
+
}
|
|
48
|
+
const service = precedingCServiceName(previous);
|
|
39
49
|
return service ? summaries.get(rstServiceTargetName(vector, service)) : undefined;
|
|
40
50
|
}
|
|
51
|
+
function firstSummary(names, summaries) {
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const summary = summaries.get(name);
|
|
54
|
+
if (summary !== undefined)
|
|
55
|
+
return summary;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|