@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.
Files changed (52) hide show
  1. package/dist/src/api-compile.d.ts +7 -1
  2. package/dist/src/api-compile.js +17 -5
  3. package/dist/src/api-register-contracts.js +69 -2
  4. package/dist/src/api-tooling.d.ts +1 -1
  5. package/dist/src/assembly/import-visibility.js +108 -33
  6. package/dist/src/cli/artifact-files.d.ts +1 -0
  7. package/dist/src/cli/artifact-files.js +5 -0
  8. package/dist/src/cli/parse-args.d.ts +6 -1
  9. package/dist/src/cli/parse-args.js +59 -0
  10. package/dist/src/cli/run.js +2 -2
  11. package/dist/src/cli/usage.js +5 -0
  12. package/dist/src/cli/write-artifacts.d.ts +1 -1
  13. package/dist/src/cli/write-artifacts.js +15 -2
  14. package/dist/src/core/compile.js +1 -0
  15. package/dist/src/expansion/op-expand-selected.js +8 -1
  16. package/dist/src/expansion/op-expansion.d.ts +3 -0
  17. package/dist/src/expansion/op-expansion.js +12 -1
  18. package/dist/src/index.d.ts +1 -1
  19. package/dist/src/node/source-host.js +5 -2
  20. package/dist/src/outputs/types.d.ts +13 -1
  21. package/dist/src/register-contracts/analyze-helpers.d.ts +6 -1
  22. package/dist/src/register-contracts/analyze-helpers.js +67 -0
  23. package/dist/src/register-contracts/analyze.d.ts +8 -1
  24. package/dist/src/register-contracts/analyze.js +353 -16
  25. package/dist/src/register-contracts/interfaceContracts.js +45 -0
  26. package/dist/src/register-contracts/liveness.js +23 -0
  27. package/dist/src/register-contracts/policy.d.ts +2 -0
  28. package/dist/src/register-contracts/policy.js +54 -0
  29. package/dist/src/register-contracts/programModel-boundaries.d.ts +5 -1
  30. package/dist/src/register-contracts/programModel-boundaries.js +20 -5
  31. package/dist/src/register-contracts/programModel-routines.js +37 -6
  32. package/dist/src/register-contracts/ratchet.d.ts +3 -0
  33. package/dist/src/register-contracts/ratchet.js +88 -0
  34. package/dist/src/register-contracts/report.d.ts +8 -1
  35. package/dist/src/register-contracts/report.js +174 -0
  36. package/dist/src/register-contracts/smartCommentParsing.js +22 -0
  37. package/dist/src/register-contracts/summary-boundary.js +9 -2
  38. package/dist/src/register-contracts/tooling.d.ts +2 -1
  39. package/dist/src/register-contracts/tooling.js +2 -0
  40. package/dist/src/register-contracts/types.d.ts +157 -0
  41. package/dist/src/source/logical-lines.d.ts +1 -0
  42. package/dist/src/source/source-span.d.ts +1 -0
  43. package/dist/src/syntax/parse-layout-declarations.js +1 -0
  44. package/dist/src/syntax/parse-line.js +4 -0
  45. package/docs/codebase/02-source-loading-and-parsing.md +10 -6
  46. package/docs/codebase/04-ops-and-register-contracts.md +49 -4
  47. package/docs/codebase/05-interfaces-and-output-artifacts.md +56 -6
  48. package/docs/codebase/06-verification-and-maintenance.md +10 -2
  49. package/docs/codebase/appendices/a-directory-file-reference.md +3 -1
  50. package/docs/codebase/appendices/b-compile-flow-reference.md +7 -5
  51. package/docs/codebase/appendices/c-public-surface-reference.md +19 -5
  52. 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, diagnosticsForConflicts, knownRoutineNames, outputCandidatesWithFixability, strictStackDiagnostics, summariesForAnnotations, } from './analyze-helpers.js';
7
- import { buildProfileSummaries, buildSummaries, buildSummaryByName, outputCandidateKey, unknownBoundaryDiagnostics, withAcceptedOutputs, } from './summaries.js';
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 autoAcceptedOutputs = autoAcceptedOutputCandidateMap(program.routines, outputCandidates, loaded.sourceTexts);
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, outputCandidates);
39
- const diagnostics = diagnosticsForConflicts(conflicts, options.mode);
40
- if (options.mode === 'strict') {
41
- diagnostics.push(...unknownBoundaryDiagnostics(program.directBoundaries, knownRoutines, 'error'));
42
- diagnostics.push(...strictStackDiagnostics(program.routines, summaries));
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
- conflicts,
50
- outputCandidates: outputCandidatesWithAutoFixability,
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
- const summariesForAnnotationsByName = summariesForAnnotations(summariesByName, outputCandidatesWithAutoFixability);
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, outputCandidatesWithAutoFixability, {
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
- outputCandidates: outputCandidatesWithAutoFixability,
66
- ...(options.emitReport ? { reportText: renderRegisterContractsReport(reportModel) } : {}),
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, file: string, line: number, column: number): void;
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, file, line, column) {
30
- boundaries.push({ target, subject, file, line, column });
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 entryNames = filesWithEntryLabels.has(item.span.sourceName)
56
- ? entriesByFile.get(item.span.sourceName)
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.span.sourceName, item.span.line, item.span.column);
76
+ pushDirectBoundary(directTailJumps, target, `JP ${target}`, effectiveInstructionSpan(item));
62
77
  }
63
78
  return directTailJumps;
64
79
  }