@jhlagado/azm 0.2.12 → 0.2.14

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