@jhlagado/azm 0.2.11 → 0.2.13
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/assembly/import-visibility.js +108 -33
- 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/core/compile.js +1 -0
- package/dist/src/expansion/op-expand-selected.js +8 -1
- package/dist/src/expansion/op-expansion.d.ts +3 -0
- package/dist/src/expansion/op-expansion.js +12 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/node/source-host.js +5 -2
- 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/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/summary-boundary.js +9 -2
- 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 +157 -0
- package/dist/src/source/logical-lines.d.ts +1 -0
- package/dist/src/source/source-span.d.ts +1 -0
- package/dist/src/syntax/parse-layout-declarations.js +1 -0
- package/dist/src/syntax/parse-line.js +4 -0
- package/docs/codebase/02-source-loading-and-parsing.md +10 -6
- package/docs/codebase/04-ops-and-register-contracts.md +49 -4
- package/docs/codebase/05-interfaces-and-output-artifacts.md +56 -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
|
@@ -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)
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { rstServiceTargetName, rstTargetName } from './profiles.js';
|
|
5
5
|
export function boundarySummary(routine, index, summaries) {
|
|
@@ -35,6 +35,13 @@ 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 numericSummary = summaries.get(rstServiceTargetName(vector, String(numericService)));
|
|
42
|
+
if (numericSummary !== undefined)
|
|
43
|
+
return numericSummary;
|
|
44
|
+
}
|
|
45
|
+
const service = precedingCServiceName(previous);
|
|
39
46
|
return service ? summaries.get(rstServiceTargetName(vector, service)) : undefined;
|
|
40
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Diagnostic } from '../model/diagnostic.js';
|
|
2
2
|
import type { LoadedProgram } from '../tooling/api.js';
|
|
3
|
-
import type { AnalyzeRegisterContractsOptions, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
|
|
3
|
+
import type { AnalyzeRegisterContractsOptions, RegisterContractsFinding, RegisterContractsOutputCandidate, RegisterContractsUnit } from './types.js';
|
|
4
4
|
export interface RegisterContractsTextEdit {
|
|
5
5
|
file: string;
|
|
6
6
|
line: number;
|
|
@@ -42,6 +42,7 @@ export interface AnalyzeRegisterContractsForToolsOptions extends Omit<AnalyzeReg
|
|
|
42
42
|
export type AnalyzeRegisterCareForToolsOptions = AnalyzeRegisterContractsForToolsOptions;
|
|
43
43
|
export interface AnalyzeRegisterContractsForToolsResult {
|
|
44
44
|
diagnostics: Diagnostic[];
|
|
45
|
+
findings: RegisterContractsFinding[];
|
|
45
46
|
outputCandidates: RegisterContractsOutputCandidate[];
|
|
46
47
|
candidateDiagnostics: RegisterContractsCandidateDiagnostic[];
|
|
47
48
|
codeActions: RegisterContractsCodeAction[];
|
|
@@ -46,12 +46,14 @@ export function analyzeRegisterContractsForTools(loaded, options) {
|
|
|
46
46
|
? baseResultOptions
|
|
47
47
|
: { ...baseResultOptions, registerContractsProfile: profile });
|
|
48
48
|
const outputCandidates = result.outputCandidates ?? [];
|
|
49
|
+
const findings = result.findings ?? [];
|
|
49
50
|
const candidateDiagnostics = outputCandidates.map(diagnosticForOutputCandidate);
|
|
50
51
|
const codeActions = outputCandidates
|
|
51
52
|
.filter((candidate) => candidate.autoFixable === true)
|
|
52
53
|
.map(codeActionForOutputCandidate);
|
|
53
54
|
return {
|
|
54
55
|
diagnostics: result.diagnostics,
|
|
56
|
+
findings,
|
|
55
57
|
outputCandidates,
|
|
56
58
|
candidateDiagnostics,
|
|
57
59
|
codeActions,
|