@jhlagado/azm 0.2.12 → 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/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/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/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 +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
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { buildRegisterContractsProgramModel } from './programModel.js';
|
|
2
|
+
import { registerContractsPolicyModeForFile } from './policy.js';
|
|
2
3
|
import { buildRoutineContracts, parseSmartComments } from './smartComments.js';
|
|
3
|
-
import { renderRegisterContractsInterface, renderRegisterContractsReport } from './report.js';
|
|
4
|
+
import { renderRegisterContractsInterface, buildRegisterContractsJsonReport, buildRegisterContractsInference, renderRegisterContractsInferenceMarkdown, renderRegisterContractsJsonReport, renderRegisterContractsReport, } from './report.js';
|
|
5
|
+
import { compareRegisterContractsBaseline } from './ratchet.js';
|
|
4
6
|
import { findCallerOutputCandidateObservations, findRegisterContractsConflicts, } from './liveness.js';
|
|
5
7
|
import { buildAnnotations } from './annotations.js';
|
|
6
|
-
import { autoAcceptedOutputCandidateMap, buildRegisterContractsReportModel,
|
|
7
|
-
import { buildProfileSummaries, buildSummaries, buildSummaryByName, outputCandidateKey,
|
|
8
|
+
import { autoAcceptedOutputCandidateMap, buildRegisterContractsReportModel, diagnosticsForFindings, knownRoutineNames, outputCandidatesWithFixability, strictStackFindings, summariesForAnnotations, unknownBoundaryFindings, } from './analyze-helpers.js';
|
|
9
|
+
import { buildProfileSummaries, buildSummaries, buildSummaryByName, outputCandidateKey, withAcceptedOutputs, } from './summaries.js';
|
|
8
10
|
export function analyzeRegisterContracts(loaded, options) {
|
|
9
11
|
const file = loaded.program.files[0];
|
|
10
12
|
const items = file?.items ?? [];
|
|
11
13
|
const program = buildRegisterContractsProgramModel(items);
|
|
12
14
|
const smartComments = parseSmartComments(loaded.sourceLineComments);
|
|
15
|
+
const suppressionSyntaxDiagnostics = malformedSuppressionDiagnostics(loaded.sourceLineComments, options.mode, options.policy);
|
|
16
|
+
const suppressions = registerContractsSuppressions(smartComments);
|
|
13
17
|
const contractMap = buildRoutineContracts(smartComments, program.routines, loaded.sourceTexts);
|
|
14
18
|
if (options.interfaceContracts !== undefined) {
|
|
15
19
|
for (const contract of options.interfaceContracts) {
|
|
@@ -22,12 +26,28 @@ export function analyzeRegisterContracts(loaded, options) {
|
|
|
22
26
|
let summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
|
|
23
27
|
const knownRoutines = knownRoutineNames(program.routines, contractMap.keys(), options.registerContractsProfile);
|
|
24
28
|
const shouldBuildOutputCandidates = options.mode !== 'off' ||
|
|
29
|
+
options.policy !== undefined ||
|
|
25
30
|
options.emitAnnotations === true ||
|
|
26
|
-
options.fixRegisterContracts === true
|
|
31
|
+
options.fixRegisterContracts === true ||
|
|
32
|
+
options.emitInference === true;
|
|
27
33
|
const outputCandidates = shouldBuildOutputCandidates
|
|
28
34
|
? findCallerOutputCandidateObservations(program.routines, summariesByName)
|
|
29
35
|
: [];
|
|
30
|
-
const
|
|
36
|
+
const suppressedOutputCandidateKeys = new Set(outputCandidates
|
|
37
|
+
.filter((candidate) => isSuppressedFinding(candidate, 'output_candidate', suppressions))
|
|
38
|
+
.map((candidate) => findingKey({
|
|
39
|
+
kind: 'output_candidate',
|
|
40
|
+
file: candidate.file,
|
|
41
|
+
line: candidate.line,
|
|
42
|
+
column: candidate.column,
|
|
43
|
+
})));
|
|
44
|
+
const outputCandidatesForPromotion = outputCandidates.filter((candidate) => !suppressedOutputCandidateKeys.has(findingKey({
|
|
45
|
+
kind: 'output_candidate',
|
|
46
|
+
file: candidate.file,
|
|
47
|
+
line: candidate.line,
|
|
48
|
+
column: candidate.column,
|
|
49
|
+
})));
|
|
50
|
+
const autoAcceptedOutputs = autoAcceptedOutputCandidateMap(program.routines, outputCandidatesForPromotion, loaded.sourceTexts);
|
|
31
51
|
if (autoAcceptedOutputs.size > 0) {
|
|
32
52
|
summaries = withAcceptedOutputs(summaries, autoAcceptedOutputs);
|
|
33
53
|
summariesByName = buildSummaryByName(program.routines, summaries, profileSummaries);
|
|
@@ -35,39 +55,356 @@ export function analyzeRegisterContracts(loaded, options) {
|
|
|
35
55
|
const conflicts = shouldBuildOutputCandidates
|
|
36
56
|
? program.routines.flatMap((routine) => findRegisterContractsConflicts(routine, summariesByName, smartComments))
|
|
37
57
|
: [];
|
|
38
|
-
const { outputCandidates: outputCandidatesWithAutoFixability, outputCandidateFixability } = outputCandidatesWithFixability(program.routines,
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
const { outputCandidates: outputCandidatesWithAutoFixability, outputCandidateFixability } = outputCandidatesWithFixability(program.routines, outputCandidatesForPromotion);
|
|
59
|
+
const { outputCandidates: allOutputCandidatesWithAutoFixability } = outputCandidatesWithFixability(program.routines, outputCandidates);
|
|
60
|
+
const diagnostics = [...suppressionSyntaxDiagnostics];
|
|
61
|
+
const unknownFindings = unknownBoundaryFindings(program.directBoundaries, knownRoutines);
|
|
62
|
+
const stackFindings = strictStackFindings(program.routines, summaries);
|
|
63
|
+
const scopedBoundaryFindings = scopedBoundaryContractFindings({
|
|
64
|
+
directBoundaries: program.directBoundaries,
|
|
65
|
+
routines: program.routines,
|
|
66
|
+
contractMap,
|
|
67
|
+
summariesByName,
|
|
68
|
+
profileSummaryNames: new Set(profileSummaries.map((summary) => summary.name)),
|
|
69
|
+
policy: options.policy,
|
|
70
|
+
mode: options.mode,
|
|
71
|
+
});
|
|
72
|
+
const findings = options.mode === 'off' && options.policy === undefined
|
|
73
|
+
? []
|
|
74
|
+
: [
|
|
75
|
+
...conflicts.map((conflict) => ({
|
|
76
|
+
kind: conflict.kind ?? 'definite_contract_violation',
|
|
77
|
+
callTarget: conflict.callTarget,
|
|
78
|
+
file: conflict.file,
|
|
79
|
+
line: conflict.line,
|
|
80
|
+
column: conflict.column,
|
|
81
|
+
...(conflict.sourceUnit !== undefined ? { sourceUnit: conflict.sourceUnit } : {}),
|
|
82
|
+
...(conflict.sourceRelation !== undefined
|
|
83
|
+
? { sourceRelation: conflict.sourceRelation }
|
|
84
|
+
: {}),
|
|
85
|
+
...(conflict.sourceUnitRelation !== undefined
|
|
86
|
+
? { sourceUnitRelation: conflict.sourceUnitRelation }
|
|
87
|
+
: {}),
|
|
88
|
+
...(conflict.routine !== undefined ? { routine: conflict.routine } : {}),
|
|
89
|
+
carriers: conflict.carriers,
|
|
90
|
+
message: conflict.message,
|
|
91
|
+
})),
|
|
92
|
+
...unknownFindings,
|
|
93
|
+
...stackFindings,
|
|
94
|
+
...outputCandidatesWithAutoFixability.map((candidate) => {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'output_candidate',
|
|
97
|
+
routine: candidate.routine,
|
|
98
|
+
file: candidate.file,
|
|
99
|
+
line: candidate.line,
|
|
100
|
+
column: candidate.column,
|
|
101
|
+
...(candidate.sourceUnit !== undefined ? { sourceUnit: candidate.sourceUnit } : {}),
|
|
102
|
+
...(candidate.sourceRelation !== undefined
|
|
103
|
+
? { sourceRelation: candidate.sourceRelation }
|
|
104
|
+
: {}),
|
|
105
|
+
...(candidate.sourceUnitRelation !== undefined
|
|
106
|
+
? { sourceUnitRelation: candidate.sourceUnitRelation }
|
|
107
|
+
: {}),
|
|
108
|
+
carriers: candidate.carriers,
|
|
109
|
+
message: candidate.message,
|
|
110
|
+
...(candidate.autoFixable !== undefined ? { autoFixable: candidate.autoFixable } : {}),
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
...scopedBoundaryFindings,
|
|
114
|
+
];
|
|
115
|
+
const { activeFindings, suppressedFindings: directlySuppressedFindings } = applyRegisterContractsSuppressions(findings, suppressions);
|
|
116
|
+
const suppressedOutputCandidateFindings = allOutputCandidatesWithAutoFixability
|
|
117
|
+
.filter((candidate) => suppressedOutputCandidateKeys.has(findingKey({ ...candidate, kind: 'output_candidate' })))
|
|
118
|
+
.map((candidate) => {
|
|
119
|
+
const suppression = suppressions.find((item) => item.file === candidate.file &&
|
|
120
|
+
item.line === candidate.line &&
|
|
121
|
+
item.findingKind === 'output_candidate');
|
|
122
|
+
if (suppression === undefined)
|
|
123
|
+
return undefined;
|
|
124
|
+
return {
|
|
125
|
+
suppression,
|
|
126
|
+
finding: {
|
|
127
|
+
kind: 'output_candidate',
|
|
128
|
+
routine: candidate.routine,
|
|
129
|
+
file: candidate.file,
|
|
130
|
+
line: candidate.line,
|
|
131
|
+
column: candidate.column,
|
|
132
|
+
...(candidate.sourceUnit !== undefined ? { sourceUnit: candidate.sourceUnit } : {}),
|
|
133
|
+
...(candidate.sourceRelation !== undefined ? { sourceRelation: candidate.sourceRelation } : {}),
|
|
134
|
+
...(candidate.sourceUnitRelation !== undefined
|
|
135
|
+
? { sourceUnitRelation: candidate.sourceUnitRelation }
|
|
136
|
+
: {}),
|
|
137
|
+
carriers: candidate.carriers,
|
|
138
|
+
message: candidate.message,
|
|
139
|
+
...(candidate.autoFixable !== undefined ? { autoFixable: candidate.autoFixable } : {}),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
})
|
|
143
|
+
.filter((item) => item !== undefined);
|
|
144
|
+
const suppressedFindings = [
|
|
145
|
+
...directlySuppressedFindings,
|
|
146
|
+
...suppressedOutputCandidateFindings,
|
|
147
|
+
];
|
|
148
|
+
const activeOutputCandidates = outputCandidatesWithAutoFixability.filter((candidate) => !suppressedOutputCandidateKeys.has(findingKey({ ...candidate, kind: 'output_candidate' })));
|
|
149
|
+
const activeConflictFindings = activeFindings.filter((finding) => finding.kind === 'definite_contract_violation' || finding.kind === 'flag_lifetime_risk');
|
|
150
|
+
if (options.policy !== undefined) {
|
|
151
|
+
diagnostics.push(...diagnosticsForScopedPolicy(activeFindings, options.policy, options.mode));
|
|
152
|
+
}
|
|
153
|
+
else if (options.mode === 'strict') {
|
|
154
|
+
diagnostics.push(...diagnosticsForFindings(activeConflictFindings, options.mode));
|
|
155
|
+
diagnostics.push(...diagnosticsForFindings(activeFindings.filter((finding) => finding.kind === 'missing_callee_contract' || finding.kind === 'unknown_control_flow'), 'strict'));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
diagnostics.push(...diagnosticsForFindings(activeConflictFindings, options.mode));
|
|
43
159
|
}
|
|
44
160
|
const reportModel = buildRegisterContractsReportModel({
|
|
45
161
|
entryFile: loaded.program.entryFile,
|
|
46
162
|
mode: options.mode,
|
|
47
163
|
summaries,
|
|
48
164
|
profileSummaries,
|
|
49
|
-
|
|
50
|
-
|
|
165
|
+
findings: activeFindings,
|
|
166
|
+
...(suppressedFindings.length > 0 ? { suppressedFindings } : {}),
|
|
167
|
+
conflicts: conflicts.filter((conflict) => activeConflictFindings.some((finding) => finding.file === conflict.file &&
|
|
168
|
+
finding.line === conflict.line &&
|
|
169
|
+
finding.column === conflict.column &&
|
|
170
|
+
'callTarget' in finding &&
|
|
171
|
+
finding.callTarget === conflict.callTarget)),
|
|
172
|
+
outputCandidates: activeOutputCandidates,
|
|
51
173
|
profile: options.registerContractsProfile,
|
|
52
174
|
directBoundaries: program.directBoundaries,
|
|
53
175
|
knownRoutines,
|
|
54
176
|
});
|
|
55
|
-
|
|
177
|
+
if (options.baselineReport !== undefined) {
|
|
178
|
+
const currentJson = buildRegisterContractsJsonReport(reportModel);
|
|
179
|
+
const ratchet = compareRegisterContractsBaseline(currentJson, options.baselineReport, options.baselineFile);
|
|
180
|
+
reportModel.ratchet = ratchet;
|
|
181
|
+
if (options.ratchet === true) {
|
|
182
|
+
for (const entry of ratchet.newFindings) {
|
|
183
|
+
diagnostics.push({
|
|
184
|
+
severity: 'error',
|
|
185
|
+
code: 'AZMN_REGISTER_CONTRACTS',
|
|
186
|
+
sourceName: entry.finding.location.file,
|
|
187
|
+
line: entry.finding.location.line,
|
|
188
|
+
column: entry.finding.location.column,
|
|
189
|
+
message: `Register contract ratchet found new ${entry.finding.kind}: ${entry.finding.message}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const entry of ratchet.changedFindings) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
severity: 'error',
|
|
195
|
+
code: 'AZMN_REGISTER_CONTRACTS',
|
|
196
|
+
sourceName: entry.current.location.file,
|
|
197
|
+
line: entry.current.location.line,
|
|
198
|
+
column: entry.current.location.column,
|
|
199
|
+
message: `Register contract ratchet found changed ${entry.current.kind}: ${entry.current.message}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const summariesForAnnotationsByName = summariesForAnnotations(summariesByName, activeOutputCandidates);
|
|
56
205
|
const annotations = options.emitAnnotations
|
|
57
|
-
? buildAnnotations(loaded, program.routines, summariesForAnnotationsByName,
|
|
206
|
+
? buildAnnotations(loaded, program.routines, summariesForAnnotationsByName, activeOutputCandidates, {
|
|
58
207
|
fixOutputCandidates: options.fixRegisterContracts === true,
|
|
59
208
|
outputCandidateFixability,
|
|
60
209
|
outputCandidateKey,
|
|
61
210
|
})
|
|
62
211
|
: [];
|
|
212
|
+
const renderedJsonReport = options.emitReport && (options.reportFormat ?? 'text') === 'json'
|
|
213
|
+
? renderRegisterContractsJsonReport(reportModel)
|
|
214
|
+
: undefined;
|
|
215
|
+
const canonicalSummariesByName = new Map(summaries.map((summary) => [summary.name, summary]));
|
|
216
|
+
const activeOutputCandidatesForInference = activeOutputCandidates.map((candidate) => {
|
|
217
|
+
const canonicalName = summariesByName.get(candidate.routine)?.name;
|
|
218
|
+
return canonicalName === undefined || canonicalName === candidate.routine
|
|
219
|
+
? candidate
|
|
220
|
+
: { ...candidate, routine: canonicalName };
|
|
221
|
+
});
|
|
222
|
+
const summariesForInference = summariesForAnnotations(canonicalSummariesByName, activeOutputCandidatesForInference);
|
|
223
|
+
const inferenceModel = options.emitInference
|
|
224
|
+
? buildRegisterContractsInference([...summariesForInference.values()])
|
|
225
|
+
: undefined;
|
|
226
|
+
const inferenceFormat = options.inferenceFormat ?? 'json';
|
|
63
227
|
return {
|
|
64
228
|
diagnostics,
|
|
65
|
-
|
|
66
|
-
|
|
229
|
+
...(activeFindings.length > 0 ? { findings: activeFindings } : {}),
|
|
230
|
+
outputCandidates: activeOutputCandidates,
|
|
231
|
+
...(options.emitReport
|
|
232
|
+
? renderedJsonReport !== undefined
|
|
233
|
+
? {
|
|
234
|
+
reportText: renderedJsonReport.text,
|
|
235
|
+
reportJson: renderedJsonReport.json,
|
|
236
|
+
reportFormat: 'json',
|
|
237
|
+
}
|
|
238
|
+
: { reportText: renderRegisterContractsReport(reportModel), reportFormat: 'text' }
|
|
239
|
+
: {}),
|
|
67
240
|
...(options.emitInterface
|
|
68
241
|
? { interfaceText: renderRegisterContractsInterface(summaries) }
|
|
69
242
|
: {}),
|
|
243
|
+
...(inferenceModel !== undefined
|
|
244
|
+
? inferenceFormat === 'markdown'
|
|
245
|
+
? {
|
|
246
|
+
inferenceText: renderRegisterContractsInferenceMarkdown(inferenceModel),
|
|
247
|
+
inferenceJson: inferenceModel,
|
|
248
|
+
inferenceFormat,
|
|
249
|
+
}
|
|
250
|
+
: {
|
|
251
|
+
inferenceText: `${JSON.stringify(inferenceModel, null, 2)}\n`,
|
|
252
|
+
inferenceJson: inferenceModel,
|
|
253
|
+
inferenceFormat,
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
70
256
|
...(annotations.length > 0 ? { annotations } : {}),
|
|
71
257
|
...(reportModel.unknownCalls.length > 0 ? { unknownCalls: reportModel.unknownCalls } : {}),
|
|
72
258
|
};
|
|
73
259
|
}
|
|
260
|
+
function isSuppressedFinding(finding, kind, suppressions) {
|
|
261
|
+
return suppressions.some((item) => item.file === finding.file &&
|
|
262
|
+
item.line === finding.line &&
|
|
263
|
+
item.findingKind === kind);
|
|
264
|
+
}
|
|
265
|
+
function registerContractsSuppressions(comments) {
|
|
266
|
+
return comments
|
|
267
|
+
.filter((item) => item.comment.kind === 'rcIgnoreNext')
|
|
268
|
+
.map((item) => {
|
|
269
|
+
if (item.comment.kind !== 'rcIgnoreNext') {
|
|
270
|
+
throw new Error('unreachable');
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
file: item.file,
|
|
274
|
+
line: item.line + 1,
|
|
275
|
+
column: 1,
|
|
276
|
+
findingKind: item.comment.findingKind,
|
|
277
|
+
reason: item.comment.reason,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function applyRegisterContractsSuppressions(findings, suppressions) {
|
|
282
|
+
const suppressedFindings = [];
|
|
283
|
+
const activeFindings = [];
|
|
284
|
+
for (const finding of findings) {
|
|
285
|
+
const suppression = suppressions.find((item) => item.file === finding.file &&
|
|
286
|
+
item.line === finding.line &&
|
|
287
|
+
item.findingKind === finding.kind);
|
|
288
|
+
if (suppression === undefined) {
|
|
289
|
+
activeFindings.push(finding);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
suppressedFindings.push({ finding, suppression });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { activeFindings, suppressedFindings };
|
|
296
|
+
}
|
|
297
|
+
function malformedSuppressionDiagnostics(sourceLineComments, mode, policy) {
|
|
298
|
+
const diagnostics = [];
|
|
299
|
+
for (const [file, comments] of sourceLineComments) {
|
|
300
|
+
for (const [line, text] of comments) {
|
|
301
|
+
const commentText = `;${text}`;
|
|
302
|
+
if (!/^;?\s*!\s*rc-ignore-next\b/iu.test(commentText))
|
|
303
|
+
continue;
|
|
304
|
+
if (!isStrictSuppressionContext(file, mode, policy))
|
|
305
|
+
continue;
|
|
306
|
+
const parsed = parseSmartComments(new Map([[file, new Map([[line, text]])]]));
|
|
307
|
+
if (parsed.some((item) => item.comment.kind === 'rcIgnoreNext'))
|
|
308
|
+
continue;
|
|
309
|
+
diagnostics.push({
|
|
310
|
+
severity: 'error',
|
|
311
|
+
code: 'AZMN_REGISTER_CONTRACTS',
|
|
312
|
+
sourceName: file,
|
|
313
|
+
line,
|
|
314
|
+
column: 1,
|
|
315
|
+
message: 'Malformed register-contract suppression; use `;! rc-ignore-next <finding-kind>: <reason>`.',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return diagnostics;
|
|
320
|
+
}
|
|
321
|
+
function isStrictSuppressionContext(file, mode, policy) {
|
|
322
|
+
if (policy !== undefined) {
|
|
323
|
+
return registerContractsPolicyModeForFile(file, policy, mode) === 'strict';
|
|
324
|
+
}
|
|
325
|
+
return mode === 'strict' || mode === 'error';
|
|
326
|
+
}
|
|
327
|
+
function findingKey(finding) {
|
|
328
|
+
return `${finding.kind ?? ''}:${finding.file}:${finding.line}:${finding.column}`;
|
|
329
|
+
}
|
|
330
|
+
function scopedBoundaryContractFindings(input) {
|
|
331
|
+
if (input.policy === undefined)
|
|
332
|
+
return [];
|
|
333
|
+
const routinesByLabel = routinesByBoundaryLabel(input.routines);
|
|
334
|
+
const out = [];
|
|
335
|
+
for (const boundary of input.directBoundaries) {
|
|
336
|
+
const callerMode = policyModeForFile(boundary.file, input.policy, input.mode);
|
|
337
|
+
if (callerMode !== 'strict')
|
|
338
|
+
continue;
|
|
339
|
+
const targetRoutine = routinesByLabel.get(boundary.target);
|
|
340
|
+
if (targetRoutine === undefined)
|
|
341
|
+
continue;
|
|
342
|
+
const targetMode = policyModeForFile(targetRoutine.span.file, input.policy, input.mode);
|
|
343
|
+
if (targetMode === 'strict')
|
|
344
|
+
continue;
|
|
345
|
+
if (hasExplicitBoundaryContract(boundary.target, input.contractMap, input.summariesByName, input.profileSummaryNames)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const routine = routineNameForBoundary(boundary.file, boundary.line, input.routines);
|
|
349
|
+
const targetDescription = registerContractsPolicyModeDescription(targetMode);
|
|
350
|
+
out.push({
|
|
351
|
+
kind: 'external_interface_unknown',
|
|
352
|
+
callTarget: boundary.target,
|
|
353
|
+
subject: boundary.subject,
|
|
354
|
+
file: boundary.file,
|
|
355
|
+
line: boundary.line,
|
|
356
|
+
column: boundary.column,
|
|
357
|
+
...(boundary.sourceUnit !== undefined ? { sourceUnit: boundary.sourceUnit } : {}),
|
|
358
|
+
...(boundary.sourceRelation !== undefined ? { sourceRelation: boundary.sourceRelation } : {}),
|
|
359
|
+
...(boundary.sourceUnitRelation !== undefined
|
|
360
|
+
? { sourceUnitRelation: boundary.sourceUnitRelation }
|
|
361
|
+
: {}),
|
|
362
|
+
...(routine !== undefined ? { routine } : {}),
|
|
363
|
+
message: `strict register-contract source calls ${targetDescription} ${boundary.target}; add an explicit source, .asmi, or profile contract at the boundary.`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function routinesByBoundaryLabel(routines) {
|
|
369
|
+
const out = new Map();
|
|
370
|
+
for (const routine of routines) {
|
|
371
|
+
for (const label of routine.labels)
|
|
372
|
+
out.set(label, routine);
|
|
373
|
+
for (const label of routine.entryLabels)
|
|
374
|
+
out.set(label, routine);
|
|
375
|
+
out.set(routine.name, routine);
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
function routineNameForBoundary(file, line, routines) {
|
|
380
|
+
return routines.find((routine) => routine.span.file === file &&
|
|
381
|
+
routine.span.start.line <= line &&
|
|
382
|
+
routine.span.end.line >= line)?.name;
|
|
383
|
+
}
|
|
384
|
+
function hasExplicitBoundaryContract(target, contractMap, summariesByName, profileSummaryNames) {
|
|
385
|
+
if (contractMap.has(target))
|
|
386
|
+
return true;
|
|
387
|
+
const summary = summariesByName.get(target);
|
|
388
|
+
if (summary === undefined)
|
|
389
|
+
return false;
|
|
390
|
+
return contractMap.has(summary.name) || profileSummaryNames.has(target) || profileSummaryNames.has(summary.name);
|
|
391
|
+
}
|
|
392
|
+
function diagnosticsForScopedPolicy(findings, policy, fallbackMode) {
|
|
393
|
+
return findings
|
|
394
|
+
.filter((finding) => policyModeForFile(finding.file, policy, fallbackMode) === 'strict')
|
|
395
|
+
.filter((finding) => finding.kind !== 'output_candidate')
|
|
396
|
+
.map((finding) => ({
|
|
397
|
+
severity: 'error',
|
|
398
|
+
code: 'AZMN_REGISTER_CONTRACTS',
|
|
399
|
+
sourceName: finding.file,
|
|
400
|
+
line: finding.line,
|
|
401
|
+
column: finding.column,
|
|
402
|
+
message: finding.message,
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
function policyModeForFile(file, policy, fallbackMode) {
|
|
406
|
+
return registerContractsPolicyModeForFile(file, policy, fallbackMode);
|
|
407
|
+
}
|
|
408
|
+
function registerContractsPolicyModeDescription(mode) {
|
|
409
|
+
return mode === 'off' ? 'disabled' : `${mode}ed`;
|
|
410
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expandCarrierList } from './carriers.js';
|
|
2
|
+
import { rstServiceTargetName } from './profiles.js';
|
|
2
3
|
import { buildRoutineContracts } from './smartComments.js';
|
|
3
4
|
const INTERFACE_TAG_RE = /^\s*(in|out|clobbers|preserves)(?:\s+(.+))?$/i;
|
|
4
5
|
const INTERFACE_CONTRACT_BUILDERS = {
|
|
@@ -9,6 +10,7 @@ const INTERFACE_CONTRACT_BUILDERS = {
|
|
|
9
10
|
};
|
|
10
11
|
export function parseInterfaceContracts(text, file = '<register-contracts-interface>') {
|
|
11
12
|
const comments = [];
|
|
13
|
+
const serviceAliases = new Map();
|
|
12
14
|
const lines = text.split(/\r?\n/u);
|
|
13
15
|
for (const [index, line] of lines.entries()) {
|
|
14
16
|
const trimmed = line.trim();
|
|
@@ -21,9 +23,23 @@ export function parseInterfaceContracts(text, file = '<register-contracts-interf
|
|
|
21
23
|
if (comment === undefined) {
|
|
22
24
|
throw new Error(`${file}:${index + 1}: invalid register contracts interface line \"${trimmed}\"`);
|
|
23
25
|
}
|
|
26
|
+
if (comment.kind === 'extern') {
|
|
27
|
+
const serviceAliasesForComment = parseInterfaceServiceAliases(trimmed);
|
|
28
|
+
if (serviceAliasesForComment !== undefined) {
|
|
29
|
+
serviceAliases.set(comment.name, serviceAliasesForComment);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
comments.push({ file, line: index + 1, comment });
|
|
25
33
|
}
|
|
26
34
|
const routines = buildRoutineContracts(comments);
|
|
35
|
+
for (const [primary, aliases] of serviceAliases) {
|
|
36
|
+
const contract = routines.get(primary);
|
|
37
|
+
if (contract === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
for (const alias of aliases) {
|
|
40
|
+
routines.set(alias, { ...contract, name: alias });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
27
43
|
const out = new Map();
|
|
28
44
|
for (const [name, contract] of routines) {
|
|
29
45
|
if (hasContractContent(contract))
|
|
@@ -46,11 +62,40 @@ function parseInterfaceContractLine(line) {
|
|
|
46
62
|
return carriers === undefined ? undefined : INTERFACE_CONTRACT_BUILDERS[tag](carriers);
|
|
47
63
|
}
|
|
48
64
|
function parseInterfaceBoundary(trimmed) {
|
|
65
|
+
const service = parseInterfaceService(trimmed);
|
|
66
|
+
if (service !== undefined)
|
|
67
|
+
return { kind: 'extern', name: service.primary };
|
|
49
68
|
const extern = /^extern\s+(\S+)\s*$/i.exec(trimmed);
|
|
50
69
|
if (extern !== null)
|
|
51
70
|
return { kind: 'extern', name: extern[1] };
|
|
52
71
|
return /^end\s*$/i.test(trimmed) ? { kind: 'end' } : undefined;
|
|
53
72
|
}
|
|
73
|
+
function parseInterfaceServiceAliases(trimmed) {
|
|
74
|
+
return parseInterfaceService(trimmed)?.aliases;
|
|
75
|
+
}
|
|
76
|
+
function parseInterfaceService(trimmed) {
|
|
77
|
+
const match = /^service\s+rst\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+(\S+))?\s*$/i.exec(trimmed);
|
|
78
|
+
if (match === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
const vector = parseInterfaceNumber(match[1]);
|
|
81
|
+
const selector = match[2].toUpperCase();
|
|
82
|
+
const value = parseInterfaceNumber(match[3]);
|
|
83
|
+
if (vector === undefined || value === undefined || selector !== 'C')
|
|
84
|
+
return undefined;
|
|
85
|
+
const primary = rstServiceTargetName(vector, String(value));
|
|
86
|
+
const name = match[4];
|
|
87
|
+
const aliases = name === undefined ? [] : [rstServiceTargetName(vector, name)];
|
|
88
|
+
return { primary, aliases };
|
|
89
|
+
}
|
|
90
|
+
function parseInterfaceNumber(raw) {
|
|
91
|
+
const trimmed = raw.trim();
|
|
92
|
+
const value = trimmed.startsWith('$')
|
|
93
|
+
? Number.parseInt(trimmed.slice(1), 16)
|
|
94
|
+
: /^0x/iu.test(trimmed)
|
|
95
|
+
? Number.parseInt(trimmed.slice(2), 16)
|
|
96
|
+
: Number.parseInt(trimmed, 10);
|
|
97
|
+
return Number.isInteger(value) && value >= 0 && value <= 0xff ? value : undefined;
|
|
98
|
+
}
|
|
54
99
|
function parseInterfaceCarrierList(rest) {
|
|
55
100
|
if (!rest)
|
|
56
101
|
return undefined;
|
|
@@ -49,7 +49,9 @@ function rstBoundaryTarget(routine, index, effect) {
|
|
|
49
49
|
function rstBoundaryTargets(routine, index, vector, fallbackTarget) {
|
|
50
50
|
const previous = routine.instructions[index - 1];
|
|
51
51
|
const service = precedingCServiceName(previous);
|
|
52
|
+
const numericService = precedingRegisterImmediateValue(previous, 'C');
|
|
52
53
|
return [
|
|
54
|
+
...(numericService !== undefined ? [rstServiceTargetName(vector, String(numericService))] : []),
|
|
53
55
|
...rstDispatcherServiceTargetNames(vector, (register) => precedingRegisterImmediateValue(previous, register)),
|
|
54
56
|
...(service ? [rstServiceTargetName(vector, service)] : []),
|
|
55
57
|
fallbackTarget,
|
|
@@ -195,9 +197,18 @@ export function findRegisterContractsConflicts(routine, summaries, hints) {
|
|
|
195
197
|
const carriers = unique(summary.mayWrite.filter((unit) => liveOut[index].has(unit) && !accepted.has(unit)));
|
|
196
198
|
if (carriers.length > 0) {
|
|
197
199
|
conflicts.push({
|
|
200
|
+
kind: carriers.every((unit) => isFlagUnit(unit))
|
|
201
|
+
? 'flag_lifetime_risk'
|
|
202
|
+
: 'definite_contract_violation',
|
|
198
203
|
file: item.file,
|
|
199
204
|
line: item.line,
|
|
200
205
|
column: item.column,
|
|
206
|
+
...(item.sourceUnit !== undefined ? { sourceUnit: item.sourceUnit } : {}),
|
|
207
|
+
...(item.sourceRelation !== undefined ? { sourceRelation: item.sourceRelation } : {}),
|
|
208
|
+
...(item.sourceUnitRelation !== undefined
|
|
209
|
+
? { sourceUnitRelation: item.sourceUnitRelation }
|
|
210
|
+
: {}),
|
|
211
|
+
routine: routine.name,
|
|
201
212
|
callTarget: target,
|
|
202
213
|
carriers,
|
|
203
214
|
message: `${boundary.subject} may modify ${carriers.join(',')}, but the pre-call value is used later.`,
|
|
@@ -206,6 +217,13 @@ export function findRegisterContractsConflicts(routine, summaries, hints) {
|
|
|
206
217
|
}
|
|
207
218
|
return conflicts;
|
|
208
219
|
}
|
|
220
|
+
function isFlagUnit(unit) {
|
|
221
|
+
return (unit === 'carry' ||
|
|
222
|
+
unit === 'zero' ||
|
|
223
|
+
unit === 'sign' ||
|
|
224
|
+
unit === 'parity' ||
|
|
225
|
+
unit === 'halfCarry');
|
|
226
|
+
}
|
|
209
227
|
function candidateMessage(boundary, units) {
|
|
210
228
|
const carriers = units.join(',');
|
|
211
229
|
const expectation = units.length === 1 ? units[0] : `{${carriers}}`;
|
|
@@ -230,6 +248,11 @@ function callerOutputCandidate(item, boundary, target, summary, liveAfter) {
|
|
|
230
248
|
file: item.file,
|
|
231
249
|
line: item.line,
|
|
232
250
|
column: item.column,
|
|
251
|
+
...(item.sourceUnit !== undefined ? { sourceUnit: item.sourceUnit } : {}),
|
|
252
|
+
...(item.sourceRelation !== undefined ? { sourceRelation: item.sourceRelation } : {}),
|
|
253
|
+
...(item.sourceUnitRelation !== undefined
|
|
254
|
+
? { sourceUnitRelation: item.sourceUnitRelation }
|
|
255
|
+
: {}),
|
|
233
256
|
routine: target,
|
|
234
257
|
carriers,
|
|
235
258
|
message: candidateMessage(boundary, carriers),
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { RegisterContractsMode, RegisterContractsPolicy, RegisterContractsPolicyMode } from './types.js';
|
|
2
|
+
export declare function registerContractsPolicyModeForFile(file: string, policy: RegisterContractsPolicy, fallbackMode: RegisterContractsMode | undefined): RegisterContractsPolicyMode;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const POLICY_MODE_PRIORITY = {
|
|
2
|
+
strict: 3,
|
|
3
|
+
audit: 2,
|
|
4
|
+
off: 1,
|
|
5
|
+
};
|
|
6
|
+
export function registerContractsPolicyModeForFile(file, policy, fallbackMode) {
|
|
7
|
+
const normalized = normalizePolicyPath(file);
|
|
8
|
+
const matches = [
|
|
9
|
+
...policyMatchesForMode(normalized, policy.strict, 'strict'),
|
|
10
|
+
...policyMatchesForMode(normalized, policy.audit, 'audit'),
|
|
11
|
+
...policyMatchesForMode(normalized, policy.off, 'off'),
|
|
12
|
+
].sort(comparePolicyMatches);
|
|
13
|
+
return matches[0]?.mode ?? fallbackPolicyMode(fallbackMode);
|
|
14
|
+
}
|
|
15
|
+
function policyMatchesForMode(file, patterns, mode) {
|
|
16
|
+
return (patterns
|
|
17
|
+
?.map((pattern) => {
|
|
18
|
+
const normalized = normalizePolicyPath(pattern);
|
|
19
|
+
if (!matchPolicyPattern(file, normalized))
|
|
20
|
+
return undefined;
|
|
21
|
+
return { mode, specificity: policyPatternSpecificity(normalized) };
|
|
22
|
+
})
|
|
23
|
+
.filter((match) => match !== undefined) ?? []);
|
|
24
|
+
}
|
|
25
|
+
function comparePolicyMatches(left, right) {
|
|
26
|
+
return (right.specificity - left.specificity ||
|
|
27
|
+
POLICY_MODE_PRIORITY[right.mode] - POLICY_MODE_PRIORITY[left.mode]);
|
|
28
|
+
}
|
|
29
|
+
function fallbackPolicyMode(fallbackMode) {
|
|
30
|
+
return fallbackMode === 'strict' || fallbackMode === 'error' || fallbackMode === 'warn'
|
|
31
|
+
? 'strict'
|
|
32
|
+
: fallbackMode === 'off'
|
|
33
|
+
? 'off'
|
|
34
|
+
: 'audit';
|
|
35
|
+
}
|
|
36
|
+
function normalizePolicyPath(path) {
|
|
37
|
+
return path.replace(/\\/g, '/');
|
|
38
|
+
}
|
|
39
|
+
function policyPatternSpecificity(pattern) {
|
|
40
|
+
return pattern.replace(/\*/g, '').length;
|
|
41
|
+
}
|
|
42
|
+
function matchPolicyPattern(file, pattern) {
|
|
43
|
+
if (pattern === file)
|
|
44
|
+
return true;
|
|
45
|
+
if (!pattern.includes('*'))
|
|
46
|
+
return false;
|
|
47
|
+
const globStar = '\u0000';
|
|
48
|
+
const escaped = pattern
|
|
49
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
50
|
+
.replace(/\*\*/g, globStar)
|
|
51
|
+
.replace(/\*/g, '[^/]*')
|
|
52
|
+
.replaceAll(globStar, '.*');
|
|
53
|
+
return new RegExp(`^${escaped}$`).test(file);
|
|
54
|
+
}
|
|
@@ -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
|
}
|